diff --git a/CMakeLists.txt b/CMakeLists.txt index 934dfae..f39d0e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,19 @@ cmake_minimum_required(VERSION 3.0) project(squawk) -# Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) -# 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) +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) + set(squawk_SRC main.cpp global.cpp @@ -18,16 +21,46 @@ set(squawk_SRC signalcatcher.cpp ) -add_executable(squawk ${squawk_SRC} resources/resources.qrc) +configure_file(resources/images/logo.svg squawk.svg COPYONLY) +execute_process(COMMAND convert -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +configure_file(packaging/squawk.desktop squawk.desktop COPYONLY) + +set(TS_FILES + translations/squawk.ru.ts +) +qt5_add_translation(QM_FILES ${TS_FILES}) +add_custom_target(translations ALL DEPENDS ${QM_FILES}) + +qt5_add_resources(RCC resources/resources.qrc) + +add_executable(squawk ${squawk_SRC} ${RCC}) target_link_libraries(squawk Qt5::Widgets) +option(SYSTEM_QXMPP "Use system qxmpp lib" ON) + +if(NOT SYSTEM_QXMPP) + add_subdirectory(external/qxmpp) +endif() + add_subdirectory(ui) add_subdirectory(core) -add_subdirectory(external/qxmpp) target_link_libraries(squawk squawkUI) target_link_libraries(squawk squawkCORE) target_link_libraries(squawk uuid) +add_dependencies(${CMAKE_PROJECT_NAME} translations) + # Install the executable -install(TARGETS squawk DESTINATION bin) +install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) +install(FILES squawk.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) +install(FILES squawk48.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps RENAME squawk.png) +install(FILES squawk64.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps RENAME squawk.png) +install(FILES squawk128.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps RENAME squawk.png) +install(FILES squawk256.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps RENAME squawk.png) +install(FILES squawk.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 8be08e3..fab36f2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,12 +1,12 @@ cmake_minimum_required(VERSION 3.0) project(squawkCORE) -# Instruct CMake to run moc automatically when needed. -set (CMAKE_CXX_STANDARD 17) 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 @@ -17,16 +17,23 @@ set(squawkCORE_SRC conference.cpp storage.cpp networkaccess.cpp + adapterFuctions.cpp ) # Tell CMake to create the helloworld executable add_library(squawkCORE ${squawkCORE_SRC}) +if(SYSTEM_QXMPP) + find_package(QXmpp CONFIG REQUIRED) + get_target_property(QXMPP_INTERFACE_INCLUDE_DIRECTORIES QXmpp::QXmpp INTERFACE_INCLUDE_DIRECTORIES) + target_include_directories(squawkCORE PUBLIC ${QXMPP_INTERFACE_INCLUDE_DIRECTORIES}) +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) - -# Install the executable -install(TARGETS squawkCORE DESTINATION lib) diff --git a/core/account.cpp b/core/account.cpp index 68c547f..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"; @@ -1153,3 +1257,297 @@ void Core::Account::addNewRoom(const QString& jid, const QString& nick, const QS {"name", conf->getName()} }); } + +void Core::Account::addContactToGroupRequest(const QString& jid, const QString& groupName) +{ + std::map::const_iterator itr = contacts.find(jid); + if (itr == contacts.end()) { + qDebug() << "An attempt to add non existing contact" << jid << "of account" << name << "to the group" << groupName << ", skipping"; + } else { + 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); + item.setGroups(groups); + + QXmppRosterIq iq; + iq.setType(QXmppIq::Set); + iq.addItem(item); + client.sendPacket(iq); + } else { + qDebug() << "An attempt to add contact" << jid << "of account" << name << "to the group" << groupName << "but it's already in that group, skipping"; + } + } +} + +void Core::Account::removeContactFromGroupRequest(const QString& jid, const QString& groupName) +{ + std::map::const_iterator itr = contacts.find(jid); + if (itr == contacts.end()) { + qDebug() << "An attempt to remove non existing contact" << jid << "of account" << name << "from the group" << groupName << ", skipping"; + } else { + QXmppRosterIq::Item item = rm->getRosterEntry(jid); + QSet groups = item.groups(); + QSet::const_iterator gItr = groups.find(groupName); + if (gItr != groups.end()) { + groups.erase(gItr); + item.setGroups(groups); + + QXmppRosterIq iq; + iq.setType(QXmppIq::Set); + iq.addItem(item); + client.sendPacket(iq); + } else { + qDebug() << "An attempt to remove contact" << jid << "of account" << name << "from the group" << groupName << "but it's not in that group, skipping"; + } + } +} + +void Core::Account::renameContactRequest(const QString& jid, const QString& newName) +{ + std::map::const_iterator itr = contacts.find(jid); + if (itr == contacts.end()) { + qDebug() << "An attempt to rename non existing contact" << jid << "of account" << name << ", skipping"; + } else { + 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 f3dd4c6..262a334 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 @@ -31,6 +37,8 @@ #include #include #include +#include +#include #include "../global.h" #include "contact.h" #include "conference.h" @@ -55,6 +63,7 @@ public: QString getServer() const; QString getPassword() const; QString getResource() const; + QString getAvatarPath() const; Shared::Availability getAvailability() const; void setName(const QString& p_name); @@ -71,13 +80,19 @@ public: void unsubscribeFromContact(const QString& jid, const QString& reason); void removeContactRequest(const QString& jid); void addContactRequest(const QString& jid, const QString& name, const QSet& groups); + void addContactToGroupRequest(const QString& jid, const QString& groupName); + void removeContactFromGroupRequest(const QString& jid, const QString& groupName); + void renameContactRequest(const QString& jid, const QString& newName); void setRoomJoined(const QString& jid, bool joined); 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); @@ -97,6 +112,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; @@ -110,6 +126,8 @@ private: QXmppMamManager* am; QXmppMucManager* mm; QXmppBookmarkManager* bm; + QXmppRosterManager* rm; + QXmppVCardManager* vm; std::map contacts; std::map conferences; unsigned int maxReconnectTimes; @@ -117,6 +135,11 @@ private: std::map queuedContacts; std::set outOfRosterContacts; + std::set pendingVCardRequests; + + QString avatarHash; + QString avatarType; + bool ownVCardRequestInProgress; private slots: void onClientConnected(); @@ -155,8 +178,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); @@ -176,6 +203,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 9615914..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() { } @@ -57,7 +61,7 @@ void Core::Archive::open(const QString& account) } mdb_env_set_maxdbs(environment, 4); - mdb_env_set_mapsize(environment, 1UL * 1024UL * 1024UL * 1024UL); + mdb_env_set_mapsize(environment, 512UL * 1024UL * 1024UL); mdb_env_open(environment, path.toStdString().c_str(), 0, 0664); MDB_txn *txn; @@ -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 4863535..095cf41 100644 --- a/core/networkaccess.cpp +++ b/core/networkaccess.cpp @@ -331,9 +331,9 @@ void Core::NetworkAccess::startDownload(const QString& messageId, const QString& Transfer* dwn = new Transfer({{messageId}, 0, 0, true, "", 0}); 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(onDownloadError(QNetworkReply::NetworkError))); - connect(dwn->reply, SIGNAL(finished()), SLOT(onDownloadFinished())); + connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress); + connect(dwn->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onDownloadError); + connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished); 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 50275a5..1679eab 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -28,11 +28,11 @@ 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, SIGNAL(uploadFileProgress(const QString&, qreal)), this, SIGNAL(uploadFileProgress(const QString&, qreal))); - connect(&network, SIGNAL(uploadFileError(const QString&, const QString&)), this, SIGNAL(uploadFileError(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); + connect(&network, &NetworkAccess::uploadFileProgress, this, &Squawk::uploadFileProgress); + connect(&network, &NetworkAccess::uploadFileError, this, &Squawk::uploadFileError); } Core::Squawk::~Squawk() @@ -103,42 +103,39 @@ void Core::Squawk::newAccountRequest(const QMap& map) void Core::Squawk::addAccount(const QString& login, const QString& server, const QString& password, const QString& name, const QString& resource) { + QSettings settings; + unsigned int reconnects = settings.value("reconnects", 2).toUInt(); + Account* acc = new Account(login, server, password, name); acc->setResource(resource); + acc->setReconnectTimes(reconnects); 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}, @@ -148,8 +145,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); } @@ -261,6 +260,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()); @@ -504,3 +509,53 @@ void Core::Squawk::downloadFileRequest(const QString& messageId, const QString& { network.downladFileRequest(messageId, url); } + +void Core::Squawk::addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName) +{ + AccountsMap::const_iterator itr = amap.find(account); + if (itr == amap.end()) { + qDebug() << "An attempt to add contact" << jid << "of non existing account" << account << "to the group" << groupName << ", skipping"; + return; + } + itr->second->addContactToGroupRequest(jid, groupName); +} + +void Core::Squawk::removeContactFromGroupRequest(const QString& account, const QString& jid, const QString& groupName) +{ + AccountsMap::const_iterator itr = amap.find(account); + if (itr == amap.end()) { + qDebug() << "An attempt to add contact" << jid << "of non existing account" << account << "to the group" << groupName << ", skipping"; + return; + } + itr->second->removeContactFromGroupRequest(jid, groupName); +} + +void Core::Squawk::renameContactRequest(const QString& account, const QString& jid, const QString& newName) +{ + AccountsMap::const_iterator itr = amap.find(account); + if (itr == amap.end()) { + 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 7403ba8..e2a6b74 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -23,7 +23,8 @@ #include #include #include -#include +#include + #include #include "account.h" @@ -67,6 +68,7 @@ signals: void downloadFileProgress(const QString& messageId, qreal value); void uploadFileError(const QString& messageId, const QString& error); void uploadFileProgress(const QString& messageId, qreal value); + void responseVCard(const QString& jid, const Shared::VCard& card); public slots: void start(); @@ -82,7 +84,10 @@ public slots: void requestArchive(const QString& account, const QString& jid, int count, const QString& before); void subscribeContact(const QString& account, const QString& jid, const QString& reason); void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); + void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); + void removeContactFromGroupRequest(const QString& account, const QString& jid, const QString& groupName); void removeContactRequest(const QString& account, const QString& jid); + void renameContactRequest(const QString& account, const QString& jid, const QString& newName); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet& groups); void setRoomJoined(const QString& account, const QString& jid, bool joined); void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); @@ -90,6 +95,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 +113,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/external/qxmpp b/external/qxmpp index e6eb0b7..b18a57d 160000 --- a/external/qxmpp +++ b/external/qxmpp @@ -1 +1 @@ -Subproject commit e6eb0b78d0cb17fccd5ddb60966ba2a0a2d2b593 +Subproject commit b18a57daa33f0fefa5f4c63aa7f448b48d302e0d diff --git a/global.cpp b/global.cpp index 1561983..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 ? @@ -350,6 +625,6 @@ QIcon Shared::icon(const QString& name, bool big) return QIcon::fromTheme(itr->second.first, QIcon(prefix + itr->second.second)); } else { qDebug() << "Icon" << name << "not found"; - throw 1; + return QIcon::fromTheme(name); } } diff --git a/global.h b/global.h index 5a8c902..d5d206e 100644 --- a/global.h +++ b/global.h @@ -20,9 +20,11 @@ #define GLOBAL_H #include +#include #include #include #include +#include namespace Shared { @@ -68,6 +70,12 @@ enum class Role { moderator }; +enum class Avatar { + empty, + autocreated, + valid +}; + static const Availability availabilityHighest = offline; static const Availability availabilityLowest = online; @@ -99,9 +107,47 @@ static const std::deque subscriptionStateNames = {"None", "From", "To", static const std::deque affiliationNames = {"Unspecified", "Outcast", "Nobody", "Member", "Admin", "Owner"}; 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 085a01f..a66230f 100644 --- a/main.cpp +++ b/main.cpp @@ -23,20 +23,42 @@ #include #include #include +#include +#include +#include int main(int argc, char *argv[]) { qRegisterMetaType("Shared::Message"); + qRegisterMetaType("Shared::VCard"); qRegisterMetaType>("std::list"); qRegisterMetaType>("QSet"); QApplication app(argc, argv); SignalCatcher sc(&app); - QCoreApplication::setOrganizationName("Macaw"); - QCoreApplication::setOrganizationDomain("macaw.me"); - QCoreApplication::setApplicationName("Squawk"); - QCoreApplication::setApplicationVersion("0.0.3"); + QApplication::setApplicationName("squawk"); + QApplication::setApplicationDisplayName("Squawk"); + QApplication::setApplicationVersion("0.0.5"); + + QTranslator qtTranslator; + qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + app.installTranslator(&qtTranslator); + + QTranslator myappTranslator; + QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + bool found = false; + for (QString share : shares) { + found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n"); + if (found) { + break; + } + } + if (!found) { + myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", QCoreApplication::applicationDirPath()); + } + + app.installTranslator(&myappTranslator); QIcon icon; icon.addFile(":images/logo.svg", QSize(16, 16)); @@ -57,75 +79,59 @@ 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(&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, qOverload(&Squawk::sendMessage), + squawk, qOverload(&Core::Squawk::sendMessage)); + QObject::connect(&w, qOverload(&Squawk::sendMessage), + squawk, qOverload(&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(&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(sendMessage(const QString&, const Shared::Message&, const QString&)), - squawk, SLOT(sendMessage(const QString&, const Shared::Message&, const QString&))); - 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(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); coreThread->start(); diff --git a/packaging/Archlinux/PKGBUILD b/packaging/Archlinux/PKGBUILD new file mode 100644 index 0000000..bd979b5 --- /dev/null +++ b/packaging/Archlinux/PKGBUILD @@ -0,0 +1,21 @@ +# Maintainer: Yury Gubich +pkgname=squawk +pkgver=0.0.5 +pkgrel=1 +pkgdesc="An XMPP desktop messenger, written on qt" +arch=('i686' 'x86_64') +url="https://git.macaw.me/blue/squawk" +license=('GPL3') +depends=('hicolor-icon-theme' 'desktop-file-utils' 'lmdb' 'qxmpp>=1.0.0') +makedepends=('cmake>=3.3' 'imagemagick' 'qt5-tools') +source=("$pkgname-$pkgver.tar.gz") +sha256sums=('12bfc517574387257a82143d8970ec0d8d434ccd32f7ac400355ed5fa18192ab') +build() { + cd "$srcdir/squawk" + cmake . -D CMAKE_INSTALL_PREFIX=/usr + cmake --build . -j $nproc +} +package() { + cd "$srcdir/squawk" + DESTDIR="$pkgdir/" cmake --build . --target install +} diff --git a/packaging/squawk.desktop b/packaging/squawk.desktop new file mode 100644 index 0000000..0395af1 --- /dev/null +++ b/packaging/squawk.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] + +Type=Application +Version=1.0 +Name=Squawk +GenericName=Instant Messenger +GenericName[ru]=Мгновенные сообщения +Comment=XMPP (Jabber) instant messenger client +Comment[ru]=XMPP (Jabber) клиент обмена мгновенными сообщениями +Exec=squawk %u +Icon=squawk +StartupNotify=true +StartupWMClass=Squawk +Terminal=false +Categories=Network;InstantMessaging;Qt; +MimeType=x-scheme-handler/xmpp; 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 new file mode 100644 index 0000000..e7bae69 --- /dev/null +++ b/translations/squawk.ru.ts @@ -0,0 +1,858 @@ + + + + + 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 + + + + Accounts + + + Accounts + Учетные записи + + + + Delete + Удалить + + + + Add + Добавить + + + + Edit + Редактировать + + + + Change password + Изменить пароль + + + + + + Connect + Подключить + + + + Disconnect + Отключить + + + + Conversation + + + Type your message here... + Введите сообщение... + + + + Chose a file to send + Выберите файл для отправки + + + + Global + + Disconnected + Отключен + + + Connecting + Подключается + + + Connected + Подключен + + + Error + Ошибка + + + Online + В сети + + + Away + Отошел + + + Busy + Занят + + + Absent + Недоступен + + + Chatty + Готов поболтать + + + Invisible + Невидимый + + + Offline + Отключен + + + None + Нет + + + From + Входящая + + + To + Исходящая + + + Both + Взаимная + + + Unknown + Неизвестно + + + Unspecified + Не назначено + + + Outcast + Изгой + + + Nobody + Никто + + + Member + Участник + + + Admin + Администратор + + + Owner + Владелец + + + Visitor + Гость + + + Participant + Участник + + + 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 + + + + Message + + + Download + Скачать + + + + Error downloading file: %1 +You can try again + Ошибка загрузки файла: %1 +Вы можете попробовать снова + + + + %1 is offering you to download a file + %1 предлагает Вам скачать файл + + + + Open + Открыть + + + + Models::Accounts + + Name + Имя + + + Server + Сервер + + + State + Состояние + + + Error + Ошибка + + + + Models::Room + + + Subscribed + Вы состоите в беседе + + + + Temporarily unsubscribed + Вы временно не состоите в беседе + + + + Temporarily subscribed + Вы временно состоите в беседе + + + + Unsubscribed + Вы не состоите в беседе + + + + Models::Roster + + + New messages + Есть непрочитанные сообщения + + + + + + + New messages: + Новых сообщений: + + + + Jabber ID: + Идентификатор: + + + + + + Availability: + Доступность: + + + + + + Status: + Статус: + + + + + + Subscription: + Подписка: + + + + Affiliation: + Я правда не знаю, как это объяснить, не то что перевести + Причастность: + + + + Role: + Роль: + + + + Online contacts: + Контакстов в сети: + + + + Total contacts: + Всего контактов: + + + + Members: + Участников: + + + + 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 + Иван Иванов + + + + 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 + Введите имя для %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 3ca0148..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,27 +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) - -# Install the executable -install(TARGETS squawkUI DESTINATION lib) diff --git a/ui/models/abstractparticipant.cpp b/ui/models/abstractparticipant.cpp index 7fef30a..53d8b2d 100644 --- a/ui/models/abstractparticipant.cpp +++ b/ui/models/abstractparticipant.cpp @@ -32,6 +32,15 @@ Models::AbstractParticipant::AbstractParticipant(Models::Item::Type p_type, cons } } +Models::AbstractParticipant::AbstractParticipant(const Models::AbstractParticipant& other): + Item(other), + availability(other.availability), + lastActivity(other.lastActivity), + status(other.status) +{ +} + + Models::AbstractParticipant::~AbstractParticipant() { } diff --git a/ui/models/abstractparticipant.h b/ui/models/abstractparticipant.h index 44cbcb9..86d5629 100644 --- a/ui/models/abstractparticipant.h +++ b/ui/models/abstractparticipant.h @@ -31,6 +31,7 @@ class AbstractParticipant : public Models::Item Q_OBJECT public: explicit AbstractParticipant(Type p_type, const QMap &data, Item *parentItem = 0); + AbstractParticipant(const AbstractParticipant& other); ~AbstractParticipant(); virtual int columnCount() const override; diff --git a/ui/models/account.cpp b/ui/models/account.cpp index 56a7806..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) { @@ -151,7 +152,7 @@ QVariant Models::Account::data(int column) const case 1: return server; case 2: - return Shared::connectionStateNames[state]; + return QCoreApplication::translate("Global", Shared::connectionStateNames[state].toLatin1()); case 3: return error; case 4: @@ -159,9 +160,11 @@ QVariant Models::Account::data(int column) const case 5: return password; case 6: - return Shared::availabilityNames[availability]; + 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 6a62376..79ed159 100644 --- a/ui/models/accounts.cpp +++ b/ui/models/accounts.cpp @@ -20,13 +20,9 @@ #include "../../global.h" #include +#include -std::deque Models::Accounts::columns = { - "name", - "server", - "state", - "error" -}; +std::deque Models::Accounts::columns = {"Name", "Server", "State", "Error"}; Models::Accounts::Accounts(QObject* parent): QAbstractTableModel(parent), @@ -72,7 +68,7 @@ int Models::Accounts::rowCount ( const QModelIndex& parent ) const QVariant Models::Accounts::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { - return columns[section]; + return tr(columns[section].toLatin1()); } return QVariant(); } @@ -81,8 +77,19 @@ QVariant Models::Accounts::headerData(int section, Qt::Orientation orientation, void Models::Accounts::addAccount(Account* account) { beginInsertRows(QModelIndex(), accs.size(), accs.size()); - accs.push_back(account); - connect(account, SIGNAL(childChanged(Models::Item*, int, int)), this, SLOT(onAccountChanged(Models::Item*, int, int))); + int index = 0; + std::deque::const_iterator before = accs.begin(); + while (before != accs.end()) { + Account* bfr = *before; + if (bfr->getDisplayedName() > account->getDisplayedName()) { + break; + } + index++; + before++; + } + + accs.insert(before, account); + connect(account, &Account::childChanged, this, &Accounts::onAccountChanged); endInsertRows(); emit sizeChanged(accs.size()); @@ -96,8 +103,32 @@ void Models::Accounts::onAccountChanged(Item* item, int row, int col) return; //it means the signal is emitted by one of accounts' children, not exactly him, this model has no interest in that } + if (col == 0) { + int newRow = 0; + std::deque::const_iterator before = accs.begin(); + while (before != accs.end()) { + Item* bfr = *before; + if (bfr->getDisplayedName() > item->getDisplayedName()) { + break; + } + newRow++; + before++; + } + + if (newRow != row || (before != accs.end() && *before != item)) { + emit beginMoveRows(createIndex(row, 0), row, row, createIndex(newRow, 0), newRow); + std::deque::const_iterator old = accs.begin(); + old += row; + accs.erase(old); + accs.insert(before, acc); + emit endMoveRows(); + + row = newRow; + } + } + if (col < columnCount(QModelIndex())) { - emit dataChanged(createIndex(row, col, this), createIndex(row, col, this)); + emit dataChanged(createIndex(row, col), createIndex(row, col)); } emit changed(); } @@ -112,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 4c7b440..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(); } @@ -313,3 +333,74 @@ void Models::Contact::toOfflineState() emit childRemoved(); refresh(); } + +QString Models::Contact::getDisplayedName() const +{ + return getContactName(); +} + +bool Models::Contact::columnInvolvedInDisplay(int col) +{ + return Item::columnInvolvedInDisplay(col) && col == 1; +} + +Models::Contact * Models::Contact::copy() const +{ + Contact* cnt = new Contact(*this); + return cnt; +} + +Models::Contact::Contact(const Models::Contact& other): + Item(other), + jid(other.jid), + availability(other.availability), + state(other.state), + presences(), + messages(other.messages), + childMessages(0) +{ + for (const Presence* pres : other.presences) { + Presence* pCopy = new Presence(*pres); + presences.insert(pCopy->getName(), pCopy); + Item::appendChild(pCopy); + 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 d390aec..fda893f 100644 --- a/ui/models/contact.h +++ b/ui/models/contact.h @@ -34,11 +34,14 @@ class Contact : public Item public: typedef std::deque Messages; Contact(const QString& p_jid, const QMap &data, Item *parentItem = 0); + Contact(const Contact& other); ~Contact(); 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; @@ -57,9 +60,13 @@ public: unsigned int getMessagesCount() const; void dropMessages(); void getMessages(Messages& container) const; + QString getDisplayedName() const override; + + Contact* copy() const; protected: void _removeChild(int index) override; + bool columnInvolvedInDisplay(int col) override; protected slots: void refresh(); @@ -70,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); @@ -77,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 1880714..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(); @@ -103,3 +103,16 @@ unsigned int Models::Group::getOnlineContacts() const return amount; } + +bool Models::Group::hasContact(const QString& jid) const +{ + for (Models::Item* item : childItems) { + if (item->type == Item::contact) { + const Contact* cnt = static_cast(item); + if (cnt->getJid() == jid) { + return true; + } + } + } + return false; +} diff --git a/ui/models/group.h b/ui/models/group.h index 8301dc8..c2f4bfe 100644 --- a/ui/models/group.h +++ b/ui/models/group.h @@ -36,6 +36,8 @@ public: unsigned int getUnreadMessages() const; unsigned int getOnlineContacts() const; + + bool hasContact(const QString& jid) const; protected: void _removeChild(int index) override; diff --git a/ui/models/item.cpp b/ui/models/item.cpp index 59457c3..f90f5e6 100644 --- a/ui/models/item.cpp +++ b/ui/models/item.cpp @@ -34,6 +34,15 @@ Models::Item::Item(Type p_type, const QMap &p_data, Item *p_p } } +Models::Item::Item(const Models::Item& other): + QObject(), + type(other.type), + name(other.name), + childItems(), + parent(nullptr) +{ +} + Models::Item::~Item() { std::deque::const_iterator itr = childItems.begin(); @@ -55,25 +64,37 @@ void Models::Item::setName(const QString& p_name) void Models::Item::appendChild(Models::Item* child) { bool moving = false; - int oldRow = child->row(); - int newRow = this->childCount(); + int newRow = 0; + std::deque::const_iterator before = childItems.begin(); + while (before != childItems.end()) { + Item* bfr = *before; + if (bfr->type > child->type) { + break; + } else if (bfr->type == child->type && bfr->getDisplayedName() > child->getDisplayedName()) { + break; + } + newRow++; + before++; + } + if (child->parent != 0) { + int oldRow = child->row(); moving = true; emit childIsAboutToBeMoved(child->parent, oldRow, oldRow, this, newRow); child->parent->_removeChild(oldRow); } else { emit childIsAboutToBeInserted(this, newRow, newRow); } - childItems.push_back(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(); @@ -120,7 +141,7 @@ const Models::Item * Models::Item::parentItemConst() const int Models::Item::columnCount() const { - return 1; + return 2; } QString Models::Item::getName() const @@ -147,13 +168,13 @@ void Models::Item::_removeChild(int index) { Item* child = childItems[index]; - QObject::disconnect(child, SIGNAL(childChanged(Models::Item*, int, int)), this, SIGNAL(childChanged(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; @@ -212,3 +233,62 @@ QString Models::Item::getAccountName() const } return acc->getName(); } + +Shared::Availability Models::Item::getAccountAvailability() const +{ + const Account* acc = static_cast(getParentAccount()); + if (acc == 0) { + return Shared::offline; + } + return acc->getAvailability(); +} + +Shared::ConnectionState Models::Item::getAccountConnectionState() const +{ + const Account* acc = static_cast(getParentAccount()); + if (acc == 0) { + return Shared::disconnected; + } + return acc->getState(); +} + +QString Models::Item::getDisplayedName() const +{ + return name; +} + +void Models::Item::onChildChanged(Models::Item* item, int row, int col) +{ + Item* parent = item->parentItem(); + if (parent != 0 && parent == this) { + if (item->columnInvolvedInDisplay(col)) { + int newRow = 0; + std::deque::const_iterator before = childItems.begin(); + while (before != childItems.end()) { + Item* bfr = *before; + if (bfr->type > item->type) { + break; + } else if (bfr->type == item->type && bfr->getDisplayedName() > item->getDisplayedName()) { + break; + } + newRow++; + before++; + } + + if (newRow != row || (before != childItems.end() && *before != item)) { + emit childIsAboutToBeMoved(this, row, row, this, newRow); + std::deque::const_iterator old = childItems.begin(); + old += row; + childItems.erase(old); + childItems.insert(before, item); + emit childMoved(); + } + } + } + emit childChanged(item, row, col); +} + +bool Models::Item::columnInvolvedInDisplay(int col) +{ + return col == 0; +} diff --git a/ui/models/item.h b/ui/models/item.h index 4728e54..20f6f89 100644 --- a/ui/models/item.h +++ b/ui/models/item.h @@ -25,6 +25,8 @@ #include +#include "../../global.h" + namespace Models { class Item : public QObject{ @@ -41,6 +43,7 @@ class Item : public QObject{ }; explicit Item(Type p_type, const QMap &data, Item *parentItem = 0); + Item(const Item& other); ~Item(); signals: @@ -55,6 +58,7 @@ class Item : public QObject{ public: virtual void appendChild(Item *child); virtual void removeChild(int index); + virtual QString getDisplayedName() const; QString getName() const; void setName(const QString& name); @@ -70,14 +74,20 @@ class Item : public QObject{ QString getAccountName() const; QString getAccountJid() const; QString getAccountResource() const; + Shared::ConnectionState getAccountConnectionState() const; + Shared::Availability getAccountAvailability() const; const Type type; protected: virtual void changed(int col); virtual void _removeChild(int index); + virtual bool columnInvolvedInDisplay(int col); const Item* getParentAccount() const; + protected slots: + void onChildChanged(Models::Item* item, int row, int col); + protected: QString name; std::deque childItems; diff --git a/ui/models/presence.cpp b/ui/models/presence.cpp index b6be0fe..36b07d2 100644 --- a/ui/models/presence.cpp +++ b/ui/models/presence.cpp @@ -24,6 +24,13 @@ Models::Presence::Presence(const QMap& data, Item* parentItem { } +Models::Presence::Presence(const Models::Presence& other): + AbstractParticipant(other), + messages(other.messages) +{ +} + + Models::Presence::~Presence() { } diff --git a/ui/models/presence.h b/ui/models/presence.h index c396f0c..8371be7 100644 --- a/ui/models/presence.h +++ b/ui/models/presence.h @@ -32,6 +32,7 @@ class Presence : public Models::AbstractParticipant public: typedef std::deque Messages; explicit Presence(const QMap &data, Item *parentItem = 0); + Presence(const Presence& other); ~Presence(); int columnCount() const override; diff --git a/ui/models/room.cpp b/ui/models/room.cpp index 972ed77..204b2b5 100644 --- a/ui/models/room.cpp +++ b/ui/models/room.cpp @@ -193,15 +193,15 @@ QString Models::Room::getStatusText() const { if (autoJoin) { if (joined) { - return "Subscribed"; + return tr("Subscribed"); } else { - return "Temporarily unsubscribed"; + return tr("Temporarily unsubscribed"); } } else { if (joined) { - return "Temporarily subscribed"; + return tr("Temporarily subscribed"); } else { - return "Unsubscribed"; + return tr("Unsubscribed"); } } } @@ -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(); } @@ -309,3 +308,13 @@ void Models::Room::setSubject(const QString& sub) changed(6); } } + +QString Models::Room::getDisplayedName() const +{ + return getRoomName(); +} + +bool Models::Room::columnInvolvedInDisplay(int col) +{ + return Item::columnInvolvedInDisplay(col) && col == 1; +} diff --git a/ui/models/room.h b/ui/models/room.h index cc87fa9..71ac221 100644 --- a/ui/models/room.h +++ b/ui/models/room.h @@ -21,7 +21,7 @@ #include "item.h" #include "participant.h" -#include "../global.h" +#include "../../global.h" namespace Models { @@ -68,10 +68,14 @@ public: void removeParticipant(const QString& name); void toOfflineState() override; + QString getDisplayedName() const override; private: void handleParticipantUpdate(std::map::const_iterator itr, const QMap& data); +protected: + bool columnInvolvedInDisplay(int col) override; + private: bool autoJoin; bool joined; diff --git a/ui/models/roster.cpp b/ui/models/roster.cpp index 70b03d5..e124db7 100644 --- a/ui/models/roster.cpp +++ b/ui/models/roster.cpp @@ -21,8 +21,6 @@ #include #include -using namespace Models; - Models::Roster::Roster(QObject* parent): QAbstractItemModel(parent), accountsModel(new Accounts()), @@ -31,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() @@ -70,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); @@ -78,7 +77,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const str += gr->getName(); unsigned int amount = gr->getUnreadMessages(); if (amount > 0) { - str += QString(" (") + "New messages" + ")"; + str += QString(" (") + tr("New messages") + ")"; } result = str; @@ -93,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); } @@ -143,7 +167,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const switch (item->type) { case Item::account: { Account* acc = static_cast(item); - result = QString(Shared::availabilityNames[acc->getAvailability()]); + result = QCoreApplication::translate("Global", Shared::availabilityNames[acc->getAvailability()].toLatin1()); } break; case Item::contact: { @@ -151,22 +175,22 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const QString str(""); int mc = contact->getMessagesCount(); if (mc > 0) { - str += QString("New messages: ") + std::to_string(mc).c_str() + "\n"; + str += QString(tr("New messages: ")) + std::to_string(mc).c_str() + "\n"; } - str += "Jabber ID: " + contact->getJid() + "\n"; + str += tr("Jabber ID: ") + contact->getJid() + "\n"; Shared::SubscriptionState ss = contact->getState(); if (ss == Shared::both) { Shared::Availability av = contact->getAvailability(); - str += "Availability: " + Shared::availabilityNames[av]; + str += tr("Availability: ") + QCoreApplication::translate("Global", Shared::availabilityNames[av].toLatin1()); if (av != Shared::offline) { QString s = contact->getStatus(); if (s.size() > 0) { - str += "\nStatus: " + s; + str += "\n" + tr("Status: ") + s; } } - str += "\nSubscription: " + Shared::subscriptionStateNames[ss]; + str += "\n" + tr("Subscription: ") + QCoreApplication::translate("Global", Shared::subscriptionStateNames[ss].toLatin1()); } else { - str += "Subscription: " + Shared::subscriptionStateNames[ss]; + str += tr("Subscription: ") + QCoreApplication::translate("Global", Shared::subscriptionStateNames[ss].toLatin1()); } result = str; @@ -177,13 +201,13 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const QString str(""); int mc = contact->getMessagesCount(); if (mc > 0) { - str += QString("New messages: ") + std::to_string(mc).c_str() + "\n"; + str += tr("New messages: ") + std::to_string(mc).c_str() + "\n"; } Shared::Availability av = contact->getAvailability(); - str += "Availability: " + Shared::availabilityNames[av]; + str += tr("Availability: ") + QCoreApplication::translate("Global", Shared::availabilityNames[av].toLatin1()); QString s = contact->getStatus(); if (s.size() > 0) { - str += "\nStatus: " + s; + str += "\n" + tr("Status: ") + s; } result = str; @@ -193,14 +217,18 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const Participant* p = static_cast(item); QString str(""); Shared::Availability av = p->getAvailability(); - str += "Availability: " + Shared::availabilityNames[av] + "\n"; + str += tr("Availability: ") + QCoreApplication::translate("Global", Shared::availabilityNames[av].toLatin1()) + "\n"; QString s = p->getStatus(); if (s.size() > 0) { - str += "Status: " + s + "\n"; + str += tr("Status: ") + s + "\n"; } - str += "Affiliation: " + Shared::affiliationNames[static_cast(p->getAffiliation())] + "\n"; - str += "Role: " + Shared::roleNames[static_cast(p->getRole())]; + str += tr("Affiliation: ") + + QCoreApplication::translate("Global", + Shared::affiliationNames[static_cast(p->getAffiliation())].toLatin1()) + "\n"; + str += tr("Role: ") + + QCoreApplication::translate("Global", + Shared::roleNames[static_cast(p->getRole())].toLatin1()); result = str; } @@ -210,10 +238,10 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const unsigned int count = gr->getUnreadMessages(); QString str(""); if (count > 0) { - str += QString("New messages: ") + std::to_string(count).c_str() + "\n"; + str += tr("New messages: ") + std::to_string(count).c_str() + "\n"; } - str += QString("Online contacts: ") + std::to_string(gr->getOnlineContacts()).c_str() + "\n"; - str += QString("Total contacts: ") + std::to_string(gr->childCount()).c_str(); + str += tr("Online contacts: ") + std::to_string(gr->getOnlineContacts()).c_str() + "\n"; + str += tr("Total contacts: ") + std::to_string(gr->childCount()).c_str(); result = str; } break; @@ -222,11 +250,11 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const unsigned int count = rm->getUnreadMessagesCount(); QString str(""); if (count > 0) { - str += QString("New messages: ") + std::to_string(count).c_str() + "\n"; + str += tr("New messages: ") + std::to_string(count).c_str() + "\n"; } - str += QString("Subscription: ") + rm->getStatusText(); + str += tr("Subscription: ") + rm->getStatusText(); if (rm->getJoined()) { - str += QString("\nMembers: ") + std::to_string(rm->childCount()).c_str(); + str += QString("\n") + tr("Members: ") + std::to_string(rm->childCount()).c_str(); } result = str; } @@ -383,50 +411,52 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons { Item* parent; Account* acc; - Contact* contact; + Contact* sample = 0; ElId id(account, jid); { std::map::iterator itr = accounts.find(account); if (itr == accounts.end()) { - qDebug() << "An attempt to add a contact " << jid << " to non existing account " << account << ", skipping"; + qDebug() << "An attempt to add a contact" << jid << "to non existing account" << account << ", skipping"; return; } acc = itr->second; } - if (group == "") { - std::multimap::iterator itr = contacts.lower_bound(id); - std::multimap::iterator eItr = contacts.upper_bound(id); - while (itr != eItr) { - if (itr->second->parentItem() == acc) { - qDebug() << "An attempt to add a contact " << jid << " ungrouped to non the account " << account << " for the second time, skipping"; - return; - } - itr++; + for (std::multimap::iterator itr = contacts.lower_bound(id), eItr = contacts.upper_bound(id); itr != eItr; ++itr) { + sample = itr->second; //need to find if this contact is already added somewhere + break; //so one iteration is enough + } + + if (group == "") { //this means this contact is already added somewhere and there is no sense to add it ungrouped + if (sample != 0) { + qDebug() << "An attempt to add a contact" << jid << "to the ungrouped contact set of account" << account << "for the second time, skipping"; + return; + } else { + parent = acc; } - parent = acc; } else { std::map::iterator itr = groups.find({account, group}); if (itr == groups.end()) { - qDebug() << "An attempt to add a contact " << jid << " to non existing group " << group << ", skipping"; - return; + qDebug() << "An attempt to add a contact" << jid << "to non existing group" << group << ", adding group"; + addGroup(account, group); + itr = groups.find({account, group}); } parent = itr->second; - for (int i = 0; i < parent->childCount(); ++i) { + for (int i = 0; i < parent->childCount(); ++i) { //checking if the contact is already added to that group Item* item = parent->child(i); if (item->type == Item::contact) { Contact* ca = static_cast(item); if (ca->getJid() == jid) { - qDebug() << "An attempt to add a contact " << jid << " to the group " << group << " for the second time, skipping"; + qDebug() << "An attempt to add a contact" << jid << "to the group" << group << "for the second time, skipping"; return; } } } - for (int i = 0; i < acc->childCount(); ++i) { + for (int i = 0; i < acc->childCount(); ++i) { //checking if that contact is among ugrouped Item* item = acc->child(i); if (item->type == Item::contact) { Contact* ca = static_cast(item); @@ -440,7 +470,12 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons } } - contact = new Contact(jid, data); + Contact* contact; + if (sample == 0) { + contact = new Contact(jid, data); + } else { + contact = sample->copy(); + } contacts.insert(std::make_pair(id, contact)); parent->appendChild(contact); } @@ -548,36 +583,67 @@ void Models::Roster::removeContact(const QString& account, const QString& jid, c qDebug() << "An attempt to remove contact " << jid << " from non existing group " << group << " of account " << account <<", skipping"; return; } + Account* acc = accounts.find(account)->second; //I assume the account is found, otherwise there will be no groups with that ElId; Group* gr = gItr->second; Contact* cont = 0; - std::multimap::iterator cBeg = contacts.lower_bound(contactId); - std::multimap::iterator cEnd = contacts.upper_bound(contactId); - for (;cBeg != cEnd; ++cBeg) { - if (cBeg->second->parentItem() == gr) { - cont = cBeg->second; - contacts.erase(cBeg); - break; + unsigned int entries(0); + unsigned int ungroupped(0); + for (std::multimap::iterator cBeg = contacts.lower_bound(contactId), cEnd = contacts.upper_bound(contactId); cBeg != cEnd; ++cBeg) { + ++entries; + Contact* elem = cBeg->second; + if (elem->parentItem() == acc) { + ++ungroupped; } } - if (cont == 0) { - qDebug() << "An attempt to remove contact " << jid << " of account " << account << " from group " << group <<", but there is no such contact in that group, skipping"; - return; - } - - gr->removeChild(cont->row()); - cont->deleteLater(); - - if (gr->childCount() == 0) { - removeGroup(account, group); + if (ungroupped == 0 && entries == 1) { + for (std::multimap::iterator cBeg = contacts.lower_bound(contactId), cEnd = contacts.upper_bound(contactId); cBeg != cEnd; ++cBeg) { + if (cBeg->second->parentItem() == gr) { + cont = cBeg->second; + break; + } + } + + if (cont == 0) { + qDebug() << "An attempt to remove contact " << jid << " of account " << account << " from group " << group <<", but there is no such contact in that group, skipping"; + return; + } + + qDebug() << "An attempt to remove last instance of contact" << jid << "from the group" << group << ", contact will be moved to ungrouped contacts of" << account; + acc->appendChild(cont); + + if (gr->childCount() == 0) { + removeGroup(account, group); + } + } else { + for (std::multimap::iterator cBeg = contacts.lower_bound(contactId), cEnd = contacts.upper_bound(contactId); cBeg != cEnd; ++cBeg) { + if (cBeg->second->parentItem() == gr) { + cont = cBeg->second; + contacts.erase(cBeg); + break; + } + } + + if (cont == 0) { + qDebug() << "An attempt to remove contact" << jid << "of account" << account << "from group" << group <<", but there is no such contact in that group, skipping"; + return; + } + + gr->removeChild(cont->row()); + cont->deleteLater(); + + if (gr->childCount() == 0) { + removeGroup(account, group); + } } } 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) @@ -738,7 +804,7 @@ QString Models::Roster::getContactName(const QString& account, const QString& ji if (rItr == rooms.end()) { qDebug() << "An attempt to get a name of non existing contact/room " << account << ":" << jid << ", skipping"; } else { - name = rItr->second->getName(); + name = rItr->second->getRoomName(); } } else { name = cItr->second->getContactName(); @@ -844,3 +910,47 @@ void Models::Roster::removeRoomParticipant(const QString& account, const QString itr->second->removeParticipant(name); } } + +std::deque Models::Roster::groupList(const QString& account) const +{ + std::deque answer; + for (std::pair pair : groups) { + if (pair.first.account == account) { + answer.push_back(pair.first.name); + } + } + + return answer; +} + +bool Models::Roster::groupHasContact(const QString& account, const QString& group, const QString& contact) const +{ + ElId grId({account, group}); + std::map::const_iterator gItr = groups.find(grId); + if (gItr == groups.end()) { + return false; + } else { + const Group* gr = gItr->second; + 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 314e92d..40c978d 100644 --- a/ui/models/roster.h +++ b/ui/models/roster.h @@ -71,6 +71,10 @@ public: QModelIndex parent ( const QModelIndex& child ) const override; QModelIndex index ( int row, int column, const QModelIndex& parent ) const override; + 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; private: diff --git a/ui/squawk.cpp b/ui/squawk.cpp index 599c909..3221a4c 100644 --- a/ui/squawk.cpp +++ b/ui/squawk.cpp @@ -20,6 +20,7 @@ #include "ui_squawk.h" #include #include +#include Squawk::Squawk(QWidget *parent) : QMainWindow(parent), @@ -28,27 +29,35 @@ 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); - m_ui->comboBox->addItem(Shared::availabilityIcon(av), Shared::availabilityNames[av]); + m_ui->comboBox->addItem(Shared::availabilityIcon(av), QCoreApplication::translate("Global", Shared::availabilityNames[av].toLatin1())); } 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() { @@ -60,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 { @@ -90,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(); } @@ -100,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(); } @@ -131,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); } @@ -283,13 +299,14 @@ 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(sendMessage(const Shared::Message&, const QString&)), this, SLOT(onConversationMessage(const Shared::Message&, const QString&))); - 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, qOverload(&Conversation::sendMessage), this, qOverload(&Squawk::onConversationMessage)); + connect(conv, qOverload(&Conversation::sendMessage), + this, qOverload(&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 +466,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 { @@ -502,10 +524,9 @@ 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::requestArchive, this, &Squawk::onConversationRequestArchive); + disconnect(conv, &Conversation::shown, this, &Squawk::onConversationShown); conv->close(); conversations.erase(lItr); } else { @@ -523,6 +544,7 @@ void Squawk::onRosterContextMenu(const QPoint& point) contextMenu->clear(); bool hasMenu = false; + bool active = item->getAccountConnectionState() == Shared::connected; switch (item->type) { case Models::Item::account: { Models::Account* acc = static_cast(item); @@ -530,18 +552,24 @@ void Squawk::onRosterContextMenu(const QPoint& point) QString name = acc->getName(); if (acc->getState() != Shared::disconnected) { - QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), "Disconnect"); + QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Disconnect")); + con->setEnabled(active); connect(con, &QAction::triggered, [this, name]() { emit disconnectAccount(name); }); } else { - QAction* con = contextMenu->addAction(Shared::icon("network-connect"), "Connect"); + QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Connect")); connect(con, &QAction::triggered, [this, name]() { emit connectAccount(name); }); } - QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), "Remove"); + 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]() { emit removeAccount(name); }); @@ -552,7 +580,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) Models::Contact* cnt = static_cast(item); hasMenu = true; - QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), "Open dialog"); + QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog")); + dialog->setEnabled(active); connect(dialog, &QAction::triggered, [this, index]() { onRosterItemDoubleClicked(index); }); @@ -561,7 +590,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) switch (state) { case Shared::both: case Shared::to: { - QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), "Unsubscribe"); + QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); + unsub->setEnabled(active); connect(unsub, &QAction::triggered, [this, cnt]() { emit unsubscribeContact(cnt->getAccountName(), cnt->getJid(), ""); }); @@ -570,14 +600,74 @@ void Squawk::onRosterContextMenu(const QPoint& point) case Shared::from: case Shared::unknown: case Shared::none: { - QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), "Subscribe"); + QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); + sub->setEnabled(active); connect(sub, &QAction::triggered, [this, cnt]() { emit subscribeContact(cnt->getAccountName(), cnt->getJid(), ""); }); } } + QString accName = cnt->getAccountName(); + QString cntJID = cnt->getJid(); + QString cntName = cnt->getName(); - QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), "Remove"); + QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename")); + rename->setEnabled(active); + connect(rename, &QAction::triggered, [this, cntName, accName, cntJID]() { + QInputDialog* dialog = new QInputDialog(this); + connect(dialog, &QDialog::accepted, [this, dialog, cntName, accName, cntJID]() { + QString newName = dialog->textValue(); + if (newName != cntName) { + emit renameContactRequest(accName, cntJID, newName); + } + dialog->deleteLater(); + }); + connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); + dialog->setInputMode(QInputDialog::TextInput); + dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(cntJID)); + dialog->setWindowTitle(tr("Renaming %1").arg(cntJID)); + dialog->setTextValue(cntName); + dialog->exec(); + }); + + + QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups")); + std::deque groupList = rosterModel.groupList(accName); + for (QString groupName : groupList) { + QAction* gr = groupsMenu->addAction(groupName); + gr->setCheckable(true); + gr->setChecked(rosterModel.groupHasContact(accName, groupName, cntJID)); + gr->setEnabled(active); + connect(gr, &QAction::toggled, [this, accName, groupName, cntJID](bool checked) { + if (checked) { + emit addContactToGroupRequest(accName, cntJID, groupName); + } else { + emit removeContactFromGroupRequest(accName, cntJID, groupName); + } + }); + } + 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); + connect(dialog, &QDialog::accepted, [this, dialog, accName, cntJID]() { + emit addContactToGroupRequest(accName, cntJID, dialog->textValue()); + dialog->deleteLater(); + }); + connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); + dialog->setInputMode(QInputDialog::TextInput); + dialog->setLabelText(tr("New group name")); + dialog->setWindowTitle(tr("Add %1 to a new group").arg(cntJID)); + dialog->exec(); + }); + + + QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); + card->setEnabled(active); + connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, accName, cnt->getJid(), false)); + + QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); + remove->setEnabled(active); connect(remove, &QAction::triggered, [this, cnt]() { emit removeContactRequest(cnt->getAccountName(), cnt->getJid()); }); @@ -588,7 +678,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) Models::Room* room = static_cast(item); hasMenu = true; - QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), "Open conversation"); + QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open conversation")); + dialog->setEnabled(active); connect(dialog, &QAction::triggered, [this, index]() { onRosterItemDoubleClicked(index); }); @@ -596,7 +687,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) Models::Roster::ElId id(room->getAccountName(), room->getJid()); if (room->getAutoJoin()) { - QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), "Unsubscribe"); + QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); + unsub->setEnabled(active); connect(unsub, &QAction::triggered, [this, id]() { emit setRoomAutoJoin(id.account, id.name, false); if (conversations.find(id) == conversations.end()) { //to leave the room if it's not opened in a conversation window @@ -604,7 +696,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) } }); } else { - QAction* unsub = contextMenu->addAction(Shared::icon("news-subscribe"), "Subscribe"); + QAction* unsub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); + unsub->setEnabled(active); connect(unsub, &QAction::triggered, [this, id]() { emit setRoomAutoJoin(id.account, id.name, true); if (conversations.find(id) == conversations.end()) { //to join the room if it's not already joined @@ -613,7 +706,8 @@ void Squawk::onRosterContextMenu(const QPoint& point) }); } - QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), "Remove"); + QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); + remove->setEnabled(active); connect(remove, &QAction::triggered, [this, id]() { emit removeRoomRequest(id.account, id.name); }); @@ -657,3 +751,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 2e92b28..70f0d5f 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; @@ -63,12 +64,17 @@ signals: void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); void removeContactRequest(const QString& account, const QString& jid); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet& groups); + void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); + void removeContactFromGroupRequest(const QString& account, const QString& jid, const QString& groupName); + void renameContactRequest(const QString& account, const QString& jid, const QString& newName); void setRoomJoined(const QString& account, const QString& jid, bool joined); void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin); void removeRoomRequest(const QString& account, const QString& jid); void 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); @@ -94,6 +100,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; @@ -105,6 +112,7 @@ private: QMenu* contextMenu; QDBusInterface dbus; std::map> requestedFiles; + std::map vCards; protected: void closeEvent(QCloseEvent * event) override; @@ -119,6 +127,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 e5d4852..951037a 100644 --- a/ui/utils/message.cpp +++ b/ui/utils/message.cpp @@ -22,7 +22,7 @@ #include #include "message.h" -const QRegExp urlReg("^(?!setFont(dFont); - date->setForegroundRole(QPalette::ToolTipText); QFont f; f.setBold(true); @@ -117,16 +116,16 @@ void Message::addDownloadDialog() text->setText(""); text->hide(); } - downloadButton = new QPushButton(QIcon::fromTheme("download"), "Download"); + downloadButton = new QPushButton(QIcon::fromTheme("download"), tr("Download")); downloadButton->setToolTip("" + msg.getOutOfBandUrl() + ""); if (errorDownloadingFile) { fileComment->setWordWrap(true); - fileComment->setText("Error downloading file: " + errorText + "\nYou can try again"); + fileComment->setText(tr("Error downloading file: %1\nYou can try again").arg(QCoreApplication::translate("NetworkErrors", errorText.toLatin1()))); } else { - fileComment->setText(sender->text() + " is offering you to download a file"); + 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; @@ -188,7 +187,7 @@ void Message::showFile(const QString& path) fileComment->show(); } file->setContextMenuPolicy(Qt::ActionsContextMenu); - QAction* openAction = new QAction(QIcon::fromTheme("document-new-from-template"), "Open", file); + QAction* openAction = new QAction(QIcon::fromTheme("document-new-from-template"), tr("Open"), file); connect(openAction, &QAction::triggered, [path]() { //TODO need to get rid of this shame QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }); 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 08cbaa4..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 "../../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/account.ui b/ui/widgets/account.ui index c9f9eb5..bfd0926 100644 --- a/ui/widgets/account.ui +++ b/ui/widgets/account.ui @@ -39,6 +39,9 @@ Your account login + + john_smith1987 + @@ -53,6 +56,9 @@ A server address of your account. Like 404.city or macaw.me + + macaw.me + @@ -83,6 +89,9 @@ QLineEdit::Password + + + false @@ -100,6 +109,9 @@ Just a name how would you call this account, doesn't affect anything + + John + diff --git a/ui/widgets/accounts.cpp b/ui/widgets/accounts.cpp index 367a0a5..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(); } @@ -120,13 +119,13 @@ void Accounts::updateConnectButton() } if (allConnected) { toDisconnect = true; - m_ui->connectButton->setText("Disconnect"); + m_ui->connectButton->setText(tr("Disconnect")); } else { toDisconnect = false; - m_ui->connectButton->setText("Connect"); + m_ui->connectButton->setText(tr("Connect")); } } else { - m_ui->connectButton->setText("Connect"); + m_ui->connectButton->setText(tr("Connect")); toDisconnect = false; m_ui->connectButton->setEnabled(false); } diff --git a/ui/widgets/chat.cpp b/ui/widgets/chat.cpp index aeaec65..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()); } @@ -56,7 +56,7 @@ void Chat::updateState() { Shared::Availability av = contact->getAvailability(); statusIcon->setPixmap(Shared::availabilityIcon(av, true).pixmap(40)); - statusIcon->setToolTip(Shared::availabilityNames[av]); + statusIcon->setToolTip(QCoreApplication::translate("Global", Shared::availabilityNames[av].toLatin1())); } void Chat::handleSendMessage(const QString& text) 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/conversation.ui b/ui/widgets/conversation.ui index 0298603..0eb7ae8 100644 --- a/ui/widgets/conversation.ui +++ b/ui/widgets/conversation.ui @@ -445,6 +445,9 @@ QFrame::NoFrame + + Type your message here... + 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 + + + + + +