diff --git a/CMakeLists.txt b/CMakeLists.txt index 28d32d2..f39d0e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,13 +2,14 @@ cmake_minimum_required(VERSION 3.0) project(squawk) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 14) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) include(GNUInstallDirs) +include_directories(.) find_package(Qt5Widgets CONFIG REQUIRED) find_package(Qt5LinguistTools) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 46bf97a..fab36f2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -3,8 +3,10 @@ project(squawkCORE) set(CMAKE_AUTOMOC ON) -find_package(Qt5Widgets CONFIG REQUIRED) +find_package(Qt5Core CONFIG REQUIRED) +find_package(Qt5Gui CONFIG REQUIRED) find_package(Qt5Network CONFIG REQUIRED) +find_package(Qt5Xml CONFIG REQUIRED) set(squawkCORE_SRC squawk.cpp @@ -15,6 +17,7 @@ set(squawkCORE_SRC conference.cpp storage.cpp networkaccess.cpp + adapterFuctions.cpp ) # Tell CMake to create the helloworld executable @@ -30,5 +33,7 @@ endif() # Use the Widgets module from Qt 5. target_link_libraries(squawkCORE Qt5::Core) target_link_libraries(squawkCORE Qt5::Network) +target_link_libraries(squawkCORE Qt5::Gui) +target_link_libraries(squawkCORE Qt5::Xml) target_link_libraries(squawkCORE qxmpp) target_link_libraries(squawkCORE lmdb) diff --git a/core/account.cpp b/core/account.cpp index bacbb6d..e3dd29f 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -36,49 +36,99 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& am(new QXmppMamManager()), mm(new QXmppMucManager()), bm(new QXmppBookmarkManager()), + rm(client.findExtension()), + vm(client.findExtension()), contacts(), conferences(), maxReconnectTimes(0), reconnectTimes(0), queuedContacts(), - outOfRosterContacts() + outOfRosterContacts(), + avatarHash(), + avatarType(), + ownVCardRequestInProgress(false) { config.setUser(p_login); config.setDomain(p_server); config.setPassword(p_password); config.setAutoAcceptSubscriptions(true); - QObject::connect(&client, SIGNAL(connected()), this, SLOT(onClientConnected())); - QObject::connect(&client, SIGNAL(disconnected()), this, SLOT(onClientDisconnected())); - QObject::connect(&client, SIGNAL(presenceReceived(const QXmppPresence&)), this, SLOT(onPresenceReceived(const QXmppPresence&))); - QObject::connect(&client, SIGNAL(messageReceived(const QXmppMessage&)), this, SLOT(onMessageReceived(const QXmppMessage&))); - QObject::connect(&client, SIGNAL(error(QXmppClient::Error)), this, SLOT(onClientError(QXmppClient::Error))); + QObject::connect(&client, &QXmppClient::connected, this, &Account::onClientConnected); + QObject::connect(&client, &QXmppClient::disconnected, this, &Account::onClientDisconnected); + QObject::connect(&client, &QXmppClient::presenceReceived, this, &Account::onPresenceReceived); + QObject::connect(&client, &QXmppClient::messageReceived, this, &Account::onMessageReceived); + QObject::connect(&client, &QXmppClient::error, this, &Account::onClientError); - QXmppRosterManager& rm = client.rosterManager(); - - QObject::connect(&rm, SIGNAL(rosterReceived()), this, SLOT(onRosterReceived())); - QObject::connect(&rm, SIGNAL(itemAdded(const QString&)), this, SLOT(onRosterItemAdded(const QString&))); - QObject::connect(&rm, SIGNAL(itemRemoved(const QString&)), this, SLOT(onRosterItemRemoved(const QString&))); - QObject::connect(&rm, SIGNAL(itemChanged(const QString&)), this, SLOT(onRosterItemChanged(const QString&))); - //QObject::connect(&rm, SIGNAL(presenceChanged(const QString&, const QString&)), this, SLOT(onRosterPresenceChanged(const QString&, const QString&))); + QObject::connect(rm, &QXmppRosterManager::rosterReceived, this, &Account::onRosterReceived); + QObject::connect(rm, &QXmppRosterManager::itemAdded, this, &Account::onRosterItemAdded); + QObject::connect(rm, &QXmppRosterManager::itemRemoved, this, &Account::onRosterItemRemoved); + QObject::connect(rm, &QXmppRosterManager::itemChanged, this, &Account::onRosterItemChanged); + //QObject::connect(&rm, &QXmppRosterManager::presenceChanged, this, &Account::onRosterPresenceChanged); client.addExtension(cm); - QObject::connect(cm, SIGNAL(messageReceived(const QXmppMessage&)), this, SLOT(onCarbonMessageReceived(const QXmppMessage&))); - QObject::connect(cm, SIGNAL(messageSent(const QXmppMessage&)), this, SLOT(onCarbonMessageSent(const QXmppMessage&))); + QObject::connect(cm, &QXmppCarbonManager::messageReceived, this, &Account::onCarbonMessageReceived); + QObject::connect(cm, &QXmppCarbonManager::messageSent, this, &Account::onCarbonMessageSent); client.addExtension(am); - QObject::connect(am, SIGNAL(logMessage(QXmppLogger::MessageType, const QString&)), this, SLOT(onMamLog(QXmppLogger::MessageType, const QString))); - QObject::connect(am, SIGNAL(archivedMessageReceived(const QString&, const QXmppMessage&)), this, SLOT(onMamMessageReceived(const QString&, const QXmppMessage&))); - QObject::connect(am, SIGNAL(resultsRecieved(const QString&, const QXmppResultSetReply&, bool)), - this, SLOT(onMamResultsReceived(const QString&, const QXmppResultSetReply&, bool))); + QObject::connect(am, &QXmppMamManager::logMessage, this, &Account::onMamLog); + QObject::connect(am, &QXmppMamManager::archivedMessageReceived, this, &Account::onMamMessageReceived); + QObject::connect(am, &QXmppMamManager::resultsRecieved, this, &Account::onMamResultsReceived); client.addExtension(mm); - QObject::connect(mm, SIGNAL(roomAdded(QXmppMucRoom*)), this, SLOT(onMucRoomAdded(QXmppMucRoom*))); + QObject::connect(mm, &QXmppMucManager::roomAdded, this, &Account::onMucRoomAdded); client.addExtension(bm); - QObject::connect(bm, SIGNAL(bookmarksReceived(const QXmppBookmarkSet&)), this, SLOT(bookmarksReceived(const QXmppBookmarkSet&))); + QObject::connect(bm, &QXmppBookmarkManager::bookmarksReceived, this, &Account::bookmarksReceived); + + 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 + + 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); + } } Account::~Account() @@ -189,8 +239,10 @@ QString Core::Account::getServer() const void Core::Account::onRosterReceived() { - QXmppRosterManager& rm = client.rosterManager(); - QStringList bj = rm.getRosterBareJids(); + vm->requestClientVCard(); //TODO need to make sure server actually supports vCards + ownVCardRequestInProgress = true; + + QStringList bj = rm->getRosterBareJids(); for (int i = 0; i < bj.size(); ++i) { const QString& jid = bj[i]; addedAccount(jid); @@ -210,8 +262,7 @@ void Core::Account::onRosterItemAdded(const QString& bareJid) addedAccount(bareJid); std::map::const_iterator itr = queuedContacts.find(bareJid); if (itr != queuedContacts.end()) { - QXmppRosterManager& rm = client.rosterManager(); - rm.subscribe(bareJid, itr->second); + rm->subscribe(bareJid, itr->second); queuedContacts.erase(itr); } } @@ -224,8 +275,7 @@ void Core::Account::onRosterItemChanged(const QString& bareJid) return; } Contact* contact = itr->second; - QXmppRosterManager& rm = client.rosterManager(); - QXmppRosterIq::Item re = rm.getRosterEntry(bareJid); + QXmppRosterIq::Item re = rm->getRosterEntry(bareJid); Shared::SubscriptionState state = castSubscriptionState(re.subscriptionType()); @@ -254,9 +304,8 @@ void Core::Account::onRosterItemRemoved(const QString& bareJid) void Core::Account::addedAccount(const QString& jid) { - QXmppRosterManager& rm = client.rosterManager(); std::map::const_iterator itr = contacts.find(jid); - QXmppRosterIq::Item re = rm.getRosterEntry(jid); + QXmppRosterIq::Item re = rm->getRosterEntry(jid); Contact* contact; bool newContact = false; if (itr == contacts.end()) { @@ -279,6 +328,19 @@ void Core::Account::addedAccount(const QString& jid) {"name", re.name()}, {"state", state} }); + + if (contact->hasAvatar()) { + if (!contact->isAvatarAutoGenerated()) { + cData.insert("avatarState", static_cast(Shared::Avatar::valid)); + } else { + cData.insert("avatarState", static_cast(Shared::Avatar::autocreated)); + } + cData.insert("avatarPath", contact->avatarPath()); + } else { + cData.insert("avatarState", static_cast(Shared::Avatar::empty)); + cData.insert("avatarPath", ""); + requestVCard(jid); + } int grCount = 0; for (QSet::const_iterator itr = gr.begin(), end = gr.end(); itr != end; ++itr) { const QString& groupName = *itr; @@ -296,39 +358,35 @@ void Core::Account::addedAccount(const QString& jid) void Core::Account::handleNewRosterItem(Core::RosterItem* contact) { - - QObject::connect(contact, SIGNAL(needHistory(const QString&, const QString&, const QDateTime&)), this, SLOT(onContactNeedHistory(const QString&, const QString&, const QDateTime&))); - QObject::connect(contact, SIGNAL(historyResponse(const std::list&)), this, SLOT(onContactHistoryResponse(const std::list&))); - QObject::connect(contact, SIGNAL(nameChanged(const QString&)), this, SLOT(onContactNameChanged(const QString&))); + QObject::connect(contact, &RosterItem::needHistory, this, &Account::onContactNeedHistory); + QObject::connect(contact, &RosterItem::historyResponse, this, &Account::onContactHistoryResponse); + QObject::connect(contact, &RosterItem::nameChanged, this, &Account::onContactNameChanged); + QObject::connect(contact, &RosterItem::avatarChanged, this, &Account::onContactAvatarChanged); } void Core::Account::handleNewContact(Core::Contact* contact) { handleNewRosterItem(contact); - QObject::connect(contact, SIGNAL(groupAdded(const QString&)), this, SLOT(onContactGroupAdded(const QString&))); - QObject::connect(contact, SIGNAL(groupRemoved(const QString&)), this, SLOT(onContactGroupRemoved(const QString&))); - QObject::connect(contact, SIGNAL(subscriptionStateChanged(Shared::SubscriptionState)), - this, SLOT(onContactSubscriptionStateChanged(Shared::SubscriptionState))); + QObject::connect(contact, &Contact::groupAdded, this, &Account::onContactGroupAdded); + QObject::connect(contact, &Contact::groupRemoved, this, &Account::onContactGroupRemoved); + QObject::connect(contact, &Contact::subscriptionStateChanged, this, &Account::onContactSubscriptionStateChanged); } void Core::Account::handleNewConference(Core::Conference* contact) { handleNewRosterItem(contact); - QObject::connect(contact, SIGNAL(nickChanged(const QString&)), this, SLOT(onMucNickNameChanged(const QString&))); - QObject::connect(contact, SIGNAL(subjectChanged(const QString&)), this, SLOT(onMucSubjectChanged(const QString&))); - QObject::connect(contact, SIGNAL(joinedChanged(bool)), this, SLOT(onMucJoinedChanged(bool))); - QObject::connect(contact, SIGNAL(autoJoinChanged(bool)), this, SLOT(onMucAutoJoinChanged(bool))); - QObject::connect(contact, SIGNAL(addParticipant(const QString&, const QMap&)), - this, SLOT(onMucAddParticipant(const QString&, const QMap&))); - QObject::connect(contact, SIGNAL(changeParticipant(const QString&, const QMap&)), - this, SLOT(onMucChangeParticipant(const QString&, const QMap&))); - QObject::connect(contact, SIGNAL(removeParticipant(const QString&)), this, SLOT(onMucRemoveParticipant(const QString&))); + QObject::connect(contact, &Conference::nickChanged, this, &Account::onMucNickNameChanged); + QObject::connect(contact, &Conference::subjectChanged, this, &Account::onMucSubjectChanged); + QObject::connect(contact, &Conference::joinedChanged, this, &Account::onMucJoinedChanged); + QObject::connect(contact, &Conference::autoJoinChanged, this, &Account::onMucAutoJoinChanged); + QObject::connect(contact, &Conference::addParticipant, this, &Account::onMucAddParticipant); + QObject::connect(contact, &Conference::changeParticipant, this, &Account::onMucChangeParticipant); + QObject::connect(contact, &Conference::removeParticipant, this, &Account::onMucRemoveParticipant); } - -void Core::Account::onPresenceReceived(const QXmppPresence& presence) +void Core::Account::onPresenceReceived(const QXmppPresence& p_presence) { - QString id = presence.from(); + QString id = p_presence.from(); QStringList comps = id.split("/"); QString jid = comps.front(); QString resource = comps.back(); @@ -337,25 +395,75 @@ void Core::Account::onPresenceReceived(const QXmppPresence& presence) if (jid == myJid) { if (resource == getResource()) { - emit availabilityChanged(presence.availableStatusType()); + emit availabilityChanged(p_presence.availableStatusType()); } else { - qDebug() << "Received a presence for another resource of my " << name << " account, skipping"; + 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; + } + } + } + } else { + if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) { + std::map::const_iterator itr = contacts.find(jid); + if (itr != contacts.end()) { + Contact* cnt = itr->second; + 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 (!cnt->hasAvatar() || (cnt->hasAvatar() && !cnt->isAvatarAutoGenerated())) { + cnt->setAutoGeneratedAvatar(); + } + break; + case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load + if (cnt->hasAvatar()) { + if (cnt->isAvatarAutoGenerated()) { + requestVCard(jid); + } else { + if (cnt->avatarHash() != p_presence.photoHash()) { + requestVCard(jid); + } + } + } else { + requestVCard(jid); + } + break; + } + } } } - switch (presence.type()) { + switch (p_presence.type()) { case QXmppPresence::Error: - qDebug() << "An error reported by presence from " << id; + qDebug() << "An error reported by presence from" << id << p_presence.error().text(); break; case QXmppPresence::Available:{ - QDateTime lastInteraction = presence.lastUserInteraction(); + QDateTime lastInteraction = p_presence.lastUserInteraction(); if (!lastInteraction.isValid()) { lastInteraction = QDateTime::currentDateTime(); } emit addPresence(jid, resource, { {"lastActivity", lastInteraction}, - {"availability", presence.availableStatusType()}, //TODO check and handle invisible - {"status", presence.statusText()} + {"availability", p_presence.availableStatusType()}, //TODO check and handle invisible + {"status", p_presence.statusText()} }); } break; @@ -380,7 +488,7 @@ void Core::Account::onRosterPresenceChanged(const QString& bareJid, const QStrin { //not used for now; qDebug() << "presence changed for " << bareJid << " resource " << resource; - const QXmppPresence& presence = client.rosterManager().getPresence(bareJid, resource); + const QXmppPresence& presence = rm->getPresence(bareJid, resource); } void Core::Account::setLogin(const QString& p_login) @@ -940,8 +1048,7 @@ void Core::Account::onClientError(QXmppClient::Error err) void Core::Account::subscribeToContact(const QString& jid, const QString& reason) { if (state == Shared::connected) { - QXmppRosterManager& rm = client.rosterManager(); - rm.subscribe(jid, reason); + rm->subscribe(jid, reason); } else { qDebug() << "An attempt to subscribe account " << name << " to contact " << jid << " but the account is not in the connected state, skipping"; } @@ -950,8 +1057,7 @@ void Core::Account::subscribeToContact(const QString& jid, const QString& reason void Core::Account::unsubscribeFromContact(const QString& jid, const QString& reason) { if (state == Shared::connected) { - QXmppRosterManager& rm = client.rosterManager(); - rm.unsubscribe(jid, reason); + rm->unsubscribe(jid, reason); } else { qDebug() << "An attempt to unsubscribe account " << name << " from contact " << jid << " but the account is not in the connected state, skipping"; } @@ -965,8 +1071,7 @@ void Core::Account::removeContactRequest(const QString& jid) outOfRosterContacts.erase(itr); onRosterItemRemoved(jid); } else { - QXmppRosterManager& rm = client.rosterManager(); - rm.removeItem(jid); + rm->removeItem(jid); } } else { qDebug() << "An attempt to remove contact " << jid << " from account " << name << " but the account is not in the connected state, skipping"; @@ -982,8 +1087,7 @@ void Core::Account::addContactRequest(const QString& jid, const QString& name, c qDebug() << "An attempt to add contact " << jid << " to account " << name << " but the account is already queued for adding, skipping"; } else { queuedContacts.insert(std::make_pair(jid, "")); //TODO need to add reason here; - QXmppRosterManager& rm = client.rosterManager(); - rm.addItem(jid, name, groups); + rm->addItem(jid, name, groups); } } else { qDebug() << "An attempt to add contact " << jid << " to account " << name << " but the account is not in the connected state, skipping"; @@ -1160,8 +1264,7 @@ void Core::Account::addContactToGroupRequest(const QString& jid, const QString& if (itr == contacts.end()) { qDebug() << "An attempt to add non existing contact" << jid << "of account" << name << "to the group" << groupName << ", skipping"; } else { - QXmppRosterManager& rm = client.rosterManager(); - QXmppRosterIq::Item item = rm.getRosterEntry(jid); + QXmppRosterIq::Item item = rm->getRosterEntry(jid); QSet groups = item.groups(); if (groups.find(groupName) == groups.end()) { //TODO need to change it, I guess that sort of code is better in qxmpp lib groups.insert(groupName); @@ -1183,8 +1286,7 @@ void Core::Account::removeContactFromGroupRequest(const QString& jid, const QStr if (itr == contacts.end()) { qDebug() << "An attempt to remove non existing contact" << jid << "of account" << name << "from the group" << groupName << ", skipping"; } else { - QXmppRosterManager& rm = client.rosterManager(); - QXmppRosterIq::Item item = rm.getRosterEntry(jid); + QXmppRosterIq::Item item = rm->getRosterEntry(jid); QSet groups = item.groups(); QSet::const_iterator gItr = groups.find(groupName); if (gItr != groups.end()) { @@ -1207,7 +1309,245 @@ void Core::Account::renameContactRequest(const QString& jid, const QString& newN if (itr == contacts.end()) { qDebug() << "An attempt to rename non existing contact" << jid << "of account" << name << ", skipping"; } else { - QXmppRosterManager& rm = client.rosterManager(); - rm.renameItem(jid, newName); + rm->renameItem(jid, newName); } } + +void Core::Account::onVCardReceived(const QXmppVCardIq& card) +{ + QString jid = card.from(); + pendingVCardRequests.erase(jid); + RosterItem* item = 0; + + std::map::const_iterator contItr = contacts.find(jid); + if (contItr == contacts.end()) { + std::map::const_iterator confItr = conferences.find(jid); + if (confItr == conferences.end()) { + if (jid == getLogin() + "@" + getServer()) { + onOwnVCardReceived(card); + } else { + qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping"; + } + return; + } else { + item = confItr->second; + } + } else { + item = contItr->second; + } + + QByteArray ava = card.photo(); + + if (ava.size() > 0) { + item->setAvatar(ava); + } else { + if (!item->hasAvatar() || !item->isAvatarAutoGenerated()) { + item->setAutoGeneratedAvatar(); + } + } + + Shared::VCard vCard; + initializeVCard(vCard, card); + + if (item->hasAvatar()) { + if (!item->isAvatarAutoGenerated()) { + vCard.setAvatarType(Shared::Avatar::valid); + } else { + vCard.setAvatarType(Shared::Avatar::autocreated); + } + vCard.setAvatarPath(item->avatarPath()); + } else { + vCard.setAvatarType(Shared::Avatar::empty); + } + + QMap cd = { + {"avatarState", static_cast(vCard.getAvatarType())}, + {"avatarPath", vCard.getAvatarPath()} + }; + emit changeContact(jid, cd); + emit receivedVCard(jid, vCard); +} + +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 +{ + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/" + "avatar." + avatarType; +} + +void Core::Account::onContactAvatarChanged(Shared::Avatar type, const QString& path) +{ + RosterItem* item = static_cast(sender()); + QMap cData({ + {"avatarState", static_cast(type)}, + {"avatarPath", path} + }); + + emit changeContact(item->jid, cData); +} + +void Core::Account::requestVCard(const QString& jid) +{ + if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) { + 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); + + bool avatarChanged = false; + if (card.getAvatarType() == Shared::Avatar::empty) { + if (avatarType.size() > 0) { + avatarChanged = true; + } + } else { + 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"; + avatarChanged = true; + } else { + data = oA.readAll(); + } + } + } else { + data = avatar.readAll(); + avatarChanged = true; + } + } 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"; + avatarChanged = true; + } 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); +} diff --git a/core/account.h b/core/account.h index 21f35d3..a5f07bd 100644 --- a/core/account.h +++ b/core/account.h @@ -19,7 +19,13 @@ #ifndef CORE_ACCOUNT_H #define CORE_ACCOUNT_H -#include +#include +#include +#include +#include +#include +#include + #include #include @@ -30,6 +36,8 @@ #include #include #include +#include +#include #include "../global.h" #include "contact.h" #include "conference.h" @@ -54,6 +62,7 @@ public: QString getServer() const; QString getPassword() const; QString getResource() const; + QString getAvatarPath() const; Shared::Availability getAvailability() const; void setName(const QString& p_name); @@ -78,8 +87,11 @@ public: void setRoomAutoJoin(const QString& jid, bool joined); void removeRoomRequest(const QString& jid); void addRoomRequest(const QString& jid, const QString& nick, const QString& password, bool autoJoin); + void requestVCard(const QString& jid); + void uploadVCard(const Shared::VCard& card); signals: + void changed(const QMap& data); void connectionStateChanged(int); void availabilityChanged(int); void addGroup(const QString& name); @@ -99,6 +111,7 @@ signals: void addRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void changeRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void removeRoomParticipant(const QString& jid, const QString& nickName); + void receivedVCard(const QString& jid, const Shared::VCard& card); private: QString name; @@ -112,6 +125,8 @@ private: QXmppMamManager* am; QXmppMucManager* mm; QXmppBookmarkManager* bm; + QXmppRosterManager* rm; + QXmppVCardManager* vm; std::map contacts; std::map conferences; unsigned int maxReconnectTimes; @@ -119,6 +134,11 @@ private: std::map queuedContacts; std::set outOfRosterContacts; + std::set pendingVCardRequests; + + QString avatarHash; + QString avatarType; + bool ownVCardRequestInProgress; private slots: void onClientConnected(); @@ -157,8 +177,12 @@ private slots: void onContactSubscriptionStateChanged(Shared::SubscriptionState state); void onContactHistoryResponse(const std::list& list); void onContactNeedHistory(const QString& before, const QString& after, const QDateTime& at); + void onContactAvatarChanged(Shared::Avatar, const QString& path); void onMamLog(QXmppLogger::MessageType type, const QString &msg); + + void onVCardReceived(const QXmppVCardIq& card); + void onOwnVCardReceived(const QXmppVCardIq& card); private: void addedAccount(const QString &bareJid); @@ -178,6 +202,8 @@ private: }; +void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card); +void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard); } diff --git a/core/adapterFuctions.cpp b/core/adapterFuctions.cpp new file mode 100644 index 0000000..e2559d8 --- /dev/null +++ b/core/adapterFuctions.cpp @@ -0,0 +1,275 @@ +/* + * 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 "account.h" + +void Core::initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card) +{ + vCard.setFullName(card.fullName()); + vCard.setFirstName(card.firstName()); + vCard.setMiddleName(card.middleName()); + vCard.setLastName(card.lastName()); + vCard.setBirthday(card.birthday()); + vCard.setNickName(card.nickName()); + vCard.setDescription(card.description()); + vCard.setUrl(card.url()); + QXmppVCardOrganization org = card.organization(); + vCard.setOrgName(org.organization()); + vCard.setOrgRole(org.role()); + vCard.setOrgUnit(org.unit()); + vCard.setOrgTitle(org.title()); + + QList emails = card.emails(); + std::deque& myEmails = vCard.getEmails(); + for (const QXmppVCardEmail& em : emails) { + QString addr = em.address(); + if (addr.size() != 0) { + QXmppVCardEmail::Type et = em.type(); + bool prefered = false; + bool accounted = false; + if (et & QXmppVCardEmail::Preferred) { + prefered = true; + } + if (et & QXmppVCardEmail::Home) { + myEmails.emplace_back(addr, Shared::VCard::Email::home, prefered); + accounted = true; + } + if (et & QXmppVCardEmail::Work) { + myEmails.emplace_back(addr, Shared::VCard::Email::work, prefered); + accounted = true; + } + if (!accounted) { + myEmails.emplace_back(addr, Shared::VCard::Email::none, prefered); + } + } + + } + + QList phones = card.phones(); + std::deque& myPhones = vCard.getPhones(); + for (const QXmppVCardPhone& ph : phones) { + QString num = ph.number(); + if (num.size() != 0) { + QXmppVCardPhone::Type pt = ph.type(); + bool prefered = false; + bool accounted = false; + if (pt & QXmppVCardPhone::Preferred) { + prefered = true; + } + + bool home = false; + bool work = false; + + if (pt & QXmppVCardPhone::Home) { + home = true; + } + if (pt & QXmppVCardPhone::Work) { + work = true; + } + + + if (pt & QXmppVCardPhone::Fax) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::fax, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::fax, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::fax, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (pt & QXmppVCardPhone::Voice) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::voice, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::voice, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::voice, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (pt & QXmppVCardPhone::Pager) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::pager, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::pager, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::pager, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (pt & QXmppVCardPhone::Cell) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::cell, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::cell, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::cell, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (pt & QXmppVCardPhone::Video) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::video, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::video, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::video, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (pt & QXmppVCardPhone::Modem) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::modem, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::modem, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::modem, Shared::VCard::Phone::none, prefered); + } + accounted = true; + } + if (!accounted) { + if (home || work) { + if (home) { + myPhones.emplace_back(num, Shared::VCard::Phone::other, Shared::VCard::Phone::home, prefered); + } + if (work) { + myPhones.emplace_back(num, Shared::VCard::Phone::other, Shared::VCard::Phone::work, prefered); + } + } else { + myPhones.emplace_back(num, Shared::VCard::Phone::other, Shared::VCard::Phone::none, prefered); + } + } + } + } +} + +void Core::initializeQXmppVCard(QXmppVCardIq& iq, const Shared::VCard& card) { + iq.setFullName(card.getFullName()); + iq.setFirstName(card.getFirstName()); + iq.setMiddleName(card.getMiddleName()); + iq.setLastName(card.getLastName()); + iq.setNickName(card.getNickName()); + iq.setBirthday(card.getBirthday()); + iq.setDescription(card.getDescription()); + iq.setUrl(card.getUrl()); + QXmppVCardOrganization org; + org.setOrganization(card.getOrgName()); + org.setUnit(card.getOrgUnit()); + org.setRole(card.getOrgRole()); + org.setTitle(card.getOrgTitle()); + iq.setOrganization(org); + + const std::deque& myEmails = card.getEmails(); + QList emails; + for (const Shared::VCard::Email& mEm : myEmails) { + QXmppVCardEmail em; + QXmppVCardEmail::Type t = QXmppVCardEmail::Internet; + if (mEm.prefered) { + t = t | QXmppVCardEmail::Preferred; + } + if (mEm.role == Shared::VCard::Email::home) { + t = t | QXmppVCardEmail::Home; + } else if (mEm.role == Shared::VCard::Email::work) { + t = t | QXmppVCardEmail::Work; + } + em.setType(t); + em.setAddress(mEm.address); + + emails.push_back(em); + } + + std::map phones; + QList phs; + const std::deque& myPhones = card.getPhones(); + for (const Shared::VCard::Phone& mPh : myPhones) { + std::map::iterator itr = phones.find(mPh.number); + if (itr == phones.end()) { + itr = phones.emplace(mPh.number, QXmppVCardPhone()).first; + } + QXmppVCardPhone& phone = itr->second; + phone.setNumber(mPh.number); + + switch (mPh.type) { + case Shared::VCard::Phone::fax: + phone.setType(phone.type() | QXmppVCardPhone::Fax); + break; + case Shared::VCard::Phone::pager: + phone.setType(phone.type() | QXmppVCardPhone::Pager); + break; + case Shared::VCard::Phone::voice: + phone.setType(phone.type() | QXmppVCardPhone::Voice); + break; + case Shared::VCard::Phone::cell: + phone.setType(phone.type() | QXmppVCardPhone::Cell); + break; + case Shared::VCard::Phone::video: + phone.setType(phone.type() | QXmppVCardPhone::Video); + break; + case Shared::VCard::Phone::modem: + phone.setType(phone.type() | QXmppVCardPhone::Modem); + break; + case Shared::VCard::Phone::other: + phone.setType(phone.type() | QXmppVCardPhone::PCS); //loss of information, but I don't even know what the heck is this type of phone! + break; + } + + switch (mPh.role) { + case Shared::VCard::Phone::home: + phone.setType(phone.type() | QXmppVCardPhone::Home); + break; + case Shared::VCard::Phone::work: + phone.setType(phone.type() | QXmppVCardPhone::Work); + break; + default: + break; + } + + if (mPh.prefered) { + phone.setType(phone.type() | QXmppVCardPhone::Preferred); + } + } + for (const std::pair& phone : phones) { + phs.push_back(phone.second); + } + + iq.setEmails(emails); + iq.setPhones(phs); +} + +#endif // CORE_ADAPTER_FUNCTIONS_H diff --git a/core/archive.cpp b/core/archive.cpp index 00139ac..5900df2 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -32,7 +32,11 @@ Core::Archive::Archive(const QString& p_jid, QObject* parent): environment(), main(), order(), - stats() + stats(), + hasAvatar(false), + avatarAutoGenerated(false), + avatarHash(), + avatarType() { } @@ -66,7 +70,49 @@ void Core::Archive::open(const QString& account) mdb_dbi_open(txn, "order", MDB_CREATE | MDB_INTEGERKEY, &order); mdb_dbi_open(txn, "stats", MDB_CREATE, &stats); mdb_txn_commit(txn); - fromTheBeginning = _isFromTheBeginning(); + + mdb_txn_begin(environment, NULL, 0, &txn); + try { + fromTheBeginning = getStatBoolValue("beginning", txn); + } catch (NotFound e) { + fromTheBeginning = false; + } + try { + hasAvatar = getStatBoolValue("hasAvatar", txn); + } catch (NotFound e) { + hasAvatar = false; + } + if (hasAvatar) { + try { + avatarAutoGenerated = getStatBoolValue("avatarAutoGenerated", txn); + } catch (NotFound e) { + avatarAutoGenerated = false; + } + + avatarType = getStatStringValue("avatarType", txn).c_str(); + if (avatarAutoGenerated) { + avatarHash = ""; + } else { + avatarHash = getStatStringValue("avatarHash", txn).c_str(); + } + } else { + avatarAutoGenerated = false; + avatarHash = ""; + avatarType = ""; + } + mdb_txn_abort(txn); + + if (hasAvatar) { + QFile ava(path + "/avatar." + avatarType); + if (!ava.exists()) { + bool success = dropAvatar(); + if (!success) { + qDebug() << "error opening archive" << jid << "for account" << account + << ". There is supposed to be avatar but the file doesn't exist, couldn't even drop it, it surely will lead to an error"; + } + } + } + opened = true; } } @@ -396,40 +442,6 @@ std::list Core::Archive::getBefore(int count, const QString& id return res; } -bool Core::Archive::_isFromTheBeginning() -{ - std::string strKey = "beginning"; - - MDB_val lmdbKey, lmdbData; - lmdbKey.mv_size = strKey.size(); - lmdbKey.mv_data = (char*)strKey.c_str(); - - MDB_txn *txn; - int rc; - mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); - rc = mdb_get(txn, stats, &lmdbKey, &lmdbData); - if (rc == MDB_NOTFOUND) { - mdb_txn_abort(txn); - return false; - } else if (rc) { - qDebug() <<"isFromTheBeginning error: " << mdb_strerror(rc); - mdb_txn_abort(txn); - throw NotFound(strKey, jid.toStdString()); - } else { - uint8_t value = *(uint8_t*)(lmdbData.mv_data); - bool is; - if (value == 144) { - is = false; - } else if (value == 72) { - is = true; - } else { - qDebug() <<"isFromTheBeginning error: stored value doesn't match any magic number, the answer is most probably wrong"; - } - mdb_txn_abort(txn); - return is; - } -} - bool Core::Archive::isFromTheBeginning() { if (!opened) { @@ -445,26 +457,15 @@ void Core::Archive::setFromTheBeginning(bool is) } if (fromTheBeginning != is) { fromTheBeginning = is; - const std::string& id = "beginning"; - uint8_t value = 144; - if (is) { - value = 72; - } - MDB_val lmdbKey, lmdbData; - lmdbKey.mv_size = id.size(); - lmdbKey.mv_data = (char*)id.c_str(); - lmdbData.mv_size = sizeof value; - lmdbData.mv_data = &value; MDB_txn *txn; mdb_txn_begin(environment, NULL, 0, &txn); - int rc; - rc = mdb_put(txn, stats, &lmdbKey, &lmdbData, 0); - if (rc != 0) { - qDebug() << "Couldn't store beginning key into stat database:" << mdb_strerror(rc); + bool success = setStatValue("beginning", is, txn); + if (success != 0) { mdb_txn_abort(txn); + } else { + mdb_txn_commit(txn); } - mdb_txn_commit(txn); } } @@ -508,3 +509,221 @@ void Core::Archive::printKeys() mdb_cursor_close(cursor); mdb_txn_abort(txn); } + +bool Core::Archive::getStatBoolValue(const std::string& id, MDB_txn* txn) +{ + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + + int rc; + rc = mdb_get(txn, stats, &lmdbKey, &lmdbData); + if (rc == MDB_NOTFOUND) { + throw NotFound(id, jid.toStdString()); + } else if (rc) { + qDebug() << "error retrieving" << id.c_str() << "from stats db of" << jid << mdb_strerror(rc); + throw 15; //TODO proper exception + } else { + uint8_t value = *(uint8_t*)(lmdbData.mv_data); + bool is; + if (value == 144) { + is = false; + } else if (value == 72) { + is = true; + } else { + qDebug() << "error retrieving boolean stat" << id.c_str() << ": stored value doesn't match any magic number, the answer is most probably wrong"; + throw NotFound(id, jid.toStdString()); + } + return is; + } +} + +std::string Core::Archive::getStatStringValue(const std::string& id, MDB_txn* txn) +{ + MDB_cursor* cursor; + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + + int rc; + rc = mdb_get(txn, stats, &lmdbKey, &lmdbData); + if (rc == MDB_NOTFOUND) { + throw NotFound(id, jid.toStdString()); + } else if (rc) { + qDebug() << "error retrieving" << id.c_str() << "from stats db of" << jid << mdb_strerror(rc); + throw 15; //TODO proper exception + } else { + std::string value((char*)lmdbData.mv_data, lmdbData.mv_size); + return value; + } +} + +bool Core::Archive::setStatValue(const std::string& id, bool value, MDB_txn* txn) +{ + uint8_t binvalue = 144; + if (value) { + binvalue = 72; + } + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = sizeof binvalue; + lmdbData.mv_data = &binvalue; + int rc = mdb_put(txn, stats, &lmdbKey, &lmdbData, 0); + if (rc != 0) { + qDebug() << "Couldn't store" << id.c_str() << "key into stat database:" << mdb_strerror(rc); + return false; + } + return true; +} + +bool Core::Archive::setStatValue(const std::string& id, const std::string& value, MDB_txn* txn) +{ + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = value.size(); + lmdbData.mv_data = (char*)value.c_str(); + int rc = mdb_put(txn, stats, &lmdbKey, &lmdbData, 0); + if (rc != 0) { + qDebug() << "Couldn't store" << id.c_str() << "key into stat database:" << mdb_strerror(rc); + return false; + } + return true; +} + +bool Core::Archive::getHasAvatar() const +{ + if (!opened) { + throw Closed("getHasAvatar", jid.toStdString()); + } + + return hasAvatar; +} + +bool Core::Archive::getAutoAvatar() const +{ + if (!opened) { + throw Closed("getAutoAvatar", jid.toStdString()); + } + + return avatarAutoGenerated; +} + +QString Core::Archive::getAvatarHash() const +{ + if (!opened) { + throw Closed("getAvatarHash", jid.toStdString()); + } + + return avatarHash; +} + +QString Core::Archive::getAvatarType() const +{ + if (!opened) { + throw Closed("getAvatarType", jid.toStdString()); + } + + return avatarType; +} + +bool Core::Archive::dropAvatar() +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + bool success = setStatValue("hasAvatar", false, txn); + success = success && setStatValue("avatarAutoGenerated", false, txn); + success = success && setStatValue("avatarHash", "", txn); + success = success && setStatValue("avatarType", "", txn); + if (!success) { + mdb_txn_abort(txn); + return false; + } else { + hasAvatar = false; + avatarAutoGenerated = false; + avatarHash = ""; + avatarType = ""; + mdb_txn_commit(txn); + return true; + } +} + +bool Core::Archive::setAvatar(const QByteArray& data, bool generated) +{ + if (!opened) { + throw Closed("setAvatar", jid.toStdString()); + } + + if (data.size() == 0) { + if (!hasAvatar) { + return false; + } else { + return dropAvatar(); + } + } else { + const char* cep; + mdb_env_get_path(environment, &cep); + QString currentPath(cep); + bool needToRemoveOld = false; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(data); + QString newHash(hash.result()); + if (hasAvatar) { + if (!generated && !avatarAutoGenerated && avatarHash == newHash) { + return false; + } + QFile oldAvatar(currentPath + "/avatar." + avatarType); + if (oldAvatar.exists()) { + if (oldAvatar.rename(currentPath + "/avatar." + avatarType + ".bak")) { + needToRemoveOld = true; + } else { + qDebug() << "Can't change avatar: couldn't get rid of the old avatar" << oldAvatar.fileName(); + return false; + } + } + } + QMimeDatabase db; + QMimeType type = db.mimeTypeForData(data); + QString ext = type.preferredSuffix(); + QFile newAvatar(currentPath + "/avatar." + ext); + if (newAvatar.open(QFile::WriteOnly)) { + newAvatar.write(data); + newAvatar.close(); + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + bool success = setStatValue("hasAvatar", true, txn); + success = success && setStatValue("avatarAutoGenerated", generated, txn); + success = success && setStatValue("avatarHash", newHash.toStdString(), txn); + success = success && setStatValue("avatarType", ext.toStdString(), txn); + if (!success) { + qDebug() << "Can't change avatar: couldn't store changes to database for" << newAvatar.fileName() << "rolling back to the previous state"; + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.rename(currentPath + "/avatar." + avatarType); + } + mdb_txn_abort(txn); + return false; + } else { + hasAvatar = true; + avatarAutoGenerated = generated; + avatarHash = newHash; + avatarType = ext; + mdb_txn_commit(txn); + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.remove(); + } + return true; + } + } else { + qDebug() << "Can't change avatar: cant open file to write" << newAvatar.fileName() << "rolling back to the previous state"; + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.rename(currentPath + "/avatar." + avatarType); + } + return false; + } + } +} diff --git a/core/archive.h b/core/archive.h index 58a5f8d..e94fac8 100644 --- a/core/archive.h +++ b/core/archive.h @@ -20,6 +20,10 @@ #define CORE_ARCHIVE_H #include +#include +#include +#include + #include "../global.h" #include #include "../exception.h" @@ -49,6 +53,11 @@ public: std::list getBefore(int count, const QString& id); bool isFromTheBeginning(); void setFromTheBeginning(bool is); + bool getHasAvatar() const; + bool getAutoAvatar() const; + QString getAvatarHash() const; + QString getAvatarType() const; + bool setAvatar(const QByteArray& data, bool generated = false); public: const QString jid; @@ -131,10 +140,19 @@ private: MDB_dbi main; MDB_dbi order; MDB_dbi stats; + bool hasAvatar; + bool avatarAutoGenerated; + QString avatarHash; + QString avatarType; - bool _isFromTheBeginning(); + bool getStatBoolValue(const std::string& id, MDB_txn* txn); + std::string getStatStringValue(const std::string& id, MDB_txn* txn); + + bool setStatValue(const std::string& id, bool value, MDB_txn* txn); + bool setStatValue(const std::string& id, const std::string& value, MDB_txn* txn); void printOrder(); void printKeys(); + bool dropAvatar(); }; } diff --git a/core/conference.cpp b/core/conference.cpp index 1305419..56a1e8f 100644 --- a/core/conference.cpp +++ b/core/conference.cpp @@ -30,15 +30,15 @@ Core::Conference::Conference(const QString& p_jid, const QString& p_account, boo muc = true; name = p_name; - connect(room, SIGNAL(joined()), this, SLOT(onRoomJoined())); - connect(room, SIGNAL(left()), this, SLOT(onRoomLeft())); - connect(room, SIGNAL(nameChanged(const QString&)), this, SLOT(onRoomNameChanged(const QString&))); - connect(room, SIGNAL(subjectChanged(const QString&)), this, SLOT(onRoomSubjectChanged(const QString&))); - connect(room, SIGNAL(participantAdded(const QString&)), this, SLOT(onRoomParticipantAdded(const QString&))); - connect(room, SIGNAL(participantChanged(const QString&)), this, SLOT(onRoomParticipantChanged(const QString&))); - connect(room, SIGNAL(participantRemoved(const QString&)), this, SLOT(onRoomParticipantRemoved(const QString&))); - connect(room, SIGNAL(nickNameChanged(const QString&)), this, SLOT(onRoomNickNameChanged(const QString&))); - connect(room, SIGNAL(error(const QXmppStanza::Error&)), this, SLOT(onRoomError(const QXmppStanza::Error&))); + connect(room, &QXmppMucRoom::joined, this, &Conference::onRoomJoined); + connect(room, &QXmppMucRoom::left, this, &Conference::onRoomLeft); + connect(room, &QXmppMucRoom::nameChanged, this, &Conference::onRoomNameChanged); + connect(room, &QXmppMucRoom::subjectChanged, this, &Conference::onRoomSubjectChanged); + connect(room, &QXmppMucRoom::participantAdded, this, &Conference::onRoomParticipantAdded); + connect(room, &QXmppMucRoom::participantChanged, this, &Conference::onRoomParticipantChanged); + connect(room, &QXmppMucRoom::participantRemoved, this, &Conference::onRoomParticipantRemoved); + connect(room, &QXmppMucRoom::nickNameChanged, this, &Conference::onRoomNickNameChanged); + connect(room, &QXmppMucRoom::error, this, &Conference::onRoomError); room->setNickName(nick); if (autoJoin) { diff --git a/core/networkaccess.cpp b/core/networkaccess.cpp index 002f9d7..090f4e7 100644 --- a/core/networkaccess.cpp +++ b/core/networkaccess.cpp @@ -323,9 +323,9 @@ void Core::NetworkAccess::startDownload(const QString& messageId, const QString& Download* dwn = new Download({{messageId}, 0, 0, true}); QNetworkRequest req(url); dwn->reply = manager->get(req); - connect(dwn->reply, SIGNAL(downloadProgress(qint64, qint64)), SLOT(onDownloadProgress(qint64, qint64))); - connect(dwn->reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onRequestError(QNetworkReply::NetworkError))); - connect(dwn->reply, SIGNAL(finished()), SLOT(onRequestFinished())); + connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress); + connect(dwn->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onRequestError); + connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onRequestFinished); downloads.insert(std::make_pair(url, dwn)); emit downloadFileProgress(messageId, 0); } diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index be6ceee..f144901 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -20,9 +20,10 @@ #include -Core::RosterItem::RosterItem(const QString& pJid, const QString& account, QObject* parent): +Core::RosterItem::RosterItem(const QString& pJid, const QString& pAccount, QObject* parent): QObject(parent), jid(pJid), + account(pAccount), name(), archiveState(empty), archive(new Archive(jid)), @@ -331,3 +332,63 @@ bool Core::RosterItem::isMuc() const { return muc; } + +QString Core::RosterItem::avatarHash() const +{ + return archive->getAvatarHash(); +} + +bool Core::RosterItem::isAvatarAutoGenerated() const +{ + return archive->getAutoAvatar(); +} + +QString Core::RosterItem::avatarPath() const +{ + QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + path += "/" + account + "/" + jid + "/avatar." + archive->getAvatarType(); + return path; +} + +bool Core::RosterItem::hasAvatar() const +{ + return archive->getHasAvatar(); +} + +void Core::RosterItem::setAvatar(const QByteArray& data) +{ + if (archive->setAvatar(data, false)) { + if (archive->getHasAvatar()) { + emit avatarChanged(Shared::Avatar::empty, ""); + } else { + emit avatarChanged(Shared::Avatar::valid, avatarPath()); + } + } +} + +void Core::RosterItem::setAutoGeneratedAvatar() +{ + QImage image(96, 96, QImage::Format_ARGB32_Premultiplied); + QPainter painter(&image); + quint8 colorIndex = rand() % Shared::colorPalette.size(); + const QColor& bg = Shared::colorPalette[colorIndex]; + painter.fillRect(image.rect(), bg); + QFont f; + f.setBold(true); + f.setPixelSize(72); + painter.setFont(f); + if (bg.lightnessF() > 0.5) { + painter.setPen(Qt::black); + } else { + painter.setPen(Qt::white); + } + painter.drawText(image.rect(), Qt::AlignCenter | Qt::AlignVCenter, jid.at(0).toUpper()); + QByteArray arr; + QBuffer stream(&arr); + stream.open(QBuffer::WriteOnly); + image.save(&stream, "PNG"); + stream.close(); + if (archive->setAvatar(arr, true)) { + emit avatarChanged(Shared::Avatar::autocreated, avatarPath()); + } +} diff --git a/core/rosteritem.h b/core/rosteritem.h index e4284af..c0a490e 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -21,6 +21,10 @@ #include #include +#include +#include +#include +#include #include @@ -58,15 +62,23 @@ public: void flushMessagesToArchive(bool finished, const QString& firstId, const QString& lastId); void requestHistory(int count, const QString& before); void requestFromEmpty(int count, const QString& before); + bool hasAvatar() const; + bool isAvatarAutoGenerated() const; + QString avatarHash() const; + QString avatarPath() const; + void setAvatar(const QByteArray& data); + void setAutoGeneratedAvatar(); signals: void nameChanged(const QString& name); void subscriptionStateChanged(Shared::SubscriptionState state); void historyResponse(const std::list& messages); void needHistory(const QString& before, const QString& after, const QDateTime& afterTime = QDateTime()); + void avatarChanged(Shared::Avatar, const QString& path); public: const QString jid; + const QString account; protected: QString name; diff --git a/core/squawk.cpp b/core/squawk.cpp index e624a8b..9f421c9 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -28,9 +28,9 @@ Core::Squawk::Squawk(QObject* parent): amap(), network() { - connect(&network, SIGNAL(fileLocalPathResponse(const QString&, const QString&)), this, SIGNAL(fileLocalPathResponse(const QString&, const QString&))); - connect(&network, SIGNAL(downloadFileProgress(const QString&, qreal)), this, SIGNAL(downloadFileProgress(const QString&, qreal))); - connect(&network, SIGNAL(downloadFileError(const QString&, const QString&)), this, SIGNAL(downloadFileError(const QString&, const QString&))); + connect(&network, &NetworkAccess::fileLocalPathResponse, this, &Squawk::fileLocalPathResponse); + connect(&network, &NetworkAccess::downloadFileProgress, this, &Squawk::downloadFileProgress); + connect(&network, &NetworkAccess::downloadFileError, this, &Squawk::downloadFileError); } Core::Squawk::~Squawk() @@ -110,37 +110,30 @@ void Core::Squawk::addAccount(const QString& login, const QString& server, const accounts.push_back(acc); amap.insert(std::make_pair(name, acc)); - connect(acc, SIGNAL(connectionStateChanged(int)), this, SLOT(onAccountConnectionStateChanged(int))); - connect(acc, SIGNAL(error(const QString&)), this, SLOT(onAccountError(const QString&))); - connect(acc, SIGNAL(availabilityChanged(int)), this, SLOT(onAccountAvailabilityChanged(int))); - connect(acc, SIGNAL(addContact(const QString&, const QString&, const QMap&)), - this, SLOT(onAccountAddContact(const QString&, const QString&, const QMap&))); - connect(acc, SIGNAL(addGroup(const QString&)), this, SLOT(onAccountAddGroup(const QString&))); - connect(acc, SIGNAL(removeGroup(const QString&)), this, SLOT(onAccountRemoveGroup(const QString&))); - connect(acc, SIGNAL(removeContact(const QString&)), this, SLOT(onAccountRemoveContact(const QString&))); - connect(acc, SIGNAL(removeContact(const QString&, const QString&)), this, SLOT(onAccountRemoveContact(const QString&, const QString&))); - connect(acc, SIGNAL(changeContact(const QString&, const QMap&)), - this, SLOT(onAccountChangeContact(const QString&, const QMap&))); - connect(acc, SIGNAL(addPresence(const QString&, const QString&, const QMap&)), - this, SLOT(onAccountAddPresence(const QString&, const QString&, const QMap&))); - connect(acc, SIGNAL(removePresence(const QString&, const QString&)), this, SLOT(onAccountRemovePresence(const QString&, const QString&))); - connect(acc, SIGNAL(message(const Shared::Message&)), this, SLOT(onAccountMessage(const Shared::Message&))); - connect(acc, SIGNAL(responseArchive(const QString&, const std::list&)), - this, SLOT(onAccountResponseArchive(const QString&, const std::list&))); + connect(acc, &Account::connectionStateChanged, this, &Squawk::onAccountConnectionStateChanged); + connect(acc, &Account::changed, this, &Squawk::onAccountChanged); + connect(acc, &Account::error, this, &Squawk::onAccountError); + connect(acc, &Account::availabilityChanged, this, &Squawk::onAccountAvailabilityChanged); + connect(acc, &Account::addContact, this, &Squawk::onAccountAddContact); + connect(acc, &Account::addGroup, this, &Squawk::onAccountAddGroup); + connect(acc, &Account::removeGroup, this, &Squawk::onAccountRemoveGroup); + connect(acc, qOverload(&Account::removeContact), this, qOverload(&Squawk::onAccountRemoveContact)); + connect(acc, qOverload(&Account::removeContact), this, qOverload(&Squawk::onAccountRemoveContact)); + connect(acc, &Account::changeContact, this, &Squawk::onAccountChangeContact); + connect(acc, &Account::addPresence, this, &Squawk::onAccountAddPresence); + connect(acc, &Account::removePresence, this, &Squawk::onAccountRemovePresence); + connect(acc, &Account::message, this, &Squawk::onAccountMessage); + connect(acc, &Account::responseArchive, this, &Squawk::onAccountResponseArchive); + + connect(acc, &Account::addRoom, this, &Squawk::onAccountAddRoom); + connect(acc, &Account::changeRoom, this, &Squawk::onAccountChangeRoom); + connect(acc, &Account::removeRoom, this, &Squawk::onAccountRemoveRoom); - connect(acc, SIGNAL(addRoom(const QString&, const QMap&)), - this, SLOT(onAccountAddRoom(const QString&, const QMap&))); - connect(acc, SIGNAL(changeRoom(const QString&, const QMap&)), - this, SLOT(onAccountChangeRoom(const QString&, const QMap&))); - connect(acc, SIGNAL(removeRoom(const QString&)), this, SLOT(onAccountRemoveRoom(const QString&))); - - connect(acc, SIGNAL(addRoomParticipant(const QString&, const QString&, const QMap&)), - this, SLOT(onAccountAddRoomPresence(const QString&, const QString&, const QMap&))); - connect(acc, SIGNAL(changeRoomParticipant(const QString&, const QString&, const QMap&)), - this, SLOT(onAccountChangeRoomPresence(const QString&, const QString&, const QMap&))); - connect(acc, SIGNAL(removeRoomParticipant(const QString&, const QString&)), - this, SLOT(onAccountRemoveRoomPresence(const QString&, const QString&))); + connect(acc, &Account::addRoomParticipant, this, &Squawk::onAccountAddRoomPresence); + connect(acc, &Account::changeRoomParticipant, this, &Squawk::onAccountChangeRoomPresence); + connect(acc, &Account::removeRoomParticipant, this, &Squawk::onAccountRemoveRoomPresence); + connect(acc, &Account::receivedVCard, this, &Squawk::responseVCard); QMap map = { {"login", login}, @@ -150,8 +143,10 @@ void Core::Squawk::addAccount(const QString& login, const QString& server, const {"resource", resource}, {"state", Shared::disconnected}, {"offline", Shared::offline}, - {"error", ""} + {"error", ""}, + {"avatarPath", acc->getAvatarPath()} }; + emit newAccount(map); } @@ -263,6 +258,12 @@ void Core::Squawk::onAccountAvailabilityChanged(int state) emit changeAccount(acc->getName(), {{"availability", state}}); } +void Core::Squawk::onAccountChanged(const QMap& data) +{ + Account* acc = static_cast(sender()); + emit changeAccount(acc->getName(), data); +} + void Core::Squawk::onAccountMessage(const Shared::Message& data) { Account* acc = static_cast(sender()); @@ -507,7 +508,7 @@ void Core::Squawk::addContactToGroupRequest(const QString& account, const QStrin { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { - qDebug() << "An attempt to add contact" << jid << "of existing account" << account << "to the group" << groupName << ", skipping"; + qDebug() << "An attempt to add contact" << jid << "of non existing account" << account << "to the group" << groupName << ", skipping"; return; } itr->second->addContactToGroupRequest(jid, groupName); @@ -517,7 +518,7 @@ void Core::Squawk::removeContactFromGroupRequest(const QString& account, const Q { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { - qDebug() << "An attempt to add contact" << jid << "of existing account" << account << "to the group" << groupName << ", skipping"; + qDebug() << "An attempt to add contact" << jid << "of non existing account" << account << "to the group" << groupName << ", skipping"; return; } itr->second->removeContactFromGroupRequest(jid, groupName); @@ -527,8 +528,28 @@ void Core::Squawk::renameContactRequest(const QString& account, const QString& j { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { - qDebug() << "An attempt to rename contact" << jid << "of existing account" << account << ", skipping"; + qDebug() << "An attempt to rename contact" << jid << "of non existing account" << account << ", skipping"; return; } itr->second->renameContactRequest(jid, newName); } + +void Core::Squawk::requestVCard(const QString& account, const QString& jid) +{ + AccountsMap::const_iterator itr = amap.find(account); + if (itr == amap.end()) { + qDebug() << "An attempt to request" << jid << "vcard of non existing account" << account << ", skipping"; + return; + } + itr->second->requestVCard(jid); +} + +void Core::Squawk::uploadVCard(const QString& account, const Shared::VCard& card) +{ + AccountsMap::const_iterator itr = amap.find(account); + if (itr == amap.end()) { + qDebug() << "An attempt to upload vcard to non existing account" << account << ", skipping"; + return; + } + itr->second->uploadVCard(card); +} diff --git a/core/squawk.h b/core/squawk.h index d6b5d2a..88ea860 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -23,7 +23,8 @@ #include #include #include -#include +#include + #include #include "account.h" @@ -65,6 +66,7 @@ signals: void fileLocalPathResponse(const QString& messageId, const QString& path); void downloadFileError(const QString& messageId, const QString& error); void downloadFileProgress(const QString& messageId, qreal value); + void responseVCard(const QString& jid, const Shared::VCard& card); public slots: void start(); @@ -90,6 +92,8 @@ public slots: void removeRoomRequest(const QString& account, const QString& jid); void fileLocalPathRequest(const QString& messageId, const QString& url); void downloadFileRequest(const QString& messageId, const QString& url); + void requestVCard(const QString& account, const QString& jid); + void uploadVCard(const QString& account, const Shared::VCard& card); private: typedef std::deque Accounts; @@ -106,6 +110,7 @@ private: private slots: void onAccountConnectionStateChanged(int state); void onAccountAvailabilityChanged(int state); + void onAccountChanged(const QMap& data); void onAccountAddGroup(const QString& name); void onAccountError(const QString& text); void onAccountRemoveGroup(const QString& name); diff --git a/global.cpp b/global.cpp index 8ab74d8..d054150 100644 --- a/global.cpp +++ b/global.cpp @@ -296,6 +296,281 @@ bool Shared::Message::storable() const return id.size() > 0 && (body.size() > 0 || oob.size()) > 0; } +Shared::VCard::Contact::Contact(Shared::VCard::Contact::Role p_role, bool p_prefered): + role(p_role), + prefered(p_prefered) +{} + +Shared::VCard::Email::Email(const QString& addr, Shared::VCard::Contact::Role p_role, bool p_prefered): + Contact(p_role, p_prefered), + address(addr) +{} + +Shared::VCard::Phone::Phone(const QString& nmbr, Shared::VCard::Phone::Type p_type, Shared::VCard::Contact::Role p_role, bool p_prefered): + Contact(p_role, p_prefered), + number(nmbr), + type(p_type) +{} + +Shared::VCard::Address::Address(const QString& zCode, const QString& cntry, const QString& rgn, const QString& lclty, const QString& strt, const QString& ext, Shared::VCard::Contact::Role p_role, bool p_prefered): + Contact(p_role, p_prefered), + zipCode(zCode), + country(cntry), + region(rgn), + locality(lclty), + street(strt), + external(ext) +{} + +Shared::VCard::VCard(): + fullName(), + firstName(), + middleName(), + lastName(), + nickName(), + description(), + url(), + organizationName(), + organizationUnit(), + organizationRole(), + jobTitle(), + birthday(), + photoType(Avatar::empty), + photoPath(), + receivingTime(QDateTime::currentDateTime()), + emails(), + phones(), + addresses() +{} + +Shared::VCard::VCard(const QDateTime& creationTime): + fullName(), + firstName(), + middleName(), + lastName(), + nickName(), + description(), + url(), + organizationName(), + organizationUnit(), + organizationRole(), + jobTitle(), + birthday(), + photoType(Avatar::empty), + photoPath(), + receivingTime(creationTime), + emails(), + phones(), + addresses() +{ +} + +QString Shared::VCard::getAvatarPath() const +{ + return photoPath; +} + +Shared::Avatar Shared::VCard::getAvatarType() const +{ + return photoType; +} + +QDate Shared::VCard::getBirthday() const +{ + return birthday; +} + +QString Shared::VCard::getDescription() const +{ + return description; +} + +QString Shared::VCard::getFirstName() const +{ + return firstName; +} + +QString Shared::VCard::getLastName() const +{ + return lastName; +} + +QString Shared::VCard::getMiddleName() const +{ + return middleName; +} + +QString Shared::VCard::getNickName() const +{ + return nickName; +} + +void Shared::VCard::setAvatarPath(const QString& path) +{ + if (path != photoPath) { + photoPath = path; + } +} + +void Shared::VCard::setAvatarType(Shared::Avatar type) +{ + if (photoType != type) { + photoType = type; + } +} + +void Shared::VCard::setBirthday(const QDate& date) +{ + if (date.isValid() && birthday != date) { + birthday = date; + } +} + +void Shared::VCard::setDescription(const QString& descr) +{ + if (description != descr) { + description = descr; + } +} + +void Shared::VCard::setFirstName(const QString& first) +{ + if (firstName != first) { + firstName = first; + } +} + +void Shared::VCard::setLastName(const QString& last) +{ + if (lastName != last) { + lastName = last; + } +} + +void Shared::VCard::setMiddleName(const QString& middle) +{ + if (middleName != middle) { + middleName = middle; + } +} + +void Shared::VCard::setNickName(const QString& nick) +{ + if (nickName != nick) { + nickName = nick; + } +} + +QString Shared::VCard::getFullName() const +{ + return fullName; +} + +QString Shared::VCard::getUrl() const +{ + return url; +} + +void Shared::VCard::setFullName(const QString& name) +{ + if (fullName != name) { + fullName = name; + } +} + +void Shared::VCard::setUrl(const QString& u) +{ + if (url != u) { + url = u; + } +} + +QString Shared::VCard::getOrgName() const +{ + return organizationName; +} + +QString Shared::VCard::getOrgRole() const +{ + return organizationRole; +} + +QString Shared::VCard::getOrgTitle() const +{ + return jobTitle; +} + +QString Shared::VCard::getOrgUnit() const +{ + return organizationUnit; +} + +void Shared::VCard::setOrgName(const QString& name) +{ + if (organizationName != name) { + organizationName = name; + } +} + +void Shared::VCard::setOrgRole(const QString& role) +{ + if (organizationRole != role) { + organizationRole = role; + } +} + +void Shared::VCard::setOrgTitle(const QString& title) +{ + if (jobTitle != title) { + jobTitle = title; + } +} + +void Shared::VCard::setOrgUnit(const QString& unit) +{ + if (organizationUnit != unit) { + organizationUnit = unit; + } +} + +QDateTime Shared::VCard::getReceivingTime() const +{ + return receivingTime; +} + +std::deque & Shared::VCard::getEmails() +{ + return emails; +} + +std::deque & Shared::VCard::getAddresses() +{ + return addresses; +} + +std::deque & Shared::VCard::getPhones() +{ + return phones; +} + +const std::deque & Shared::VCard::getEmails() const +{ + return emails; +} + +const std::deque & Shared::VCard::getAddresses() const +{ + return addresses; +} + +const std::deque & Shared::VCard::getPhones() const +{ + return phones; +} + +const std::dequeShared::VCard::Contact::roleNames = {"Not specified", "Personal", "Business"}; +const std::dequeShared::VCard::Phone::typeNames = {"Fax", "Pager", "Voice", "Cell", "Video", "Modem", "Other"}; + QIcon Shared::availabilityIcon(Shared::Availability av, bool big) { const std::deque& fallback = QApplication::palette().window().color().lightnessF() > 0.5 ? diff --git a/global.h b/global.h index 77f89bf..d5d206e 100644 --- a/global.h +++ b/global.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace Shared { @@ -69,6 +70,12 @@ enum class Role { moderator }; +enum class Avatar { + empty, + autocreated, + valid +}; + static const Availability availabilityHighest = offline; static const Availability availabilityLowest = online; @@ -102,6 +109,45 @@ static const std::deque affiliationNames = {"Unspecified", "Outcast", " static const std::deque roleNames = {"Unspecified", "Nobody", "Visitor", "Participant", "Moderator"}; QString generateUUID(); +static const std::vector colorPalette = { + QColor(244, 27, 63), + QColor(21, 104, 156), + QColor(38, 156, 98), + QColor(247, 103, 101), + QColor(121, 37, 117), + QColor(242, 202, 33), + QColor(168, 22, 63), + QColor(35, 100, 52), + QColor(52, 161, 152), + QColor(239, 53, 111), + QColor(237, 234, 36), + QColor(153, 148, 194), + QColor(211, 102, 151), + QColor(194, 63, 118), + QColor(249, 149, 51), + QColor(244, 206, 109), + QColor(121, 105, 153), + QColor(244, 199, 30), + QColor(28, 112, 28), + QColor(172, 18, 20), + QColor(25, 66, 110), + QColor(25, 149, 104), + QColor(214, 148, 0), + QColor(203, 47, 57), + QColor(4, 54, 84), + QColor(116, 161, 97), + QColor(50, 68, 52), + QColor(237, 179, 20), + QColor(69, 114, 147), + QColor(242, 212, 31), + QColor(248, 19, 20), + QColor(84, 102, 84), + QColor(25, 53, 122), + QColor(91, 91, 109), + QColor(17, 17, 80), + QColor(54, 54, 94) +}; + class Message { public: enum Type { @@ -169,6 +215,125 @@ private: QString oob; }; +class VCard { + class Contact { + public: + enum Role { + none, + home, + work + }; + static const std::deque roleNames; + + Contact(Role p_role = none, bool p_prefered = false); + + Role role; + bool prefered; + }; +public: + class Email : public Contact { + public: + Email(const QString& address, Role p_role = none, bool p_prefered = false); + + QString address; + }; + class Phone : public Contact { + public: + enum Type { + fax, + pager, + voice, + cell, + video, + modem, + other + }; + static const std::deque typeNames; + Phone(const QString& number, Type p_type = voice, Role p_role = none, bool p_prefered = false); + + QString number; + Type type; + }; + class Address : public Contact { + public: + Address( + const QString& zCode = "", + const QString& cntry = "", + const QString& rgn = "", + const QString& lclty = "", + const QString& strt = "", + const QString& ext = "", + Role p_role = none, + bool p_prefered = false + ); + + QString zipCode; + QString country; + QString region; + QString locality; + QString street; + QString external; + }; + VCard(); + VCard(const QDateTime& creationTime); + + QString getFullName() const; + void setFullName(const QString& name); + QString getFirstName() const; + void setFirstName(const QString& first); + QString getMiddleName() const; + void setMiddleName(const QString& middle); + QString getLastName() const; + void setLastName(const QString& last); + QString getNickName() const; + void setNickName(const QString& nick); + QString getDescription() const; + void setDescription(const QString& descr); + QString getUrl() const; + void setUrl(const QString& u); + QDate getBirthday() const; + void setBirthday(const QDate& date); + Avatar getAvatarType() const; + void setAvatarType(Avatar type); + QString getAvatarPath() const; + void setAvatarPath(const QString& path); + QString getOrgName() const; + void setOrgName(const QString& name); + QString getOrgUnit() const; + void setOrgUnit(const QString& unit); + QString getOrgRole() const; + void setOrgRole(const QString& role); + QString getOrgTitle() const; + void setOrgTitle(const QString& title); + QDateTime getReceivingTime() const; + std::deque& getEmails(); + const std::deque& getEmails() const; + std::deque& getPhones(); + const std::deque& getPhones() const; + std::deque
& getAddresses(); + const std::deque
& getAddresses() const; + +private: + QString fullName; + QString firstName; + QString middleName; + QString lastName; + QString nickName; + QString description; + QString url; + QString organizationName; + QString organizationUnit; + QString organizationRole; + QString jobTitle; + QDate birthday; + Avatar photoType; + QString photoPath; + QDateTime receivingTime; + std::deque emails; + std::deque phones; + std::deque
addresses; +}; + static const std::deque fallbackAvailabilityThemeIconsLightBig = { ":images/fallback/light/big/online.svg", ":images/fallback/light/big/away.svg", @@ -292,7 +457,9 @@ static const std::map> icons = { {"state-ok", {"state-ok", "state-ok"}}, {"state-error", {"state-error", "state-error"}}, + {"edit-copy", {"edit-copy", "copy"}}, {"edit-delete", {"edit-delete", "edit-delete"}}, + {"edit-rename", {"edit-rename", "edit-rename"}}, {"mail-message", {"mail-message", "mail-message"}}, {"mail-attachment", {"mail-attachment", "mail-attachment"}}, {"network-connect", {"network-connect", "network-connect"}}, @@ -302,6 +469,13 @@ static const std::map> icons = { {"view-refresh", {"view-refresh", "view-refresh"}}, {"send", {"document-send", "send"}}, {"clean", {"edit-clear-all", "clean"}}, + {"user", {"user", "user"}}, + {"user-properties", {"user-properties", "user-properties"}}, + {"group", {"group", "group"}}, + {"group-new", {"resurce-group-new", "group-new"}}, + {"favorite", {"favorite", "favorite"}}, + {"unfavorite", {"draw-star", "unfavorite"}}, + {"list-add", {"list-add", "add"}}, }; }; diff --git a/main.cpp b/main.cpp index 6fcf6c9..1c455bc 100644 --- a/main.cpp +++ b/main.cpp @@ -30,6 +30,7 @@ int main(int argc, char *argv[]) { qRegisterMetaType("Shared::Message"); + qRegisterMetaType("Shared::VCard"); qRegisterMetaType>("std::list"); qRegisterMetaType>("QSet"); @@ -48,7 +49,6 @@ int main(int argc, char *argv[]) QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); bool found = false; for (QString share : shares) { - qDebug() << share; found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n"); if (found) { break; @@ -79,76 +79,61 @@ int main(int argc, char *argv[]) QThread* coreThread = new QThread(); squawk->moveToThread(coreThread); - QObject::connect(coreThread, SIGNAL(started()), squawk, SLOT(start())); - QObject::connect(&app, SIGNAL(aboutToQuit()), squawk, SLOT(stop())); - QObject::connect(squawk, SIGNAL(quit()), coreThread, SLOT(quit())); - QObject::connect(coreThread, SIGNAL(finished()), squawk, SLOT(deleteLater())); + QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start); + QObject::connect(&app, &QApplication::aboutToQuit, squawk, &Core::Squawk::stop); + QObject::connect(squawk, &Core::Squawk::quit, coreThread, &QThread::quit); + QObject::connect(coreThread, &QThread::finished, squawk, &Core::Squawk::deleteLater); - QObject::connect(&w, SIGNAL(newAccountRequest(const QMap&)), squawk, SLOT(newAccountRequest(const QMap&))); - QObject::connect(&w, SIGNAL(modifyAccountRequest(const QString&, const QMap&)), - squawk, SLOT(modifyAccountRequest(const QString&, const QMap&))); - QObject::connect(&w, SIGNAL(removeAccountRequest(const QString&)), squawk, SLOT(removeAccountRequest(const QString&))); - QObject::connect(&w, SIGNAL(connectAccount(const QString&)), squawk, SLOT(connectAccount(const QString&))); - QObject::connect(&w, SIGNAL(disconnectAccount(const QString&)), squawk, SLOT(disconnectAccount(const QString&))); - QObject::connect(&w, SIGNAL(changeState(int)), squawk, SLOT(changeState(int))); - QObject::connect(&w, SIGNAL(sendMessage(const QString&, const Shared::Message&)), squawk, SLOT(sendMessage(const QString&, const Shared::Message&))); - QObject::connect(&w, SIGNAL(requestArchive(const QString&, const QString&, int, const QString&)), - squawk, SLOT(requestArchive(const QString&, const QString&, int, const QString&))); - QObject::connect(&w, SIGNAL(subscribeContact(const QString&, const QString&, const QString&)), - squawk, SLOT(subscribeContact(const QString&, const QString&, const QString&))); - QObject::connect(&w, SIGNAL(unsubscribeContact(const QString&, const QString&, const QString&)), - squawk, SLOT(unsubscribeContact(const QString&, const QString&, const QString&))); - QObject::connect(&w, SIGNAL(addContactRequest(const QString&, const QString&, const QString&, const QSet&)), - squawk, SLOT(addContactRequest(const QString&, const QString&, const QString&, const QSet&))); - QObject::connect(&w, SIGNAL(removeContactRequest(const QString&, const QString&)), - squawk, SLOT(removeContactRequest(const QString&, const QString&))); - QObject::connect(&w, SIGNAL(setRoomJoined(const QString&, const QString&, bool)), squawk, SLOT(setRoomJoined(const QString&, const QString&, bool))); - QObject::connect(&w, SIGNAL(setRoomAutoJoin(const QString&, const QString&, bool)), squawk, SLOT(setRoomAutoJoin(const QString&, const QString&, bool))); - - QObject::connect(&w, SIGNAL(removeRoomRequest(const QString&, const QString&)), - squawk, SLOT(removeRoomRequest(const QString&, const QString&))); - QObject::connect(&w, SIGNAL(addRoomRequest(const QString&, const QString&, const QString&, const QString&, bool)), - squawk, SLOT(addRoomRequest(const QString&, const QString&, const QString&, const QString&, bool))); - QObject::connect(&w, SIGNAL(fileLocalPathRequest(const QString&, const QString&)), squawk, SLOT(fileLocalPathRequest(const QString&, const QString&))); - QObject::connect(&w, SIGNAL(downloadFileRequest(const QString&, const QString&)), squawk, SLOT(downloadFileRequest(const QString&, const QString&))); + 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::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::fileLocalPathRequest, squawk, &Core::Squawk::fileLocalPathRequest); + QObject::connect(&w, &Squawk::downloadFileRequest, squawk, &Core::Squawk::downloadFileRequest); 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(squawk, SIGNAL(newAccount(const QMap&)), &w, SLOT(newAccount(const QMap&))); - QObject::connect(squawk, SIGNAL(addContact(const QString&, const QString&, const QString&, const QMap&)), - &w, SLOT(addContact(const QString&, const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(changeAccount(const QString&, const QMap&)), - &w, SLOT(changeAccount(const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(removeAccount(const QString&)), &w, SLOT(removeAccount(const QString&))); - QObject::connect(squawk, SIGNAL(addGroup(const QString&, const QString&)), &w, SLOT(addGroup(const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(removeGroup(const QString&, const QString&)), &w, SLOT(removeGroup(const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(removeContact(const QString&, const QString&)), &w, SLOT(removeContact(const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(removeContact(const QString&, const QString&, const QString&)), &w, SLOT(removeContact(const QString&, const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(changeContact(const QString&, const QString&, const QMap&)), - &w, SLOT(changeContact(const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(addPresence(const QString&, const QString&, const QString&, const QMap&)), - &w, SLOT(addPresence(const QString&, const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(removePresence(const QString&, const QString&, const QString&)), &w, SLOT(removePresence(const QString&, const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(stateChanged(int)), &w, SLOT(stateChanged(int))); - QObject::connect(squawk, SIGNAL(accountMessage(const QString&, const Shared::Message&)), &w, SLOT(accountMessage(const QString&, const Shared::Message&))); - QObject::connect(squawk, SIGNAL(responseArchive(const QString&, const QString&, const std::list&)), - &w, SLOT(responseArchive(const QString&, const QString&, const std::list&))); - - QObject::connect(squawk, SIGNAL(addRoom(const QString&, const QString&, const QMap&)), - &w, SLOT(addRoom(const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(changeRoom(const QString&, const QString&, const QMap&)), - &w, SLOT(changeRoom(const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(removeRoom(const QString&, const QString&)), &w, SLOT(removeRoom(const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(addRoomParticipant(const QString&, const QString&, const QString&, const QMap&)), - &w, SLOT(addRoomParticipant(const QString&, const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(changeRoomParticipant(const QString&, const QString&, const QString&, const QMap&)), - &w, SLOT(changeRoomParticipant(const QString&, const QString&, const QString&, const QMap&))); - QObject::connect(squawk, SIGNAL(removeRoomParticipant(const QString&, const QString&, const QString&)), - &w, SLOT(removeRoomParticipant(const QString&, const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(fileLocalPathResponse(const QString&, const QString&)), &w, SLOT(fileLocalPathResponse(const QString&, const QString&))); - QObject::connect(squawk, SIGNAL(downloadFileProgress(const QString&, qreal)), &w, SLOT(downloadFileProgress(const QString&, qreal))); - QObject::connect(squawk, SIGNAL(downloadFileError(const QString&, const QString&)), &w, SLOT(downloadFileError(const QString&, const QString&))); + 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::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::fileLocalPathResponse, &w, &Squawk::fileLocalPathResponse); + QObject::connect(squawk, &Core::Squawk::downloadFileProgress, &w, &Squawk::downloadFileProgress); + QObject::connect(squawk, &Core::Squawk::downloadFileError, &w, &Squawk::downloadFileError); + QObject::connect(squawk, &Core::Squawk::responseVCard, &w, &Squawk::responseVCard); //qDebug() << QStandardPaths::writableLocation(QStandardPaths::CacheLocation); diff --git a/resources/images/fallback/dark/big/add.svg b/resources/images/fallback/dark/big/add.svg new file mode 100644 index 0000000..f9bae3c --- /dev/null +++ b/resources/images/fallback/dark/big/add.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/copy.svg b/resources/images/fallback/dark/big/copy.svg new file mode 100644 index 0000000..7470e5f --- /dev/null +++ b/resources/images/fallback/dark/big/copy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/edit-rename.svg b/resources/images/fallback/dark/big/edit-rename.svg new file mode 100644 index 0000000..8075a3b --- /dev/null +++ b/resources/images/fallback/dark/big/edit-rename.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/favorite.svg b/resources/images/fallback/dark/big/favorite.svg new file mode 100644 index 0000000..5751db6 --- /dev/null +++ b/resources/images/fallback/dark/big/favorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/group-new.svg b/resources/images/fallback/dark/big/group-new.svg new file mode 100644 index 0000000..a28270a --- /dev/null +++ b/resources/images/fallback/dark/big/group-new.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/group.svg b/resources/images/fallback/dark/big/group.svg new file mode 100644 index 0000000..0b9c379 --- /dev/null +++ b/resources/images/fallback/dark/big/group.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/unfavorite.svg b/resources/images/fallback/dark/big/unfavorite.svg new file mode 100644 index 0000000..3d128ea --- /dev/null +++ b/resources/images/fallback/dark/big/unfavorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/big/user-properties.svg b/resources/images/fallback/dark/big/user-properties.svg new file mode 100644 index 0000000..afec990 --- /dev/null +++ b/resources/images/fallback/dark/big/user-properties.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/add.svg b/resources/images/fallback/dark/small/add.svg new file mode 100644 index 0000000..8779062 --- /dev/null +++ b/resources/images/fallback/dark/small/add.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/copy.svg b/resources/images/fallback/dark/small/copy.svg new file mode 100644 index 0000000..061d734 --- /dev/null +++ b/resources/images/fallback/dark/small/copy.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/edit-rename.svg b/resources/images/fallback/dark/small/edit-rename.svg new file mode 100644 index 0000000..18ccc58 --- /dev/null +++ b/resources/images/fallback/dark/small/edit-rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/favorite.svg b/resources/images/fallback/dark/small/favorite.svg new file mode 100644 index 0000000..5751db6 --- /dev/null +++ b/resources/images/fallback/dark/small/favorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/group-new.svg b/resources/images/fallback/dark/small/group-new.svg new file mode 100644 index 0000000..848af85 --- /dev/null +++ b/resources/images/fallback/dark/small/group-new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/group.svg b/resources/images/fallback/dark/small/group.svg new file mode 100644 index 0000000..7ca4c26 --- /dev/null +++ b/resources/images/fallback/dark/small/group.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/unfavorite.svg b/resources/images/fallback/dark/small/unfavorite.svg new file mode 100644 index 0000000..3d128ea --- /dev/null +++ b/resources/images/fallback/dark/small/unfavorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/dark/small/user-properties.svg b/resources/images/fallback/dark/small/user-properties.svg new file mode 100644 index 0000000..fa4c9e0 --- /dev/null +++ b/resources/images/fallback/dark/small/user-properties.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/big/add.svg b/resources/images/fallback/light/big/add.svg new file mode 100644 index 0000000..0d6166a --- /dev/null +++ b/resources/images/fallback/light/big/add.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/copy.svg b/resources/images/fallback/light/big/copy.svg new file mode 100644 index 0000000..427de29 --- /dev/null +++ b/resources/images/fallback/light/big/copy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/edit-rename.svg b/resources/images/fallback/light/big/edit-rename.svg new file mode 100644 index 0000000..0c3d22c --- /dev/null +++ b/resources/images/fallback/light/big/edit-rename.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/favorite.svg b/resources/images/fallback/light/big/favorite.svg new file mode 100644 index 0000000..f6025c6 --- /dev/null +++ b/resources/images/fallback/light/big/favorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/group-new.svg b/resources/images/fallback/light/big/group-new.svg new file mode 100644 index 0000000..9c8b823 --- /dev/null +++ b/resources/images/fallback/light/big/group-new.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/group.svg b/resources/images/fallback/light/big/group.svg new file mode 100644 index 0000000..ef4758b --- /dev/null +++ b/resources/images/fallback/light/big/group.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/unfavorite.svg b/resources/images/fallback/light/big/unfavorite.svg new file mode 100644 index 0000000..5eef7a3 --- /dev/null +++ b/resources/images/fallback/light/big/unfavorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/big/user-properties.svg b/resources/images/fallback/light/big/user-properties.svg new file mode 100644 index 0000000..9a14b93 --- /dev/null +++ b/resources/images/fallback/light/big/user-properties.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/small/add.svg b/resources/images/fallback/light/small/add.svg new file mode 100644 index 0000000..f05db06 --- /dev/null +++ b/resources/images/fallback/light/small/add.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/small/copy.svg b/resources/images/fallback/light/small/copy.svg new file mode 100644 index 0000000..c557cef --- /dev/null +++ b/resources/images/fallback/light/small/copy.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/small/edit-rename.svg b/resources/images/fallback/light/small/edit-rename.svg new file mode 100644 index 0000000..6a84496 --- /dev/null +++ b/resources/images/fallback/light/small/edit-rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/small/favorite.svg b/resources/images/fallback/light/small/favorite.svg new file mode 100644 index 0000000..f6025c6 --- /dev/null +++ b/resources/images/fallback/light/small/favorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/small/group-new.svg b/resources/images/fallback/light/small/group-new.svg new file mode 100644 index 0000000..43b465a --- /dev/null +++ b/resources/images/fallback/light/small/group-new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/small/group.svg b/resources/images/fallback/light/small/group.svg new file mode 100644 index 0000000..dfefc94 --- /dev/null +++ b/resources/images/fallback/light/small/group.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/small/unfavorite.svg b/resources/images/fallback/light/small/unfavorite.svg new file mode 100644 index 0000000..5eef7a3 --- /dev/null +++ b/resources/images/fallback/light/small/unfavorite.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/images/fallback/light/small/user-properties.svg b/resources/images/fallback/light/small/user-properties.svg new file mode 100644 index 0000000..2a0bebd --- /dev/null +++ b/resources/images/fallback/light/small/user-properties.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/resources.qrc b/resources/resources.qrc index 244db04..4fb3e5b 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -32,6 +32,14 @@ images/fallback/dark/big/clean.svg images/fallback/dark/big/send.svg images/fallback/dark/big/mail-attachment.svg + images/fallback/dark/big/group.svg + images/fallback/dark/big/group-new.svg + images/fallback/dark/big/edit-rename.svg + images/fallback/dark/big/user-properties.svg + images/fallback/dark/big/copy.svg + images/fallback/dark/big/favorite.svg + images/fallback/dark/big/unfavorite.svg + images/fallback/dark/big/add.svg images/fallback/dark/small/absent.svg @@ -64,6 +72,14 @@ images/fallback/dark/small/clean.svg images/fallback/dark/small/send.svg images/fallback/dark/small/mail-attachment.svg + images/fallback/dark/small/group.svg + images/fallback/dark/small/group-new.svg + images/fallback/dark/small/edit-rename.svg + images/fallback/dark/small/user-properties.svg + images/fallback/dark/small/copy.svg + images/fallback/dark/small/favorite.svg + images/fallback/dark/small/unfavorite.svg + images/fallback/dark/small/add.svg images/fallback/light/big/absent.svg @@ -96,6 +112,14 @@ images/fallback/light/big/clean.svg images/fallback/light/big/send.svg images/fallback/light/big/mail-attachment.svg + images/fallback/light/big/group.svg + images/fallback/light/big/group-new.svg + images/fallback/light/big/edit-rename.svg + images/fallback/light/big/user-properties.svg + images/fallback/light/big/copy.svg + images/fallback/light/big/favorite.svg + images/fallback/light/big/unfavorite.svg + images/fallback/light/big/add.svg images/fallback/light/small/absent.svg @@ -128,5 +152,13 @@ images/fallback/light/small/clean.svg images/fallback/light/small/send.svg images/fallback/light/small/mail-attachment.svg + images/fallback/light/small/group.svg + images/fallback/light/small/group-new.svg + images/fallback/light/small/edit-rename.svg + images/fallback/light/small/user-properties.svg + images/fallback/light/small/copy.svg + images/fallback/light/small/favorite.svg + images/fallback/light/small/unfavorite.svg + images/fallback/light/small/add.svg diff --git a/signalcatcher.cpp b/signalcatcher.cpp index 3460d0e..9ac3aae 100644 --- a/signalcatcher.cpp +++ b/signalcatcher.cpp @@ -38,7 +38,7 @@ SignalCatcher::SignalCatcher(QCoreApplication *p_app, QObject *parent): } snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this); - connect(snInt, SIGNAL(activated(int)), this, SLOT(handleSigInt())); + connect(snInt, &QSocketNotifier::activated, this, &SignalCatcher::handleSigInt); } SignalCatcher::~SignalCatcher() diff --git a/translations/squawk.ru.ts b/translations/squawk.ru.ts index bb1fdef..e7bae69 100644 --- a/translations/squawk.ru.ts +++ b/translations/squawk.ru.ts @@ -5,92 +5,77 @@ Account - Account Заголовок окна Учетная запись - Your account login Имя пользователя Вашей учетной записи - john_smith1987 ivan_ivanov1987 - Server Сервер - A server address of your account. Like 404.city or macaw.me Адресс сервера вашей учетной записи (выглядит как 404.city или macaw.me) - macaw.me macaw.me - Login Имя учетной записи - Password Пароль - Password of your account Пароль вашей учетной записи - Name Имя - Just a name how would you call this account, doesn't affect anything Просто имя, то как Вы называете свою учетную запись, может быть любым - John Иван - Resource Ресурс - A resource name like "Home" or "Work" Имя этой программы для ваших контактов, может быть "Home" или "Phone" - QXmpp Ресурс по умолчанию QXmpp @@ -100,44 +85,38 @@ Accounts - Accounts Учетные записи - Delete Удалить - Add Добавить - Edit Редактировать - Change password Изменить пароль - - - + + Connect Подключить - + Disconnect Отключить @@ -146,10 +125,14 @@ Conversation - Type your message here... Введите сообщение... + + + Chose a file to send + Выберите файл для отправки + Global @@ -253,67 +236,97 @@ Moderator Модератор + + Not specified + Не указан + + + Personal + Личный + + + Business + Рабочий + + + Fax + Факс + + + Pager + Пэйджер + + + Voice + Стационарный + + + Cell + Мобильный + + + Video + Видеофон + + + Modem + Модем + + + Other + Другой + JoinConference - Join new conference Заголовок окна Присоединиться к новой беседе - JID JID - Room JID Jabber-идентификатор беседы - identifier@conference.server.org identifier@conference.server.org - Account Учетная запись - Join on login Автовход - If checked Squawk will try to join this conference on login Если стоит галочка Squawk автоматически присоединится к этой беседе при подключении - 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 Ivan @@ -321,24 +334,24 @@ Message - + Download Скачать - + Error downloading file: %1 You can try again Ошибка загрузки файла: %1 Вы можете попробовать снова - + %1 is offering you to download a file %1 предлагает Вам скачать файл - + Open Открыть @@ -388,67 +401,67 @@ You can try again Models::Roster - + New messages Есть непрочитанные сообщения - - - + + + New messages: Новых сообщений: - + Jabber ID: Идентификатор: - - - + + + Availability: Доступность: - - - + + + Status: Статус: - - - + + + Subscription: Подписка: - + Affiliation: Я правда не знаю, как это объяснить, не то что перевести Причастность: - + Role: Роль: - + Online contacts: Контакстов в сети: - + Total contacts: Всего контактов: - + Members: Участников: @@ -457,57 +470,48 @@ You can try again NewContact - Add new contact Заголовок окна Добавление нового контакта - Account Учетная запись - An account that is going to have new contact Учетная запись для которой будет добавлен контакт - JID JID - Jabber id of your new contact Jabber-идентификатор нового контакта - name@server.dmn Placeholder поля ввода JID name@server.dmn - Name Имя - The way this new contact will be labeled in your roster (optional) То, как будет подписан контакт в вашем списке контактов (не обязательно) - John Smith Иван Иванов @@ -516,87 +520,91 @@ You can try again Squawk - squawk Squawk - - + Settings Настройки - - + Squawk Squawk - - + Accounts Учетные записи - - + Quit Выйти - - + Add contact Добавить контакт - - + Add conference Присоединиться к беседе - + + Contact list + Список контактов + + + Disconnect Отключить - + Connect Подключить - - - + + + VCard + Карточка + + + + + Remove Удалить - + Open dialog Открыть диалог - - + + Unsubscribe Отписаться - - + + Subscribe Подписаться - + Rename Переименовать - + Input new name for %1 or leave it empty for the contact to be displayed as %1 @@ -606,34 +614,245 @@ to be displayed as %1 %1 - + Renaming %1 Назначение имени контакту %1 - + Groups Группы - + New group Создать новую группу - + New group name Имя группы - + Add %1 to a new group Добавление %1 в новую группу - + Open conversation Открыть окно беседы + + + %1 account card + Карточка учетной записи %1 + + + + %1 contact card + Карточка контакта %1 + + + + Downloading vCard + Получение карточки + + + + VCard + + + Received 12.07.2007 at 17.35 + Не обновлялось + + + + + General + Общее + + + + Organization + Место работы + + + + Middle name + Среднее имя + + + + First name + Имя + + + + Last name + Фамилия + + + + Nick name + Псевдоним + + + + Birthday + Дата рождения + + + + Organization name + Название организации + + + + Unit / Department + Отдел + + + + Role / Profession + Профессия + + + + Job title + Наименование должности + + + + Full name + Полное имя + + + + Personal information + Личная информация + + + + Addresses + Адреса + + + + E-Mail addresses + Адреса электронной почты + + + + Phone numbers + Номера телефонов + + + + + Contact + Контактная информация + + + + Jabber ID + Jabber ID + + + + Web site + Веб сайт + + + + + Description + Описание + + + + Set avatar + Установить иконку + + + + Clear avatar + Убрать иконку + + + + Account %1 card + Карточка учетной записи %1 + + + + Contact %1 card + Карточка контакта %1 + + + + Received %1 at %2 + Получено %1 в %2 + + + + Chose your new avatar + Выберите новую иконку + + + + Images (*.png *.jpg *.jpeg) + Изображения (*.png *.jpg *.jpeg) + + + + Add email address + Добавить адрес электронной почты + + + + Unset this email as preferred + Убрать отметку "предпочтительный" с этого адреса + + + + Set this email as preferred + Отметить этот адрес как "предпочтительный" + + + + Remove selected email addresses + Удалить выбранные адреса + + + + Copy selected emails to clipboard + Скопировать выбранные адреса в буфер обмена + + + + Add phone number + Добавить номер телефона + + + + Unset this phone as preferred + Убрать отметку "предпочтительный" с этого номера + + + + Set this phone as preferred + Отметить этот номер как "предпочтительный" + + + + Remove selected phone numbers + Удалить выбранные телефонные номера + + + + Copy selected phones to clipboard + Скопировать выбранные телефонные номера в буфер обмена + diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 982bb9a..50d5304 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -10,6 +10,8 @@ set(CMAKE_AUTOUIC ON) find_package(Qt5Widgets CONFIG REQUIRED) find_package(Qt5DBus CONFIG REQUIRED) +add_subdirectory(widgets) + set(squawkUI_SRC squawk.cpp models/accounts.cpp @@ -22,24 +24,20 @@ set(squawkUI_SRC models/room.cpp models/abstractparticipant.cpp models/participant.cpp - widgets/conversation.cpp - widgets/chat.cpp - widgets/room.cpp - widgets/newcontact.cpp - widgets/accounts.cpp - widgets/account.cpp - widgets/joinconference.cpp utils/messageline.cpp utils//message.cpp utils/resizer.cpp utils/image.cpp utils/flowlayout.cpp utils/badge.cpp + utils/progress.cpp + utils/comboboxdelegate.cpp ) # Tell CMake to create the helloworld executable add_library(squawkUI ${squawkUI_SRC}) # Use the Widgets module from Qt 5. +target_link_libraries(squawkUI squawkWidgets) target_link_libraries(squawkUI Qt5::Widgets) target_link_libraries(squawkUI Qt5::DBus) diff --git a/ui/models/account.cpp b/ui/models/account.cpp index b8758c2..eeb8731 100644 --- a/ui/models/account.cpp +++ b/ui/models/account.cpp @@ -26,6 +26,7 @@ Models::Account::Account(const QMap& data, Models::Item* pare server(data.value("server").toString()), resource(data.value("resource").toString()), error(data.value("error").toString()), + avatarPath(data.value("avatarPath").toString()), state(Shared::disconnected), availability(Shared::offline) { @@ -162,6 +163,8 @@ QVariant Models::Account::data(int column) const return QCoreApplication::translate("Global", Shared::availabilityNames[availability].toLatin1()); case 7: return resource; + case 8: + return avatarPath; default: return QVariant(); } @@ -169,7 +172,7 @@ QVariant Models::Account::data(int column) const int Models::Account::columnCount() const { - return 8; + return 9; } void Models::Account::update(const QString& field, const QVariant& value) @@ -190,6 +193,8 @@ void Models::Account::update(const QString& field, const QVariant& value) setResource(value.toString()); } else if (field == "error") { setError(value.toString()); + } else if (field == "avatarPath") { + setAvatarPath(value.toString()); } } @@ -224,3 +229,24 @@ void Models::Account::toOfflineState() setAvailability(Shared::offline); Item::toOfflineState(); } + +QString Models::Account::getAvatarPath() +{ + return avatarPath; +} + +void Models::Account::setAvatarPath(const QString& path) +{ + avatarPath = path; + changed(8); //it's uncoditional because the path doesn't change when one avatar of the same type replaces another, sha1 sums checks are on the backend +} + +QString Models::Account::getBareJid() const +{ + return login + "@" + server; +} + +QString Models::Account::getFullJid() const +{ + return getBareJid() + "/" + resource; +} diff --git a/ui/models/account.h b/ui/models/account.h index fbdf204..e114699 100644 --- a/ui/models/account.h +++ b/ui/models/account.h @@ -50,6 +50,9 @@ namespace Models { void setError(const QString& p_resource); QString getError() const; + void setAvatarPath(const QString& path); + QString getAvatarPath(); + void setAvailability(Shared::Availability p_avail); void setAvailability(unsigned int p_avail); Shared::Availability getAvailability() const; @@ -61,12 +64,16 @@ namespace Models { void update(const QString& field, const QVariant& value); + QString getBareJid() const; + QString getFullJid() const; + private: QString login; QString password; QString server; QString resource; QString error; + QString avatarPath; Shared::ConnectionState state; Shared::Availability availability; diff --git a/ui/models/accounts.cpp b/ui/models/accounts.cpp index b7f16ef..79ed159 100644 --- a/ui/models/accounts.cpp +++ b/ui/models/accounts.cpp @@ -89,7 +89,7 @@ void Models::Accounts::addAccount(Account* account) } accs.insert(before, account); - connect(account, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onAccountChanged(Models::Item*, int, int))); + connect(account, &Account::childChanged, this, &Accounts::onAccountChanged); endInsertRows(); emit sizeChanged(accs.size()); @@ -143,7 +143,7 @@ void Models::Accounts::removeAccount(int index) { Account* account = accs[index]; beginRemoveRows(QModelIndex(), index, index); - disconnect(account, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onAccountChanged(Models::Item*, int, int))); + disconnect(account, &Account::childChanged, this, &Accounts::onAccountChanged); accs.erase(accs.begin() + index); endRemoveRows(); diff --git a/ui/models/contact.cpp b/ui/models/contact.cpp index 5f4ba21..eee6b4e 100644 --- a/ui/models/contact.cpp +++ b/ui/models/contact.cpp @@ -25,14 +25,26 @@ Models::Contact::Contact(const QString& p_jid ,const QMap &da jid(p_jid), availability(Shared::offline), state(Shared::none), + avatarState(Shared::Avatar::empty), presences(), messages(), - childMessages(0) + childMessages(0), + status(), + avatarPath() { QMap::const_iterator itr = data.find("state"); if (itr != data.end()) { setState(itr.value().toUInt()); } + + itr = data.find("avatarState"); + if (itr != data.end()) { + setAvatarState(itr.value().toUInt()); + } + itr = data.find("avatarPath"); + if (itr != data.end()) { + setAvatarPath(itr.value().toString()); + } } Models::Contact::~Contact() @@ -100,7 +112,7 @@ void Models::Contact::setStatus(const QString& p_state) int Models::Contact::columnCount() const { - return 6; + return 8; } QVariant Models::Contact::data(int column) const @@ -118,6 +130,10 @@ QVariant Models::Contact::data(int column) const return getMessagesCount(); case 5: return getStatus(); + case 6: + return static_cast(getAvatarState()); + case 7: + return getAvatarPath(); default: return QVariant(); } @@ -142,6 +158,10 @@ void Models::Contact::update(const QString& field, const QVariant& value) setAvailability(value.toUInt()); } else if (field == "state") { setState(value.toUInt()); + } else if (field == "avatarState") { + setAvatarState(value.toUInt()); + } else if (field == "avatarPath") { + setAvatarPath(value.toString()); } } @@ -209,7 +229,7 @@ void Models::Contact::refresh() void Models::Contact::_removeChild(int index) { Item* child = childItems[index]; - disconnect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + disconnect(child, &Item::childChanged, this, &Contact::refresh); Item::_removeChild(index); refresh(); } @@ -217,7 +237,7 @@ void Models::Contact::_removeChild(int index) void Models::Contact::appendChild(Models::Item* child) { Item::appendChild(child); - connect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + connect(child, &Item::childChanged, this, &Contact::refresh); refresh(); } @@ -304,7 +324,7 @@ void Models::Contact::toOfflineState() emit childIsAboutToBeRemoved(this, 0, childItems.size()); for (int i = 0; i < childItems.size(); ++i) { Item* item = childItems[i]; - disconnect(item, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + disconnect(item, &Item::childChanged, this, &Contact::refresh); Item::_removeChild(i); item->deleteLater(); } @@ -343,8 +363,44 @@ Models::Contact::Contact(const Models::Contact& other): Presence* pCopy = new Presence(*pres); presences.insert(pCopy->getName(), pCopy); Item::appendChild(pCopy); - connect(pCopy, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + connect(pCopy, &Item::childChanged, this, &Contact::refresh); } refresh(); } + +QString Models::Contact::getAvatarPath() const +{ + return avatarPath; +} + +Shared::Avatar Models::Contact::getAvatarState() const +{ + return avatarState; +} + +void Models::Contact::setAvatarPath(const QString& path) +{ + if (path != avatarPath) { + avatarPath = path; + changed(7); + } +} + +void Models::Contact::setAvatarState(Shared::Avatar p_state) +{ + if (avatarState != p_state) { + avatarState = p_state; + changed(6); + } +} + +void Models::Contact::setAvatarState(unsigned int p_state) +{ + if (p_state <= static_cast(Shared::Avatar::valid)) { + Shared::Avatar state = static_cast(p_state); + setAvatarState(state); + } else { + qDebug() << "An attempt to set invalid avatar state" << p_state << "to the contact" << jid << ", skipping"; + } +} diff --git a/ui/models/contact.h b/ui/models/contact.h index 6f0a5fc..fda893f 100644 --- a/ui/models/contact.h +++ b/ui/models/contact.h @@ -40,6 +40,8 @@ public: QString getJid() const; Shared::Availability getAvailability() const; Shared::SubscriptionState getState() const; + Shared::Avatar getAvatarState() const; + QString getAvatarPath() const; QIcon getStatusIcon(bool big = false) const; int columnCount() const override; @@ -75,6 +77,9 @@ protected: void setAvailability(unsigned int p_state); void setState(Shared::SubscriptionState p_state); void setState(unsigned int p_state); + void setAvatarState(Shared::Avatar p_state); + void setAvatarState(unsigned int p_state); + void setAvatarPath(const QString& path); void setJid(const QString p_jid); void setStatus(const QString& p_state); @@ -82,10 +87,12 @@ private: QString jid; Shared::Availability availability; Shared::SubscriptionState state; + Shared::Avatar avatarState; QMap presences; Messages messages; unsigned int childMessages; QString status; + QString avatarPath; }; } diff --git a/ui/models/group.cpp b/ui/models/group.cpp index d857710..5e4411e 100644 --- a/ui/models/group.cpp +++ b/ui/models/group.cpp @@ -32,7 +32,7 @@ Models::Group::~Group() void Models::Group::appendChild(Models::Item* child) { Item::appendChild(child); - connect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + connect(child, &Item::childChanged, this, &Group::refresh); changed(1); refresh(); } @@ -59,7 +59,7 @@ QVariant Models::Group::data(int column) const void Models::Group::_removeChild(int index) { Item* child = childItems[index]; - disconnect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); + disconnect(child, &Item::childChanged, this, &Group::refresh); Item::_removeChild(index); changed(1); refresh(); diff --git a/ui/models/item.cpp b/ui/models/item.cpp index 7c0cb96..f90f5e6 100644 --- a/ui/models/item.cpp +++ b/ui/models/item.cpp @@ -88,13 +88,13 @@ void Models::Item::appendChild(Models::Item* child) childItems.insert(before, child); child->parent = this; - QObject::connect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SIGNAL(childChanged(Models::Item*, int, int))); - QObject::connect(child, SIGNAL(childIsAboutToBeInserted(Item*, int, int)), this, SIGNAL(childIsAboutToBeInserted(Item*, int, int))); - QObject::connect(child, SIGNAL(childInserted()), this, SIGNAL(childInserted())); - QObject::connect(child, SIGNAL(childIsAboutToBeRemoved(Item*, int, int)), this, SIGNAL(childIsAboutToBeRemoved(Item*, int, int))); - QObject::connect(child, SIGNAL(childRemoved()), this, SIGNAL(childRemoved())); - QObject::connect(child, SIGNAL(childIsAboutToBeMoved(Item*, int, int, Item*, int)), this, SIGNAL(childIsAboutToBeMoved(Item*, int, int, Item*, int))); - QObject::connect(child, SIGNAL(childMoved()), this, SIGNAL(childMoved())); + QObject::connect(child, &Item::childChanged, this, &Item::childChanged); + QObject::connect(child, &Item::childIsAboutToBeInserted, this, &Item::childIsAboutToBeInserted); + QObject::connect(child, &Item::childInserted, this, &Item::childInserted); + QObject::connect(child, &Item::childIsAboutToBeRemoved, this, &Item::childIsAboutToBeRemoved); + QObject::connect(child, &Item::childRemoved, this, &Item::childRemoved); + QObject::connect(child, &Item::childIsAboutToBeMoved, this, &Item::childIsAboutToBeMoved); + QObject::connect(child, &Item::childMoved, this, &Item::childMoved); if (moving) { emit childMoved(); @@ -141,7 +141,7 @@ const Models::Item * Models::Item::parentItemConst() const int Models::Item::columnCount() const { - return 1; + return 2; } QString Models::Item::getName() const @@ -168,13 +168,13 @@ void Models::Item::_removeChild(int index) { Item* child = childItems[index]; - QObject::disconnect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onChildChanged(Models::Item*, int, int))); - QObject::disconnect(child, SIGNAL(childIsAboutToBeInserted(Item*, int, int)), this, SIGNAL(childIsAboutToBeInserted(Item*, int, int))); - QObject::disconnect(child, SIGNAL(childInserted()), this, SIGNAL(childInserted())); - QObject::disconnect(child, SIGNAL(childIsAboutToBeRemoved(Item*, int, int)), this, SIGNAL(childIsAboutToBeRemoved(Item*, int, int))); - QObject::disconnect(child, SIGNAL(childRemoved()), this, SIGNAL(childRemoved())); - QObject::disconnect(child, SIGNAL(childIsAboutToBeMoved(Item*, int, int, Item*, int)), this, SIGNAL(childIsAboutToBeMoved(Item*, int, int, Item*, int))); - QObject::disconnect(child, SIGNAL(childMoved()), this, SIGNAL(childMoved())); + QObject::disconnect(child, &Item::childChanged, this, &Item::childChanged); + QObject::disconnect(child, &Item::childIsAboutToBeInserted, this, &Item::childIsAboutToBeInserted); + QObject::disconnect(child, &Item::childInserted, this, &Item::childInserted); + QObject::disconnect(child, &Item::childIsAboutToBeRemoved, this, &Item::childIsAboutToBeRemoved); + QObject::disconnect(child, &Item::childRemoved, this, &Item::childRemoved); + QObject::disconnect(child, &Item::childIsAboutToBeMoved, this, &Item::childIsAboutToBeMoved); + QObject::disconnect(child, &Item::childMoved, this, &Item::childMoved); childItems.erase(childItems.begin() + index); child->parent = 0; diff --git a/ui/models/room.cpp b/ui/models/room.cpp index 6addead..204b2b5 100644 --- a/ui/models/room.cpp +++ b/ui/models/room.cpp @@ -238,7 +238,6 @@ void Models::Room::toOfflineState() emit childIsAboutToBeRemoved(this, 0, childItems.size()); for (int i = 0; i < childItems.size(); ++i) { Item* item = childItems[i]; - disconnect(item, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(refresh())); Item::_removeChild(i); item->deleteLater(); } diff --git a/ui/models/roster.cpp b/ui/models/roster.cpp index f252583..e124db7 100644 --- a/ui/models/roster.cpp +++ b/ui/models/roster.cpp @@ -29,17 +29,14 @@ Models::Roster::Roster(QObject* parent): groups(), contacts() { - connect(accountsModel, - SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)), - this, - SLOT(onAccountDataChanged(const QModelIndex&, const QModelIndex&, const QVector&))); - connect(root, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onChildChanged(Models::Item*, int, int))); - connect(root, SIGNAL(childIsAboutToBeInserted(Item*, int, int)), this, SLOT(onChildIsAboutToBeInserted(Item*, int, int))); - connect(root, SIGNAL(childInserted()), this, SLOT(onChildInserted())); - connect(root, SIGNAL(childIsAboutToBeRemoved(Item*, int, int)), this, SLOT(onChildIsAboutToBeRemoved(Item*, int, int))); - connect(root, SIGNAL(childRemoved()), this, SLOT(onChildRemoved())); - connect(root, SIGNAL(childIsAboutToBeMoved(Item*, int, int, Item*, int)), this, SLOT(onChildIsAboutToBeMoved(Item*, int, int, Item*, int))); - connect(root, SIGNAL(childMoved()), this, SLOT(onChildMoved())); + connect(accountsModel, &Accounts::dataChanged, this, &Roster::onAccountDataChanged); + connect(root, &Item::childChanged, this, &Roster::onChildChanged); + connect(root, &Item::childIsAboutToBeInserted, this, &Roster::onChildIsAboutToBeInserted); + connect(root, &Item::childInserted, this, &Roster::onChildInserted); + connect(root, &Item::childIsAboutToBeRemoved, this, &Roster::onChildIsAboutToBeRemoved); + connect(root, &Item::childRemoved, this, &Roster::onChildRemoved); + connect(root, &Item::childIsAboutToBeMoved, this, &Roster::onChildIsAboutToBeMoved); + connect(root, &Item::childMoved, this, &Roster::onChildMoved); } Models::Roster::~Roster() @@ -68,6 +65,10 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: { + if (index.column() != 0) { + result = ""; + break; + } switch (item->type) { case Item::group: { Group* gr = static_cast(item); @@ -91,26 +92,51 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const case Qt::DecorationRole: switch (item->type) { case Item::account: { + quint8 col = index.column(); Account* acc = static_cast(item); - result = acc->getStatusIcon(false); + if (col == 0) { + result = acc->getStatusIcon(false); + } else if (col == 1) { + QString path = acc->getAvatarPath(); + + if (path.size() > 0) { + result = QIcon(path); + } + } } break; case Item::contact: { Contact* contact = static_cast(item); - result = contact->getStatusIcon(false); + quint8 col = index.column(); + if (col == 0) { + result = contact->getStatusIcon(false); + } else if (col == 1) { + if (contact->getAvatarState() != Shared::Avatar::empty) { + result = QIcon(contact->getAvatarPath()); + } + } } break; case Item::presence: { + if (index.column() != 0) { + break; + } Presence* presence = static_cast(item); result = presence->getStatusIcon(false); } break; case Item::room: { + if (index.column() != 0) { + break; + } Room* room = static_cast(item); result = room->getStatusIcon(false); } break; case Item::participant: { + if (index.column() != 0) { + break; + } Participant* p = static_cast(item); result = p->getStatusIcon(false); } @@ -616,7 +642,8 @@ void Models::Roster::removeContact(const QString& account, const QString& jid, c void Models::Roster::onChildChanged(Models::Item* item, int row, int col) { QModelIndex index = createIndex(row, 0, item); - emit dataChanged(index, index); + QModelIndex index2 = createIndex(row, 1, item); + emit dataChanged(index, index2); } void Models::Roster::onChildIsAboutToBeInserted(Models::Item* parent, int first, int last) @@ -907,3 +934,23 @@ bool Models::Roster::groupHasContact(const QString& account, const QString& grou return gr->hasContact(contact); } } + +QString Models::Roster::getContactIconPath(const QString& account, const QString& jid) +{ + ElId id(account, jid); + std::multimap::const_iterator cItr = contacts.find(id); + QString path = ""; + if (cItr == contacts.end()) { + std::map::const_iterator rItr = rooms.find(id); + if (rItr == rooms.end()) { + qDebug() << "An attempt to get an icon path of non existing contact" << account << ":" << jid << ", returning empty value"; + } else { + //path = rItr->second->getRoomName(); + } + } else { + if (cItr->second->getAvatarState() != Shared::Avatar::empty) { + path = cItr->second->getAvatarPath(); + } + } + return path; +} diff --git a/ui/models/roster.h b/ui/models/roster.h index 30fb884..40c978d 100644 --- a/ui/models/roster.h +++ b/ui/models/roster.h @@ -73,6 +73,7 @@ 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); Accounts* accountsModel; diff --git a/ui/squawk.cpp b/ui/squawk.cpp index e382ebd..396eeb6 100644 --- a/ui/squawk.cpp +++ b/ui/squawk.cpp @@ -29,11 +29,17 @@ Squawk::Squawk(QWidget *parent) : rosterModel(), conversations(), contextMenu(new QMenu()), - dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()) + dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()), + requestedFiles(), + vCards() { m_ui->setupUi(this); m_ui->roster->setModel(&rosterModel); m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu); + m_ui->roster->setColumnWidth(1, 20); + m_ui->roster->setIconSize(QSize(20, 20)); + m_ui->roster->header()->setStretchLastSection(false); + m_ui->roster->header()->setSectionResizeMode(0, QHeaderView::Stretch); for (unsigned int i = Shared::availabilityLowest; i < Shared::availabilityHighest + 1; ++i) { Shared::Availability av = static_cast(i); @@ -41,15 +47,17 @@ Squawk::Squawk(QWidget *parent) : } m_ui->comboBox->setCurrentIndex(Shared::offline); - connect(m_ui->actionAccounts, SIGNAL(triggered()), this, SLOT(onAccounts())); - connect(m_ui->actionAddContact, SIGNAL(triggered()), this, SLOT(onNewContact())); - connect(m_ui->actionAddConference, SIGNAL(triggered()), this, SLOT(onNewConference())); - connect(m_ui->comboBox, SIGNAL(activated(int)), this, SLOT(onComboboxActivated(int))); - connect(m_ui->roster, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(onRosterItemDoubleClicked(const QModelIndex&))); - connect(m_ui->roster, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onRosterContextMenu(const QPoint&))); + connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts); + connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact); + connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference); + 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(rosterModel.accountsModel, SIGNAL(sizeChanged(unsigned int)), this, SLOT(onAccountsSizeChanged(unsigned int))); + connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged); //m_ui->mainToolBar->addWidget(m_ui->comboBox); + + setWindowTitle(tr("Contact list")); } Squawk::~Squawk() { @@ -61,12 +69,12 @@ void Squawk::onAccounts() if (accounts == 0) { accounts = new Accounts(rosterModel.accountsModel, this); accounts->setAttribute(Qt::WA_DeleteOnClose); - connect(accounts, SIGNAL(destroyed(QObject*)), this, SLOT(onAccountsClosed(QObject*))); - connect(accounts, SIGNAL(newAccount(const QMap&)), this, SIGNAL(newAccountRequest(const QMap&))); - connect(accounts, SIGNAL(changeAccount(const QString&, const QMap&)), this, SIGNAL(modifyAccountRequest(const QString&, const QMap&))); - connect(accounts, SIGNAL(connectAccount(const QString&)), this, SIGNAL(connectAccount(const QString&))); - connect(accounts, SIGNAL(disconnectAccount(const QString&)), this, SIGNAL(disconnectAccount(const QString&))); - connect(accounts, SIGNAL(removeAccount(const QString&)), this, SIGNAL(removeAccountRequest(const QString&))); + connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed); + connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest); + connect(accounts, &Accounts::changeAccount, this, &Squawk::modifyAccountRequest); + connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount); + connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount); + connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest); accounts->show(); } else { @@ -91,8 +99,8 @@ void Squawk::onNewContact() { NewContact* nc = new NewContact(rosterModel.accountsModel, this); - connect(nc, SIGNAL(accepted()), this, SLOT(onNewContactAccepted())); - connect(nc, SIGNAL(rejected()), nc, SLOT(deleteLater())); + connect(nc, &NewContact::accepted, this, &Squawk::onNewContactAccepted); + connect(nc, &NewContact::rejected, nc, &NewContact::deleteLater); nc->exec(); } @@ -101,8 +109,8 @@ void Squawk::onNewConference() { JoinConference* jc = new JoinConference(rosterModel.accountsModel, this); - connect(jc, SIGNAL(accepted()), this, SLOT(onJoinConferenceAccepted())); - connect(jc, SIGNAL(rejected()), jc, SLOT(deleteLater())); + connect(jc, &JoinConference::accepted, this, &Squawk::onJoinConferenceAccepted); + connect(jc, &JoinConference::rejected, jc, &JoinConference::deleteLater); jc->exec(); } @@ -132,12 +140,19 @@ void Squawk::closeEvent(QCloseEvent* event) if (accounts != 0) { accounts->close(); } + for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) { - disconnect(itr->second, SIGNAL(destroyed(QObject*)), this, SLOT(onConversationClosed(QObject*))); + 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); } @@ -284,12 +299,12 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) if (created) { conv->setAttribute(Qt::WA_DeleteOnClose); - connect(conv, SIGNAL(destroyed(QObject*)), this, SLOT(onConversationClosed(QObject*))); - connect(conv, SIGNAL(sendMessage(const Shared::Message&)), this, SLOT(onConversationMessage(const Shared::Message&))); - connect(conv, SIGNAL(requestArchive(const QString&)), this, SLOT(onConversationRequestArchive(const QString&))); - connect(conv, SIGNAL(requestLocalFile(const QString&, const QString&)), this, SLOT(onConversationRequestLocalFile(const QString&, const QString&))); - connect(conv, SIGNAL(downloadFile(const QString&, const QString&)), this, SLOT(onConversationDownloadFile(const QString&, const QString&))); - connect(conv, SIGNAL(shown()), this, SLOT(onConversationShown())); + connect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); + connect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage); + connect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive); + connect(conv, &Conversation::requestLocalFile, this, &Squawk::onConversationRequestLocalFile); + connect(conv, &Conversation::downloadFile, this, &Squawk::onConversationDownloadFile); + connect(conv, &Conversation::shown, this, &Squawk::onConversationShown); conversations.insert(std::make_pair(*id, conv)); @@ -449,11 +464,16 @@ void Squawk::accountMessage(const QString& account, const Shared::Message& data) void Squawk::notify(const QString& account, const Shared::Message& msg) { - QString name = QString(rosterModel.getContactName(account, msg.getPenPalJid()));; + QString name = QString(rosterModel.getContactName(account, msg.getPenPalJid())); + QString path = QString(rosterModel.getContactIconPath(account, msg.getPenPalJid())); QVariantList args; args << QString(QCoreApplication::applicationName()); args << QVariant(QVariant::UInt); //TODO some normal id - args << QString("mail-message"); //TODO icon + if (path.size() > 0) { + args << path; + } else { + args << QString("mail-message"); + } if (msg.getType() == Shared::Message::groupChat) { args << msg.getFromResource() + " from " + name; } else { @@ -497,10 +517,10 @@ void Squawk::removeAccount(const QString& account) Conversations::const_iterator lItr = itr; ++itr; Conversation* conv = lItr->second; - disconnect(conv, SIGNAL(destroyed(QObject*)), this, SLOT(onConversationClosed(QObject*))); - disconnect(conv, SIGNAL(sendMessage(const Shared::Message&)), this, SLOT(onConversationMessage(const Shared::Message&))); - disconnect(conv, SIGNAL(requestArchive(const QString&)), this, SLOT(onConversationRequestArchive(const QString&))); - disconnect(conv, SIGNAL(shown()), this, SLOT(onConversationShown())); + disconnect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); + disconnect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage); + disconnect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive); + disconnect(conv, &Conversation::shown, this, &Squawk::onConversationShown); conv->close(); conversations.erase(lItr); } else { @@ -538,6 +558,10 @@ void Squawk::onRosterContextMenu(const QPoint& point) }); } + QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); + card->setEnabled(active); + 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]() { @@ -616,7 +640,7 @@ void Squawk::onRosterContextMenu(const QPoint& point) } }); } - QAction* newGroup = groupsMenu->addAction(Shared::icon("resource-group-new"), tr("New group")); + QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group")); newGroup->setEnabled(active); connect(newGroup, &QAction::triggered, [this, accName, cntJID]() { QInputDialog* dialog = new QInputDialog(this); @@ -632,6 +656,10 @@ void Squawk::onRosterContextMenu(const QPoint& point) }); + 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)); + QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); remove->setEnabled(active); connect(remove, &QAction::triggered, [this, cnt]() { @@ -717,3 +745,61 @@ void Squawk::removeRoomParticipant(const QString& account, const QString& jid, c { rosterModel.removeRoomParticipant(account, jid, name); } + +void Squawk::responseVCard(const QString& jid, const Shared::VCard& card) +{ + std::map::const_iterator itr = vCards.find(jid); + if (itr != vCards.end()) { + itr->second->setVCard(card); + itr->second->hideProgress(); + } +} + +void Squawk::onVCardClosed() +{ + VCard* vCard = static_cast(sender()); + + std::map::const_iterator itr = vCards.find(vCard->getJid()); + if (itr == vCards.end()) { + qDebug() << "VCard has been closed but can not be found among other opened vCards, application is most probably going to crash"; + return; + } + vCards.erase(itr); +} + +void Squawk::onActivateVCard(const QString& account, const QString& jid, bool edition) +{ + std::map::const_iterator itr = vCards.find(jid); + VCard* card; + Models::Contact::Messages deque; + if (itr != vCards.end()) { + card = itr->second; + } else { + card = new VCard(jid, edition); + if (edition) { + card->setWindowTitle(tr("%1 account card").arg(account)); + } else { + card->setWindowTitle(tr("%1 contact card").arg(jid)); + } + card->setAttribute(Qt::WA_DeleteOnClose); + vCards.insert(std::make_pair(jid, card)); + + connect(card, &VCard::destroyed, this, &Squawk::onVCardClosed); + connect(card, &VCard::saveVCard, std::bind( &Squawk::onVCardSave, this, std::placeholders::_1, account)); + } + + card->show(); + card->raise(); + card->activateWindow(); + card->showProgress(tr("Downloading vCard")); + + emit requestVCard(account, jid); +} + +void Squawk::onVCardSave(const Shared::VCard& card, const QString& account) +{ + VCard* widget = static_cast(sender()); + emit uploadVCard(account, card); + + widget->deleteLater(); +} diff --git a/ui/squawk.h b/ui/squawk.h index c4d7f6f..819d255 100644 --- a/ui/squawk.h +++ b/ui/squawk.h @@ -34,8 +34,9 @@ #include "widgets/newcontact.h" #include "widgets/joinconference.h" #include "models/roster.h" +#include "widgets/vcard/vcard.h" -#include "../global.h" +#include "global.h" namespace Ui { class Squawk; @@ -71,6 +72,8 @@ signals: void removeRoomRequest(const QString& account, const QString& jid); void fileLocalPathRequest(const QString& messageId, const QString& url); void downloadFileRequest(const QString& messageId, const QString& url); + void requestVCard(const QString& account, const QString& jid); + void uploadVCard(const QString& account, const Shared::VCard& card); public slots: void newAccount(const QMap& account); @@ -96,6 +99,7 @@ public slots: void fileLocalPathResponse(const QString& messageId, const QString& path); void downloadFileError(const QString& messageId, const QString& error); void downloadFileProgress(const QString& messageId, qreal value); + void responseVCard(const QString& jid, const Shared::VCard& card); private: typedef std::map Conversations; @@ -107,6 +111,7 @@ private: QMenu* contextMenu; QDBusInterface dbus; std::map> requestedFiles; + std::map vCards; protected: void closeEvent(QCloseEvent * event) override; @@ -121,6 +126,9 @@ private slots: void onAccountsSizeChanged(unsigned int size); void onAccountsClosed(QObject* parent = 0); 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); diff --git a/ui/squawk.ui b/ui/squawk.ui index 642ded2..04cf999 100644 --- a/ui/squawk.ui +++ b/ui/squawk.ui @@ -51,6 +51,9 @@ QFrame::Sunken + + true + false @@ -122,7 +125,8 @@ false - + + .. Add conference diff --git a/ui/utils/badge.cpp b/ui/utils/badge.cpp index 94277f2..ef15bd2 100644 --- a/ui/utils/badge.cpp +++ b/ui/utils/badge.cpp @@ -42,7 +42,7 @@ Badge::Badge(const QString& p_id, const QString& p_text, const QIcon& icon, QWid layout->setContentsMargins(2, 2, 2, 2); - connect(closeButton, SIGNAL(clicked()), this, SIGNAL(close())); + connect(closeButton, &QPushButton::clicked, this, &Badge::close); } Badge::~Badge() diff --git a/ui/utils/comboboxdelegate.cpp b/ui/utils/comboboxdelegate.cpp new file mode 100644 index 0000000..7153405 --- /dev/null +++ b/ui/utils/comboboxdelegate.cpp @@ -0,0 +1,85 @@ +/* + * 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 "QTimer" + +#include "comboboxdelegate.h" + +ComboboxDelegate::ComboboxDelegate(QObject *parent): + QStyledItemDelegate(parent), + entries(), + ff(new FocusFilter()) +{ +} + + +ComboboxDelegate::~ComboboxDelegate() +{ + delete ff; +} + + +QWidget* ComboboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QComboBox *cb = new QComboBox(parent); + + for (const std::pair pair : entries) { + cb->addItem(pair.second, pair.first); + } + + return cb; +} + + +void ComboboxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QComboBox *cb = static_cast(editor); + int currentIndex = index.data(Qt::EditRole).toInt(); + if (currentIndex >= 0) { + cb->setCurrentIndex(currentIndex); + cb->installEventFilter(ff); + } +} + + +void ComboboxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QComboBox *cb = static_cast(editor); + model->setData(index, cb->currentIndex(), Qt::EditRole); +} + +void ComboboxDelegate::addEntry(const QString& title, const QIcon& icon) +{ + entries.emplace_back(title, icon); +} + +bool ComboboxDelegate::FocusFilter::eventFilter(QObject* src, QEvent* evt) +{ + if (evt->type() == QEvent::FocusIn) { + QComboBox* cb = static_cast(src); + cb->removeEventFilter(this); + QTimer* timer = new QTimer; //TODO that is ridiculous! I refuse to believe there is no better way than that one! + QObject::connect(timer, &QTimer::timeout, [timer, cb]() { + cb->showPopup(); + timer->deleteLater(); + }); + + timer->setSingleShot(true); + timer->start(100); + } + return QObject::eventFilter(src, evt); +} diff --git a/ui/utils/comboboxdelegate.h b/ui/utils/comboboxdelegate.h new file mode 100644 index 0000000..a5d79e4 --- /dev/null +++ b/ui/utils/comboboxdelegate.h @@ -0,0 +1,57 @@ +/* + * 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 COMBOBOXDELEGATE_H +#define COMBOBOXDELEGATE_H + +#include +#include +#include + +#include + +/** + * @todo write docs + */ +class ComboboxDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + class FocusFilter : public QObject { + public: + bool eventFilter(QObject *src, QEvent *evt) override; + }; + +public: + ComboboxDelegate(QObject *parent = nullptr); + ~ComboboxDelegate(); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + + void addEntry(const QString& title, const QIcon& icon = QIcon()); + +private: + std::deque> entries; + FocusFilter* ff; +}; + + + +#endif // COMBOBOXDELEGATE_H diff --git a/ui/utils/image.cpp b/ui/utils/image.cpp index 0abccf1..1d09709 100644 --- a/ui/utils/image.cpp +++ b/ui/utils/image.cpp @@ -19,19 +19,14 @@ #include #include "image.h" -Image::Image(const QString& path, QWidget* parent): +Image::Image(const QString& path, quint16 p_minWidth, QWidget* parent): QLabel(parent), pixmap(path), - aspectRatio(0) + aspectRatio(0), + minWidth(p_minWidth) { - - qreal height = pixmap.height(); - qreal width = pixmap.width(); - aspectRatio = width / height; - setPixmap(pixmap); setScaledContents(true); - setMinimumHeight(50 / aspectRatio); - setMinimumWidth(50); + recalculateAspectRatio(); } Image::~Image() @@ -42,11 +37,39 @@ Image::~Image() int Image::heightForWidth(int width) const { int height = width / aspectRatio; - //qDebug() << height << width << aspectRatio; return height; } +int Image::widthForHeight(int height) const +{ + return height * aspectRatio; +} + bool Image::hasHeightForWidth() const { return true; } + +void Image::recalculateAspectRatio() +{ + qreal height = pixmap.height(); + qreal width = pixmap.width(); + aspectRatio = width / height; + setPixmap(pixmap); + setMinimumHeight(minWidth / aspectRatio); + setMinimumWidth(minWidth); +} + +void Image::setMinWidth(quint16 p_minWidth) +{ + if (minWidth != p_minWidth) { + minWidth = p_minWidth; + recalculateAspectRatio(); + } +} + +void Image::setPath(const QString& path) +{ + pixmap = QPixmap(path); + recalculateAspectRatio(); +} diff --git a/ui/utils/image.h b/ui/utils/image.h index 7d61202..883ddf4 100644 --- a/ui/utils/image.h +++ b/ui/utils/image.h @@ -28,34 +28,23 @@ class Image : public QLabel { public: - /** - * Default constructor - */ - Image(const QString& path, QWidget* parent = nullptr); + Image(const QString& path, quint16 minWidth = 50, QWidget* parent = nullptr); - /** - * Destructor - */ ~Image(); - /** - * @todo write docs - * - * @param TODO - * @return TODO - */ int heightForWidth(int width) const override; - - /** - * @todo write docs - * - * @return TODO - */ - virtual bool hasHeightForWidth() const; + int widthForHeight(int height) const; + bool hasHeightForWidth() const override; + void setPath(const QString& path); + void setMinWidth(quint16 minWidth); private: QPixmap pixmap; qreal aspectRatio; + quint16 minWidth; + +private: + void recalculateAspectRatio(); }; #endif // IMAGE_H diff --git a/ui/utils/message.cpp b/ui/utils/message.cpp index 4c8debe..951037a 100644 --- a/ui/utils/message.cpp +++ b/ui/utils/message.cpp @@ -63,7 +63,6 @@ Message::Message(const Shared::Message& source, bool outgoing, const QString& p_ dFont.setItalic(true); dFont.setPointSize(dFont.pointSize() - 2); date->setFont(dFont); - date->setForegroundRole(QPalette::ToolTipText); QFont f; f.setBold(true); @@ -126,7 +125,7 @@ void Message::addDownloadDialog() fileComment->setText(tr("%1 is offering you to download a file").arg(sender->text())); } fileComment->show(); - connect(downloadButton, SIGNAL(clicked()), this, SLOT(onDownload())); + connect(downloadButton, &QPushButton::clicked, this, &Message::onDownload); bodyLayout->insertWidget(2, fileComment); bodyLayout->insertWidget(3, downloadButton); hasDownloadButton = true; diff --git a/ui/utils/messageline.cpp b/ui/utils/messageline.cpp index 0560344..06efa85 100644 --- a/ui/utils/messageline.cpp +++ b/ui/utils/messageline.cpp @@ -31,35 +31,11 @@ MessageLine::MessageLine(bool p_room, QWidget* parent): palNames(), views(), room(p_room), - busyPixmap(new QGraphicsPixmapItem(Shared::icon("view-refresh", true).pixmap(70))), - busyScene(), - busyLabel(&busyScene), - busyLayout(), busyShown(false), - rotation() + progress() { setBackgroundRole(QPalette::Base); layout->addStretch(); - - busyScene.addItem(busyPixmap); - busyLayout.addStretch(); - busyLayout.addWidget(&busyLabel); - busyLayout.addStretch(); - busyLabel.setMaximumSize(70, 70); - busyLabel.setMinimumSize(70, 70); - busyLabel.setSceneRect(0, 0, 70, 70); - busyLabel.setFrameStyle(0); - busyLabel.setContentsMargins(0, 0, 0, 0); - busyLabel.setInteractive(false); - busyPixmap->setTransformOriginPoint(35, 35); - busyPixmap->setTransformationMode(Qt::SmoothTransformation); - busyPixmap->setOffset(0, 0);; - - rotation.setDuration(500); - rotation.setStartValue(0.0f); - rotation.setEndValue(180.0f); - rotation.setLoopCount(-1); - connect(&rotation, SIGNAL(valueChanged(const QVariant&)), this, SLOT(onAnimationValueChanged(const QVariant&))); } MessageLine::~MessageLine() @@ -151,7 +127,7 @@ MessageLine::Position MessageLine::message(const Shared::Message& msg) if (msg.hasOutOfBandUrl()) {\ emit requestLocalFile(msg.getId(), msg.getOutOfBandUrl()); - connect(message, SIGNAL(downloadFile(const QString&, const QString&)), this, SIGNAL(downloadFile(const QString&, const QString&))); + connect(message, &Message::downloadFile, this, &MessageLine::downloadFile); } return res; @@ -201,28 +177,21 @@ QString MessageLine::firstMessageId() const void MessageLine::showBusyIndicator() { if (!busyShown) { - layout->insertLayout(0, &busyLayout); + layout->insertWidget(0, &progress); + progress.start(); busyShown = true; - rotation.start(); - busyLabel.show(); } } void MessageLine::hideBusyIndicator() { if (busyShown) { - busyLabel.hide(); - rotation.stop(); - layout->removeItem(&busyLayout); + progress.stop(); + layout->removeWidget(&progress); busyShown = false; } } -void MessageLine::onAnimationValueChanged(const QVariant& value) -{ - busyPixmap->setRotation(value.toReal()); -} - void MessageLine::responseDownloadProgress(const QString& messageId, qreal progress) { Index::const_iterator itr = messageIndex.find(messageId); diff --git a/ui/utils/messageline.h b/ui/utils/messageline.h index 3d7fb56..67280e4 100644 --- a/ui/utils/messageline.h +++ b/ui/utils/messageline.h @@ -25,13 +25,10 @@ #include #include #include -#include -#include -#include -#include #include "../../global.h" #include "message.h" +#include "progress.h" class MessageLine : public QWidget { @@ -85,15 +82,8 @@ private: std::map palNames; std::deque views; bool room; - QGraphicsPixmapItem* busyPixmap; - QGraphicsScene busyScene; - QGraphicsView busyLabel; - QHBoxLayout busyLayout; bool busyShown; - QVariantAnimation rotation; - -private slots: - void onAnimationValueChanged(const QVariant& value); + Progress progress; }; #endif // MESSAGELINE_H diff --git a/ui/utils/progress.cpp b/ui/utils/progress.cpp new file mode 100644 index 0000000..95eafa2 --- /dev/null +++ b/ui/utils/progress.cpp @@ -0,0 +1,85 @@ +/* + * 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 "progress.h" + +Progress::Progress(quint16 p_size, QWidget* parent): + QWidget(parent), + pixmap(new QGraphicsPixmapItem(Shared::icon("view-refresh", true).pixmap(p_size))), + scene(), + label(&scene), + progress(false), + animation(), + size(p_size) +{ + scene.addItem(pixmap); + label.setMaximumSize(size, size); + label.setMinimumSize(size, size); + label.setSceneRect(0, 0, size, size); + label.setFrameStyle(0); + label.setContentsMargins(0, 0, 0, 0); + label.setInteractive(false); + label.setStyleSheet("background: transparent"); + pixmap->setTransformOriginPoint(size / 2, size / 2); + pixmap->setTransformationMode(Qt::SmoothTransformation); + pixmap->setOffset(0, 0); + + animation.setDuration(500); + animation.setStartValue(0.0f); + animation.setEndValue(180.0f); + animation.setLoopCount(-1); + connect(&animation, &QVariantAnimation::valueChanged, this, &Progress::onValueChanged); + + QGridLayout* layout = new QGridLayout(); + setLayout(layout); + layout->setMargin(0); + layout->setVerticalSpacing(0); + layout->setHorizontalSpacing(0); + + setContentsMargins(0, 0, 0, 0); + + layout->addWidget(&label, 0, 0, 1, 1); + label.hide(); +} + +Progress::~Progress() +{ +} + +void Progress::onValueChanged(const QVariant& value) +{ + pixmap->setRotation(value.toReal()); +} + +void Progress::start() +{ + if (!progress) { + label.show(); + animation.start(); + progress = true; + } +} + +void Progress::stop() +{ + if (progress) { + label.hide(); + animation.stop(); + progress = false; + } +} diff --git a/ui/utils/progress.h b/ui/utils/progress.h new file mode 100644 index 0000000..c6501aa --- /dev/null +++ b/ui/utils/progress.h @@ -0,0 +1,57 @@ +/* + * 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 PROGRESS_H +#define PROGRESS_H + +#include +#include +#include +#include +#include +#include +#include + +#include "../../global.h" + +/** + * @todo write docs + */ +class Progress : public QWidget +{ + Q_OBJECT +public: + Progress(quint16 p_size = 70, QWidget* parent = nullptr); + ~Progress(); + + void start(); + void stop(); + +private slots: + void onValueChanged(const QVariant& value); + +private: + QGraphicsPixmapItem* pixmap; + QGraphicsScene scene; + QGraphicsView label; + bool progress; + QVariantAnimation animation; + quint16 size; +}; + +#endif // PROGRESS_H diff --git a/ui/widgets/CMakeLists.txt b/ui/widgets/CMakeLists.txt new file mode 100644 index 0000000..29e2dbc --- /dev/null +++ b/ui/widgets/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.0) +project(squawkWidgets) + +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) +# Instruct CMake to create code from Qt designer ui files +set(CMAKE_AUTOUIC ON) + +# Find the QtWidgets library +find_package(Qt5Widgets CONFIG REQUIRED) + +add_subdirectory(vcard) + +set(squawkWidgets_SRC + conversation.cpp + chat.cpp + room.cpp + newcontact.cpp + accounts.cpp + account.cpp + joinconference.cpp +) + +# Tell CMake to create the helloworld executable +add_library(squawkWidgets ${squawkWidgets_SRC}) + +# Use the Widgets module from Qt 5. +target_link_libraries(squawkWidgets vCardUI) +target_link_libraries(squawkWidgets Qt5::Widgets) diff --git a/ui/widgets/accounts.cpp b/ui/widgets/accounts.cpp index cb526cf..62e9ed3 100644 --- a/ui/widgets/accounts.cpp +++ b/ui/widgets/accounts.cpp @@ -29,14 +29,13 @@ Accounts::Accounts(Models::Accounts* p_model, QWidget *parent) : { m_ui->setupUi(this); - connect(m_ui->addButton, SIGNAL(clicked(bool)), this, SLOT(onAddButton(bool))); - connect(m_ui->editButton, SIGNAL(clicked(bool)), this, SLOT(onEditButton(bool))); - connect(m_ui->connectButton, SIGNAL(clicked(bool)), this, SLOT(onConnectButton(bool))); - connect(m_ui->deleteButton, SIGNAL(clicked(bool)), this, SLOT(onDeleteButton(bool))); + connect(m_ui->addButton, &QPushButton::clicked, this, &Accounts::onAddButton); + connect(m_ui->editButton, &QPushButton::clicked, this, &Accounts::onEditButton); + connect(m_ui->connectButton, &QPushButton::clicked, this, &Accounts::onConnectButton); + connect(m_ui->deleteButton, &QPushButton::clicked, this, &Accounts::onDeleteButton); m_ui->tableView->setModel(model); - connect(m_ui->tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), - this, SLOT(onSelectionChanged(const QItemSelection&, const QItemSelection&))); - connect(p_model, SIGNAL(changed()), this, SLOT(updateConnectButton())); + connect(m_ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Accounts::onSelectionChanged); + connect(p_model, &Models::Accounts::changed, this, &Accounts::updateConnectButton); } Accounts::~Accounts() = default; @@ -44,8 +43,8 @@ Accounts::~Accounts() = default; void Accounts::onAddButton(bool clicked) { Account* acc = new Account(); - connect(acc, SIGNAL(accepted()), this, SLOT(onAccountAccepted())); - connect(acc, SIGNAL(rejected()), this, SLOT(onAccountRejected())); + connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); + connect(acc, &Account::rejected, this, &Accounts::onAccountRejected); acc->exec(); } @@ -84,8 +83,8 @@ void Accounts::onEditButton(bool clicked) {"resource", mAcc->getResource()} }); acc->lockId(); - connect(acc, SIGNAL(accepted()), this, SLOT(onAccountAccepted())); - connect(acc, SIGNAL(rejected()), this, SLOT(onAccountRejected())); + connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); + connect(acc, &Account::rejected, this, &Accounts::onAccountRejected); editing = true; acc->exec(); } diff --git a/ui/widgets/chat.cpp b/ui/widgets/chat.cpp index 5e0b390..c876679 100644 --- a/ui/widgets/chat.cpp +++ b/ui/widgets/chat.cpp @@ -26,7 +26,7 @@ Chat::Chat(Models::Contact* p_contact, QWidget* parent): updateState(); setStatus(p_contact->getStatus()); - connect(contact, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onContactChanged(Models::Item*, int, int))); + connect(contact, &Models::Contact::childChanged, this, &Chat::onContactChanged); line->setMyName(p_contact->getAccountName()); } diff --git a/ui/widgets/conversation.cpp b/ui/widgets/conversation.cpp index d661a5c..36e7b6e 100644 --- a/ui/widgets/conversation.cpp +++ b/ui/widgets/conversation.cpp @@ -59,15 +59,15 @@ Conversation::Conversation(bool muc, const QString& mJid, const QString mRes, co statusIcon = m_ui->statusIcon; statusLabel = m_ui->statusLabel; - connect(&ker, SIGNAL(enterPressed()), this, SLOT(onEnterPressed())); - connect(&res, SIGNAL(resized()), this, SLOT(onScrollResize())); - connect(&vis, SIGNAL(shown()), this, SLOT(onScrollResize())); - connect(&vis, SIGNAL(hidden()), this, SLOT(onScrollResize())); - connect(m_ui->sendButton, SIGNAL(clicked(bool)), this, SLOT(onEnterPressed())); - connect(line, SIGNAL(resize(int)), this, SLOT(onMessagesResize(int))); - connect(line, SIGNAL(downloadFile(const QString&, const QString&)), this, SIGNAL(downloadFile(const QString&, const QString&))); - connect(line, SIGNAL(requestLocalFile(const QString&, const QString&)), this, SIGNAL(requestLocalFile(const QString&, const QString&))); - connect(m_ui->attachButton, SIGNAL(clicked(bool)), this, SLOT(onAttach())); + connect(&ker, &KeyEnterReceiver::enterPressed, this, &Conversation::onEnterPressed); + connect(&res, &Resizer::resized, this, &Conversation::onScrollResize); + connect(&vis, &VisibilityCatcher::shown, this, &Conversation::onScrollResize); + connect(&vis, &VisibilityCatcher::hidden, this, &Conversation::onScrollResize); + connect(m_ui->sendButton, &QPushButton::clicked, this, &Conversation::onEnterPressed); + connect(line, &MessageLine::resize, this, &Conversation::onMessagesResize); + connect(line, &MessageLine::downloadFile, this, &Conversation::downloadFile); + connect(line, &MessageLine::requestLocalFile, this, &Conversation::requestLocalFile); + connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach); m_ui->messageEditor->installEventFilter(&ker); @@ -76,7 +76,7 @@ Conversation::Conversation(bool muc, const QString& mJid, const QString mRes, co vs->installEventFilter(&vis); vs->setBackgroundRole(QPalette::Base); vs->setAutoFillBackground(true); - connect(vs, SIGNAL(valueChanged(int)), this, SLOT(onSliderValueChanged(int))); + connect(vs, &QScrollBar::valueChanged, this, &Conversation::onSliderValueChanged); m_ui->scrollArea->installEventFilter(&res); applyVisualEffects(); @@ -257,11 +257,11 @@ void Conversation::showEvent(QShowEvent* event) void Conversation::onAttach() { - QFileDialog* d = new QFileDialog(this, "Chose a file to send"); + QFileDialog* d = new QFileDialog(this, tr("Chose a file to send")); d->setFileMode(QFileDialog::ExistingFile); - connect(d, SIGNAL(accepted()), this, SLOT(onFileSelected())); - connect(d, SIGNAL(rejected()), d, SLOT(deleteLater())); + connect(d, &QFileDialog::accepted, this, &Conversation::onFileSelected); + connect(d, &QFileDialog::rejected, d, &QFileDialog::deleteLater); d->show(); } @@ -317,7 +317,7 @@ void Conversation::addAttachedFile(const QString& path) Badge* badge = new Badge(path, info.fileName(), QIcon::fromTheme(type.iconName())); - connect(badge, SIGNAL(close()), this, SLOT(onBadgeClose())); + connect(badge, &Badge::close, this, &Conversation::onBadgeClose); filesToAttach.push_back(badge); //TODO neet to check if there are any duplicated ids filesLayout->addWidget(badge); if (filesLayout->count() == 1) { diff --git a/ui/widgets/room.cpp b/ui/widgets/room.cpp index b1f9c9c..98fc97d 100644 --- a/ui/widgets/room.cpp +++ b/ui/widgets/room.cpp @@ -26,7 +26,7 @@ Room::Room(Models::Room* p_room, QWidget* parent): line->setMyName(room->getNick()); setStatus(room->getSubject()); - connect(room, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onRoomChanged(Models::Item*, int, int))); + connect(room, &Models::Room::childChanged, this, &Room::onRoomChanged); } Room::~Room() diff --git a/ui/widgets/vcard/CMakeLists.txt b/ui/widgets/vcard/CMakeLists.txt new file mode 100644 index 0000000..4d2ee15 --- /dev/null +++ b/ui/widgets/vcard/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.0) +project(vCardUI) + +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) +# Instruct CMake to create code from Qt designer ui files +set(CMAKE_AUTOUIC ON) + +# Find the QtWidgets library +find_package(Qt5Widgets CONFIG REQUIRED) + +set(vCardUI_SRC + vcard.cpp + emailsmodel.cpp + phonesmodel.cpp +) + +# Tell CMake to create the helloworld executable +add_library(vCardUI ${vCardUI_SRC}) + +# Use the Widgets module from Qt 5. +target_link_libraries(vCardUI Qt5::Widgets) diff --git a/ui/widgets/vcard/emailsmodel.cpp b/ui/widgets/vcard/emailsmodel.cpp new file mode 100644 index 0000000..4044322 --- /dev/null +++ b/ui/widgets/vcard/emailsmodel.cpp @@ -0,0 +1,205 @@ +/* + * 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 "emailsmodel.h" + +UI::VCard::EMailsModel::EMailsModel(bool p_edit, QObject* parent): + QAbstractTableModel(parent), + edit(p_edit), + deque() +{ +} + +int UI::VCard::EMailsModel::columnCount(const QModelIndex& parent) const +{ + return 3; +} + +int UI::VCard::EMailsModel::rowCount(const QModelIndex& parent) const +{ + return deque.size(); +} + +QVariant UI::VCard::EMailsModel::data(const QModelIndex& index, int role) const +{ + if (index.isValid()) { + int col = index.column(); + switch (col) { + case 0: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return deque[index.row()].address; + default: + return QVariant(); + } + break; + case 1: + switch (role) { + case Qt::DisplayRole: + return QCoreApplication::translate("Global", Shared::VCard::Email::roleNames[deque[index.row()].role].toStdString().c_str()); + case Qt::EditRole: + return deque[index.row()].role; + default: + return QVariant(); + } + break; + case 2: + switch (role) { + case Qt::DisplayRole: + return QVariant(); + case Qt::DecorationRole: + if (deque[index.row()].prefered) { + return Shared::icon("favorite", false); + } + return QVariant(); + default: + return QVariant(); + } + break; + default: + return QVariant(); + } + } + return QVariant(); +} + +Qt::ItemFlags UI::VCard::EMailsModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = QAbstractTableModel::flags(index); + if (edit && index.column() != 2) { + f = Qt::ItemIsEditable | f; + } + return f; +} + +bool UI::VCard::EMailsModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (role == Qt::EditRole && checkIndex(index)) { + Shared::VCard::Email& item = deque[index.row()]; + switch (index.column()) { + case 0: + item.address = value.toString(); + return true; + case 1: { + quint8 newRole = value.toUInt(); + if (newRole > Shared::VCard::Email::work) { + return false; + } + item.role = static_cast(newRole); + return true; + } + case 2: { + bool newDef = value.toBool(); + if (newDef != item.prefered) { + if (newDef) { + //dropPrefered(); + } + item.prefered = newDef; + return true; + } + } + } + return true; + } + return false; +} + + +bool UI::VCard::EMailsModel::dropPrefered() +{ + bool dropped = false; + int i = 0; + for (Shared::VCard::Email& email : deque) { + if (email.prefered) { + email.prefered = false; + QModelIndex ci = createIndex(i, 2, &email); + emit dataChanged(ci, ci); + dropped = true; + } + ++i; + } + return dropped; +} + +QModelIndex UI::VCard::EMailsModel::addNewEmptyLine() +{ + beginInsertRows(QModelIndex(), deque.size(), deque.size()); + deque.emplace_back(""); + endInsertRows(); + return createIndex(deque.size() - 1, 0, &(deque.back())); +} + +bool UI::VCard::EMailsModel::isPreferred(int row) const +{ + if (row < deque.size()) { + return deque[row].prefered; + } else { + return false; + } +} + +void UI::VCard::EMailsModel::removeLines(int index, int count) +{ + if (index < deque.size()) { + int maxCount = deque.size() - index; + if (count > maxCount) { + count = maxCount; + } + + if (count > 0) { + beginRemoveRows(QModelIndex(), index, index + count - 1); + std::deque::const_iterator itr = deque.begin() + index; + std::deque::const_iterator end = itr + count; + deque.erase(itr, end); + endRemoveRows(); + } + } +} + +void UI::VCard::EMailsModel::getEmails(std::deque& emails) const +{ + for (const Shared::VCard::Email& my : deque) { + emails.emplace_back(my); + } +} + +void UI::VCard::EMailsModel::setEmails(const std::deque& emails) +{ + if (deque.size() > 0) { + removeLines(0, deque.size()); + } + + if (emails.size() > 0) { + beginInsertRows(QModelIndex(), 0, emails.size() - 1); + for (const Shared::VCard::Email& comming : emails) { + deque.emplace_back(comming); + } + endInsertRows(); + } +} + +void UI::VCard::EMailsModel::revertPreferred(int row) +{ + setData(createIndex(row, 2), !isPreferred(row)); +} + +QString UI::VCard::EMailsModel::getEmail(int row) const +{ + return deque[row].address; +} diff --git a/ui/widgets/vcard/emailsmodel.h b/ui/widgets/vcard/emailsmodel.h new file mode 100644 index 0000000..358536f --- /dev/null +++ b/ui/widgets/vcard/emailsmodel.h @@ -0,0 +1,64 @@ +/* + * 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 UI_VCARD_EMAILSMODEL_H +#define UI_VCARD_EMAILSMODEL_H + +#include +#include + +#include + +#include "global.h" + +namespace UI { +namespace VCard { + +class EMailsModel : public QAbstractTableModel +{ + Q_OBJECT +public: + EMailsModel(bool edit = false, QObject *parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool isPreferred(int row) const; + + void removeLines(int index, int count); + void setEmails(const std::deque& emails); + void getEmails(std::deque& emails) const; + QString getEmail(int row) const; + +public slots: + QModelIndex addNewEmptyLine(); + void revertPreferred(int row); + +private: + bool edit; + std::deque deque; + +private: + bool dropPrefered(); +}; + +}} + +#endif // UI_VCARD_EMAILSMODEL_H diff --git a/ui/widgets/vcard/phonesmodel.cpp b/ui/widgets/vcard/phonesmodel.cpp new file mode 100644 index 0000000..df9cad6 --- /dev/null +++ b/ui/widgets/vcard/phonesmodel.cpp @@ -0,0 +1,222 @@ +/* + * 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 "phonesmodel.h" + +UI::VCard::PhonesModel::PhonesModel(bool p_edit, QObject* parent): + QAbstractTableModel(parent), + edit(p_edit), + deque() +{ +} + +int UI::VCard::PhonesModel::columnCount(const QModelIndex& parent) const +{ + return 4; +} + +int UI::VCard::PhonesModel::rowCount(const QModelIndex& parent) const +{ + return deque.size(); +} + +QVariant UI::VCard::PhonesModel::data(const QModelIndex& index, int role) const +{ + if (index.isValid()) { + int col = index.column(); + switch (col) { + case 0: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return deque[index.row()].number; + default: + return QVariant(); + } + break; + case 1: + switch (role) { + case Qt::DisplayRole: + return QCoreApplication::translate("Global", Shared::VCard::Phone::roleNames[deque[index.row()].role].toStdString().c_str()); + case Qt::EditRole: + return deque[index.row()].role; + default: + return QVariant(); + } + break; + case 2: + switch (role) { + case Qt::DisplayRole: + return QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[deque[index.row()].type].toStdString().c_str()); + case Qt::EditRole: + return deque[index.row()].type; + default: + return QVariant(); + } + break; + case 3: + switch (role) { + case Qt::DisplayRole: + return QVariant(); + case Qt::DecorationRole: + if (deque[index.row()].prefered) { + return Shared::icon("favorite", false); + } + return QVariant(); + default: + return QVariant(); + } + break; + default: + return QVariant(); + } + } + return QVariant(); +} + +QModelIndex UI::VCard::PhonesModel::addNewEmptyLine() +{ + beginInsertRows(QModelIndex(), deque.size(), deque.size()); + deque.emplace_back("", Shared::VCard::Phone::other); + endInsertRows(); + return createIndex(deque.size() - 1, 0, &(deque.back())); +} + +Qt::ItemFlags UI::VCard::PhonesModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = QAbstractTableModel::flags(index); + if (edit && index.column() != 3) { + f = Qt::ItemIsEditable | f; + } + return f; +} + +bool UI::VCard::PhonesModel::dropPrefered() +{ + bool dropped = false; + int i = 0; + for (Shared::VCard::Phone& phone : deque) { + if (phone.prefered) { + phone.prefered = false; + QModelIndex ci = createIndex(i, 2, &phone); + emit dataChanged(ci, ci); + dropped = true; + } + ++i; + } + return dropped; +} + +void UI::VCard::PhonesModel::getPhones(std::deque& phones) const +{ + for (const Shared::VCard::Phone& my : deque) { + phones.emplace_back(my); + } +} + +bool UI::VCard::PhonesModel::isPreferred(int row) const +{ + if (row < deque.size()) { + return deque[row].prefered; + } else { + return false; + } +} + +void UI::VCard::PhonesModel::removeLines(int index, int count) +{ + if (index < deque.size()) { + int maxCount = deque.size() - index; + if (count > maxCount) { + count = maxCount; + } + + if (count > 0) { + beginRemoveRows(QModelIndex(), index, index + count - 1); + std::deque::const_iterator itr = deque.begin() + index; + std::deque::const_iterator end = itr + count; + deque.erase(itr, end); + endRemoveRows(); + } + } +} + +void UI::VCard::PhonesModel::revertPreferred(int row) +{ + setData(createIndex(row, 3), !isPreferred(row)); +} + +bool UI::VCard::PhonesModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (role == Qt::EditRole && checkIndex(index)) { + Shared::VCard::Phone& item = deque[index.row()]; + switch (index.column()) { + case 0: + item.number = value.toString(); + return true; + case 1: { + quint8 newRole = value.toUInt(); + if (newRole > Shared::VCard::Phone::work) { + return false; + } + item.role = static_cast(newRole); + return true; + } + case 2: { + quint8 newType = value.toUInt(); + if (newType > Shared::VCard::Phone::other) { + return false; + } + item.type = static_cast(newType); + return true; + } + case 3: { + bool newDef = value.toBool(); + if (newDef != item.prefered) { + if (newDef) { + //dropPrefered(); + } + item.prefered = newDef; + return true; + } + } + } + return true; + } + return false; +} + +void UI::VCard::PhonesModel::setPhones(const std::deque& phones) +{ + if (deque.size() > 0) { + removeLines(0, deque.size()); + } + + if (phones.size() > 0) { + beginInsertRows(QModelIndex(), 0, phones.size() - 1); + for (const Shared::VCard::Phone& comming : phones) { + deque.emplace_back(comming); + } + endInsertRows(); + } +} + +QString UI::VCard::PhonesModel::getPhone(int row) const +{ + return deque[row].number; +} diff --git a/ui/widgets/vcard/phonesmodel.h b/ui/widgets/vcard/phonesmodel.h new file mode 100644 index 0000000..bf847d2 --- /dev/null +++ b/ui/widgets/vcard/phonesmodel.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 UI_VCARD_PHONESMODEL_H +#define UI_VCARD_PHONESMODEL_H + +#include +#include + +#include "global.h" + +namespace UI { +namespace VCard { + +/** + * @todo write docs + */ +class PhonesModel : public QAbstractTableModel +{ + Q_OBJECT +public: + PhonesModel(bool edit = false, QObject *parent = nullptr); + + QVariant data(const QModelIndex& index, int role) const override; + int columnCount(const QModelIndex& parent) const override; + int rowCount(const QModelIndex& parent) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool isPreferred(int row) const; + + void removeLines(int index, int count); + void setPhones(const std::deque& phones); + void getPhones(std::deque& phones) const; + QString getPhone(int row) const; + +public slots: + QModelIndex addNewEmptyLine(); + void revertPreferred(int row); + +private: + bool edit; + std::deque deque; + +private: + bool dropPrefered(); +}; + +}} + +#endif // UI_VCARD_PHONESMODEL_H diff --git a/ui/widgets/vcard/vcard.cpp b/ui/widgets/vcard/vcard.cpp new file mode 100644 index 0000000..44b5aa3 --- /dev/null +++ b/ui/widgets/vcard/vcard.cpp @@ -0,0 +1,460 @@ +/* + * 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 "vcard.h" +#include "ui_vcard.h" + +#include + +#include + +const std::set VCard::supportedTypes = {"image/jpeg", "image/png"}; + +VCard::VCard(const QString& jid, bool edit, QWidget* parent): + QWidget(parent), + m_ui(new Ui::VCard()), + avatarButtonMargins(), + avatarMenu(nullptr), + editable(edit), + currentAvatarType(Shared::Avatar::empty), + currentAvatarPath(""), + progress(new Progress(100)), + progressLabel(new QLabel()), + overlay(new QWidget()), + contextMenu(new QMenu()), + emails(edit), + phones(edit), + roleDelegate(new ComboboxDelegate()), + phoneTypeDelegate(new ComboboxDelegate()) +{ + m_ui->setupUi(this); + m_ui->jabberID->setText(jid); + m_ui->jabberID->setReadOnly(true); + + QAction* setAvatar = m_ui->actionSetAvatar; + QAction* clearAvatar = m_ui->actionClearAvatar; + + connect(setAvatar, &QAction::triggered, this, &VCard::onSetAvatar); + connect(clearAvatar, &QAction::triggered, this, &VCard::onClearAvatar); + + setAvatar->setEnabled(true); + clearAvatar->setEnabled(false); + + roleDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Email::roleNames[0].toStdString().c_str())); + roleDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Email::roleNames[1].toStdString().c_str())); + roleDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Email::roleNames[2].toStdString().c_str())); + + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[0].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[1].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[2].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[3].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[4].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[5].toStdString().c_str())); + phoneTypeDelegate->addEntry(QCoreApplication::translate("Global", Shared::VCard::Phone::typeNames[6].toStdString().c_str())); + + m_ui->emailsView->setContextMenuPolicy(Qt::CustomContextMenu); + m_ui->emailsView->setModel(&emails); + m_ui->emailsView->setItemDelegateForColumn(1, roleDelegate); + m_ui->emailsView->setColumnWidth(2, 25); + m_ui->emailsView->horizontalHeader()->setStretchLastSection(false); + m_ui->emailsView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->phonesView->setContextMenuPolicy(Qt::CustomContextMenu); + m_ui->phonesView->setModel(&phones); + m_ui->phonesView->setItemDelegateForColumn(1, roleDelegate); + m_ui->phonesView->setItemDelegateForColumn(2, phoneTypeDelegate); + m_ui->phonesView->setColumnWidth(3, 25); + m_ui->phonesView->horizontalHeader()->setStretchLastSection(false); + m_ui->phonesView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + + connect(m_ui->phonesView, &QWidget::customContextMenuRequested, this, &VCard::onContextMenu); + connect(m_ui->emailsView, &QWidget::customContextMenuRequested, this, &VCard::onContextMenu); + + if (edit) { + avatarMenu = new QMenu(); + m_ui->avatarButton->setMenu(avatarMenu); + avatarMenu->addAction(setAvatar); + avatarMenu->addAction(clearAvatar); + m_ui->title->setText(tr("Account %1 card").arg(jid)); + } else { + m_ui->buttonBox->hide(); + m_ui->fullName->setReadOnly(true); + m_ui->firstName->setReadOnly(true); + m_ui->middleName->setReadOnly(true); + m_ui->lastName->setReadOnly(true); + m_ui->nickName->setReadOnly(true); + m_ui->birthday->setReadOnly(true); + m_ui->organizationName->setReadOnly(true); + m_ui->organizationDepartment->setReadOnly(true); + m_ui->organizationTitle->setReadOnly(true); + m_ui->organizationRole->setReadOnly(true); + m_ui->description->setReadOnly(true); + m_ui->url->setReadOnly(true); + m_ui->title->setText(tr("Contact %1 card").arg(jid)); + } + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &VCard::onButtonBoxAccepted); + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &VCard::close); + + avatarButtonMargins = m_ui->avatarButton->size(); + + int height = m_ui->personalForm->minimumSize().height() - avatarButtonMargins.height(); + m_ui->avatarButton->setIconSize(QSize(height, height)); + + QGridLayout* gr = static_cast(layout()); + gr->addWidget(overlay, 0, 0, 4, 1); + QVBoxLayout* nl = new QVBoxLayout(); + QGraphicsOpacityEffect* opacity = new QGraphicsOpacityEffect(); + opacity->setOpacity(0.8); + overlay->setLayout(nl); + overlay->setBackgroundRole(QPalette::Base); + overlay->setAutoFillBackground(true); + overlay->setGraphicsEffect(opacity); + progressLabel->setAlignment(Qt::AlignCenter); + progressLabel->setStyleSheet("font: 16pt"); + nl->addStretch(); + nl->addWidget(progress); + nl->addWidget(progressLabel); + nl->addStretch(); + overlay->hide(); +} + +VCard::~VCard() +{ + if (editable) { + avatarMenu->deleteLater(); + } + + phoneTypeDelegate->deleteLater(); + roleDelegate->deleteLater(); + contextMenu->deleteLater(); +} + +void VCard::setVCard(const QString& jid, const Shared::VCard& card) +{ + m_ui->jabberID->setText(jid); + setVCard(card); +} + +void VCard::setVCard(const Shared::VCard& card) +{ + m_ui->fullName->setText(card.getFullName()); + m_ui->firstName->setText(card.getFirstName()); + m_ui->middleName->setText(card.getMiddleName()); + m_ui->lastName->setText(card.getLastName()); + m_ui->nickName->setText(card.getNickName()); + m_ui->birthday->setDate(card.getBirthday()); + m_ui->organizationName->setText(card.getOrgName()); + m_ui->organizationDepartment->setText(card.getOrgUnit()); + m_ui->organizationTitle->setText(card.getOrgTitle()); + m_ui->organizationRole->setText(card.getOrgRole()); + m_ui->description->setText(card.getDescription()); + m_ui->url->setText(card.getUrl()); + + QDateTime receivingTime = card.getReceivingTime(); + m_ui->receivingTimeLabel->setText(tr("Received %1 at %2").arg(receivingTime.date().toString()).arg(receivingTime.time().toString())); + currentAvatarType = card.getAvatarType(); + currentAvatarPath = card.getAvatarPath(); + + updateAvatar(); + + const std::deque& ems = card.getEmails(); + const std::deque& phs = card.getPhones(); + emails.setEmails(ems); + phones.setPhones(phs); +} + +QString VCard::getJid() const +{ + return m_ui->jabberID->text(); +} + +void VCard::onButtonBoxAccepted() +{ + Shared::VCard card; + card.setFullName(m_ui->fullName->text()); + card.setFirstName(m_ui->firstName->text()); + card.setMiddleName(m_ui->middleName->text()); + card.setLastName(m_ui->lastName->text()); + card.setNickName(m_ui->nickName->text()); + card.setBirthday(m_ui->birthday->date()); + card.setDescription(m_ui->description->toPlainText()); + card.setUrl(m_ui->url->text()); + card.setOrgName(m_ui->organizationName->text()); + card.setOrgUnit(m_ui->organizationDepartment->text()); + card.setOrgRole(m_ui->organizationRole->text()); + card.setOrgTitle(m_ui->organizationTitle->text()); + card.setAvatarPath(currentAvatarPath); + card.setAvatarType(currentAvatarType); + + emails.getEmails(card.getEmails()); + phones.getPhones(card.getPhones()); + + emit saveVCard(card); +} + +void VCard::onClearAvatar() +{ + currentAvatarType = Shared::Avatar::empty; + currentAvatarPath = ""; + + updateAvatar(); +} + +void VCard::onSetAvatar() +{ + QFileDialog* d = new QFileDialog(this, tr("Chose your new avatar")); + d->setFileMode(QFileDialog::ExistingFile); + d->setNameFilter(tr("Images (*.png *.jpg *.jpeg)")); + + connect(d, &QFileDialog::accepted, this, &VCard::onAvatarSelected); + connect(d, &QFileDialog::rejected, d, &QFileDialog::deleteLater); + + d->show(); +} + +void VCard::updateAvatar() +{ + int height = m_ui->personalForm->minimumSize().height() - avatarButtonMargins.height(); + switch (currentAvatarType) { + case Shared::Avatar::empty: + m_ui->avatarButton->setIcon(Shared::icon("user", true)); + m_ui->avatarButton->setIconSize(QSize(height, height)); + m_ui->actionClearAvatar->setEnabled(false); + break; + case Shared::Avatar::autocreated: + case Shared::Avatar::valid: + QPixmap pixmap(currentAvatarPath); + qreal h = pixmap.height(); + qreal w = pixmap.width(); + qreal aspectRatio = w / h; + m_ui->avatarButton->setIconSize(QSize(height * aspectRatio, height)); + m_ui->avatarButton->setIcon(QIcon(currentAvatarPath)); + m_ui->actionClearAvatar->setEnabled(true); + break; + } +} + +void VCard::onAvatarSelected() +{ + QFileDialog* d = static_cast(sender()); + QMimeDatabase db; + QString path = d->selectedFiles().front(); + QMimeType type = db.mimeTypeForFile(path); + d->deleteLater(); + + if (supportedTypes.find(type.name()) == supportedTypes.end()) { + qDebug() << "Selected for avatar file is not supported, skipping"; + } else { + QImage src(path); + QImage dst; + if (src.width() > 160 || src.height() > 160) { + dst = src.scaled(160, 160, Qt::KeepAspectRatio); + } + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + m_ui->jabberID->text() + ".temp." + type.preferredSuffix(); + QFile oldTemp(path); + if (oldTemp.exists()) { + if (!oldTemp.remove()) { + qDebug() << "Error removing old temp avatar" << path; + return; + } + } + bool success = dst.save(path); + if (success) { + currentAvatarPath = path; + currentAvatarType = Shared::Avatar::valid; + + updateAvatar(); + } else { + qDebug() << "couldn't save avatar" << path << ", skipping"; + } + } +} + +void VCard::showProgress(const QString& line) +{ + progressLabel->setText(line); + overlay->show(); + progress->start(); +} + +void VCard::hideProgress() +{ + overlay->hide(); + progress->stop(); +} + +void VCard::onContextMenu(const QPoint& point) +{ + contextMenu->clear(); + bool hasMenu = false; + QAbstractItemView* snd = static_cast(sender()); + if (snd == m_ui->emailsView) { + hasMenu = true; + if (editable) { + QAction* add = contextMenu->addAction(Shared::icon("list-add"), tr("Add email address")); + connect(add, &QAction::triggered, this, &VCard::onAddEmail); + + QItemSelectionModel* sm = m_ui->emailsView->selectionModel(); + int selectionSize = sm->selectedRows().size(); + + if (selectionSize > 0) { + if (selectionSize == 1) { + int row = sm->selectedRows().at(0).row(); + if (emails.isPreferred(row)) { + QAction* rev = contextMenu->addAction(Shared::icon("unfavorite"), tr("Unset this email as preferred")); + connect(rev, &QAction::triggered, std::bind(&UI::VCard::EMailsModel::revertPreferred, &emails, row)); + } else { + QAction* rev = contextMenu->addAction(Shared::icon("favorite"), tr("Set this email as preferred")); + connect(rev, &QAction::triggered, std::bind(&UI::VCard::EMailsModel::revertPreferred, &emails, row)); + } + } + + QAction* del = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove selected email addresses")); + connect(del, &QAction::triggered, this, &VCard::onRemoveEmail); + } + } + + QAction* cp = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected emails to clipboard")); + connect(cp, &QAction::triggered, this, &VCard::onCopyEmail); + } else if (snd == m_ui->phonesView) { + hasMenu = true; + if (editable) { + QAction* add = contextMenu->addAction(Shared::icon("list-add"), tr("Add phone number")); + connect(add, &QAction::triggered, this, &VCard::onAddPhone); + + QItemSelectionModel* sm = m_ui->phonesView->selectionModel(); + int selectionSize = sm->selectedRows().size(); + + if (selectionSize > 0) { + if (selectionSize == 1) { + int row = sm->selectedRows().at(0).row(); + if (phones.isPreferred(row)) { + QAction* rev = contextMenu->addAction(Shared::icon("view-media-favorite"), tr("Unset this phone as preferred")); + connect(rev, &QAction::triggered, std::bind(&UI::VCard::PhonesModel::revertPreferred, &phones, row)); + } else { + QAction* rev = contextMenu->addAction(Shared::icon("favorite"), tr("Set this phone as preferred")); + connect(rev, &QAction::triggered, std::bind(&UI::VCard::PhonesModel::revertPreferred, &phones, row)); + } + } + + QAction* del = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove selected phone numbers")); + connect(del, &QAction::triggered, this, &VCard::onRemovePhone); + } + } + + QAction* cp = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected phones to clipboard")); + connect(cp, &QAction::triggered, this, &VCard::onCopyPhone); + } + + if (hasMenu) { + contextMenu->popup(snd->viewport()->mapToGlobal(point)); + } +} + +void VCard::onAddEmail() +{ + QModelIndex index = emails.addNewEmptyLine(); + m_ui->emailsView->setCurrentIndex(index); + m_ui->emailsView->edit(index); +} + +void VCard::onAddAddress() +{ + +} +void VCard::onAddPhone() +{ + QModelIndex index = phones.addNewEmptyLine(); + m_ui->phonesView->setCurrentIndex(index); + m_ui->phonesView->edit(index); +} +void VCard::onRemoveAddress() +{ +} +void VCard::onRemoveEmail() +{ + QItemSelection selection(m_ui->emailsView->selectionModel()->selection()); + + QList rows; + for (const QModelIndex& index : selection.indexes()) { + rows.append(index.row()); + } + + std::sort(rows.begin(), rows.end()); + + int prev = -1; + for (int i = rows.count() - 1; i >= 0; i -= 1) { + int current = rows[i]; + if (current != prev) { + emails.removeLines(current, 1); + prev = current; + } + } +} + +void VCard::onRemovePhone() +{ + QItemSelection selection(m_ui->phonesView->selectionModel()->selection()); + + QList rows; + for (const QModelIndex& index : selection.indexes()) { + rows.append(index.row()); + } + + std::sort(rows.begin(), rows.end()); + + int prev = -1; + for (int i = rows.count() - 1; i >= 0; i -= 1) { + int current = rows[i]; + if (current != prev) { + phones.removeLines(current, 1); + prev = current; + } + } +} + +void VCard::onCopyEmail() +{ + QList selection(m_ui->emailsView->selectionModel()->selectedRows()); + + QList addrs; + for (const QModelIndex& index : selection) { + addrs.push_back(emails.getEmail(index.row())); + } + + QString list = addrs.join("\n"); + + QClipboard* cb = QApplication::clipboard(); + cb->setText(list); +} + +void VCard::onCopyPhone() +{ + QList selection(m_ui->phonesView->selectionModel()->selectedRows()); + + QList phs; + for (const QModelIndex& index : selection) { + phs.push_back(phones.getPhone(index.row())); + } + + QString list = phs.join("\n"); + + QClipboard* cb = QApplication::clipboard(); + cb->setText(list); +} diff --git a/ui/widgets/vcard/vcard.h b/ui/widgets/vcard/vcard.h new file mode 100644 index 0000000..8346fc3 --- /dev/null +++ b/ui/widgets/vcard/vcard.h @@ -0,0 +1,106 @@ +/* + * 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 VCARD_H +#define VCARD_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "global.h" +#include "emailsmodel.h" +#include "phonesmodel.h" +#include "ui/utils/progress.h" +#include "ui/utils/comboboxdelegate.h" + +namespace Ui +{ +class VCard; +} + +/** + * @todo write docs + */ +class VCard : public QWidget +{ + Q_OBJECT +public: + VCard(const QString& jid, bool edit = false, QWidget* parent = nullptr); + ~VCard(); + + void setVCard(const Shared::VCard& card); + void setVCard(const QString& jid, const Shared::VCard& card); + QString getJid() const; + void showProgress(const QString& = ""); + void hideProgress(); + +signals: + void saveVCard(const Shared::VCard& card); + +private slots: + void onButtonBoxAccepted(); + void onClearAvatar(); + void onSetAvatar(); + void onAvatarSelected(); + void onAddAddress(); + void onRemoveAddress(); + void onAddEmail(); + void onCopyEmail(); + void onRemoveEmail(); + void onAddPhone(); + void onCopyPhone(); + void onRemovePhone(); + void onContextMenu(const QPoint& point); + +private: + QScopedPointer m_ui; + QSize avatarButtonMargins; + QMenu* avatarMenu; + bool editable; + Shared::Avatar currentAvatarType; + QString currentAvatarPath; + Progress* progress; + QLabel* progressLabel; + QWidget* overlay; + QMenu* contextMenu; + UI::VCard::EMailsModel emails; + UI::VCard::PhonesModel phones; + ComboboxDelegate* roleDelegate; + ComboboxDelegate* phoneTypeDelegate; + + static const std::set supportedTypes; + +private: + void updateAvatar(); +}; + +#endif // VCARD_H diff --git a/ui/widgets/vcard/vcard.ui b/ui/widgets/vcard/vcard.ui new file mode 100644 index 0000000..26db8f9 --- /dev/null +++ b/ui/widgets/vcard/vcard.ui @@ -0,0 +1,892 @@ + + + VCard + + + true + + + + 0 + 0 + 578 + 671 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + font: 16pt + + + Contact john@dow.org card + + + Qt::AlignCenter + + + + + + + font: italic 8pt; + + + Received 12.07.2007 at 17.35 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + Qt::TabFocus + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + Qt::ElideNone + + + true + + + false + + + + General + + + + 0 + + + 6 + + + 0 + + + 0 + + + 6 + + + + + + 0 + 0 + + + + font: 600 16pt; + + + Organization + + + Qt::AlignCenter + + + + + + + QLayout::SetDefaultConstraint + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Middle name + + + middleName + + + + + + + First name + + + firstName + + + + + + + Last name + + + lastName + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Nick name + + + nickName + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Birthday + + + birthday + + + + + + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Organization name + + + organizationName + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Unit / Department + + + organizationDepartment + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Role / Profession + + + organizationRole + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + Job title + + + organizationTitle + + + + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + Full name + + + fullName + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + + + + font: 600 24pt ; + + + General + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + font: 600 16pt; + + + QFrame::NoFrame + + + QFrame::Plain + + + Personal information + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + .. + + + + 0 + 0 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonIconOnly + + + Qt::NoArrow + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Contact + + + + 0 + + + 0 + + + 6 + + + 0 + + + 0 + + + + + font: 600 24pt ; + + + Contact + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + true + + + + + 0 + 0 + 566 + 498 + + + + + + + Qt::Horizontal + + + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + font: 600 16pt; + + + Addresses + + + Qt::AlignCenter + + + + + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + font: 600 16pt; + + + E-Mail addresses + + + Qt::AlignCenter + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + 150 + 0 + + + + + 300 + 16777215 + + + + + + + + Jabber ID + + + jabberID + + + + + + + + 150 + 0 + + + + + 300 + 16777215 + + + + + + + + Web site + + + url + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + font: 600 16pt; + + + Phone numbers + + + Qt::AlignCenter + + + + + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + + + + + + Description + + + + 0 + + + 6 + + + 0 + + + 0 + + + 6 + + + + + font: 600 24pt ; + + + Description + + + + + + + QFrame::StyledPanel + + + Qt::LinksAccessibleByMouse|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + QDialogButtonBox::Close|QDialogButtonBox::Save + + + false + + + + + + + + + + .. + + + Set avatar + + + + + + .. + + + Clear avatar + + + + + fullName + firstName + middleName + lastName + nickName + birthday + avatarButton + organizationName + organizationDepartment + organizationRole + organizationTitle + jabberID + url + description + + + + + +