feat/tray_pictogram #70

Open
pavanvo wants to merge 30 commits from pavanvo/squawk:feat/tray_pictogram into settings
82 changed files with 6134 additions and 1609 deletions

View File

@ -1,6 +1,28 @@
# Changelog # Changelog
## Squawk 0.2.1 (UNRELEASED) ## Squawk 0.2.2 (May 05, 2022)
### Bug fixes
- now when you remove an account it actually gets removed
- segfault on unitialized Availability in some rare occesions
- fixed crash when you open a dialog with someone that has only error messages in archive
- message height is now calculated correctly on Chinese and Japanese paragraphs
- the app doesn't crash on SIGINT anymore
### Improvements
- there is a way to disable an account and it wouldn't connect when you change availability
- if you cancel password query an account becomes inactive and doesn't annoy you anymore
- if you filled password field and chose KWallet as a storage Squawk wouldn't ask you again for the same password
- if left the password field empty and chose KWallet as a storage Squawk will try to get that passord from KWallet before asking you to input it
- accounts now connect to the server asyncronously - if one is stopped on password prompt another is connecting
- actualized translations, added English localization file
### New features
- new "About" window with links, license, gratitudes
- if the authentication failed Squawk will ask againg for your password and login
- now there is an amount of unread messages showing on top of Squawk launcher icon
- notifications now have buttons to open a conversation or to mark that message as read
## Squawk 0.2.1 (Apr 02, 2022)
### Bug fixes ### Bug fixes
- build in release mode now no longer spams warnings - build in release mode now no longer spams warnings
- build now correctly installs all build plugin libs - build now correctly installs all build plugin libs
@ -11,12 +33,13 @@
### Improvements ### Improvements
- reduced amount of places where platform specific path separator is used - reduced amount of places where platform specific path separator is used
- now message input is automatically focused when you open a dialog or a room - now message input is automatically focused when you open a dialog or a room
- what() method on unhandled exception now actually tells what happened
### New features ### New features
- the settings are here! You con config different stuff from there - the settings are here! You con config different stuff from there
- now it's possible to set up different qt styles from settings - now it's possible to set up different qt styles from settings
- if you have KConfig nad KConfigWidgets packages installed - you can chose from global color schemes - if you have KConfig nad KConfigWidgets packages installed - you can choose from global color schemes
- it's possible now to chose a folder where squawk is going to store downloaded files - it's possible now to choose a folder where squawk is going to store downloaded files
- now you can correct your message - now you can correct your message
## Squawk 0.2.0 (Jan 10, 2022) ## Squawk 0.2.0 (Jan 10, 2022)

View File

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.4) cmake_minimum_required(VERSION 3.4)
project(squawk VERSION 0.2.1 LANGUAGES CXX) project(squawk VERSION 0.2.2 LANGUAGES CXX)
cmake_policy(SET CMP0076 NEW) cmake_policy(SET CMP0076 NEW)
cmake_policy(SET CMP0079 NEW) cmake_policy(SET CMP0079 NEW)
@ -148,6 +148,7 @@ if(CMAKE_COMPILER_IS_GNUCXX)
target_compile_options(squawk PRIVATE ${COMPILE_OPTIONS}) target_compile_options(squawk PRIVATE ${COMPILE_OPTIONS})
endif(CMAKE_COMPILER_IS_GNUCXX) endif(CMAKE_COMPILER_IS_GNUCXX)
add_subdirectory(main)
add_subdirectory(core) add_subdirectory(core)
add_subdirectory(external/simpleCrypt) add_subdirectory(external/simpleCrypt)
add_subdirectory(packaging) add_subdirectory(packaging)
@ -159,6 +160,8 @@ add_subdirectory(ui)
# Install the executable # Install the executable
install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES README.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk)
install(FILES LICENSE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk)
if (CMAKE_BUILD_TYPE STREQUAL "Release") if (CMAKE_BUILD_TYPE STREQUAL "Release")
if (APPLE) if (APPLE)

View File

@ -595,17 +595,17 @@ pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> <one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author> Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -4,7 +4,7 @@
[![AUR version](https://img.shields.io/aur/version/squawk?style=flat-square)](https://aur.archlinux.org/packages/squawk/) [![AUR version](https://img.shields.io/aur/version/squawk?style=flat-square)](https://aur.archlinux.org/packages/squawk/)
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/macaw.me?logo=liberapay&style=flat-square)](https://liberapay.com/macaw.me) [![Liberapay patrons](https://img.shields.io/liberapay/patrons/macaw.me?logo=liberapay&style=flat-square)](https://liberapay.com/macaw.me)
![Squawk screenshot](https://macaw.me/images/squawk/0.2.0.png) ![Squawk screenshot](https://macaw.me/images/squawk/0.2.2.png)
### Prerequisites ### Prerequisites

View File

@ -6,14 +6,12 @@ endif(WIN32)
target_sources(squawk PRIVATE target_sources(squawk PRIVATE
account.cpp account.cpp
account.h account.h
adapterFuctions.cpp adapterfunctions.cpp
archive.cpp adapterfunctions.h
archive.h
conference.cpp conference.cpp
conference.h conference.h
contact.cpp contact.cpp
contact.h contact.h
main.cpp
networkaccess.cpp networkaccess.cpp
networkaccess.h networkaccess.h
rosteritem.cpp rosteritem.cpp
@ -22,13 +20,10 @@ target_sources(squawk PRIVATE
signalcatcher.h signalcatcher.h
squawk.cpp squawk.cpp
squawk.h squawk.h
storage.cpp
storage.h
urlstorage.cpp
urlstorage.h
) )
target_include_directories(squawk PRIVATE ${LMDB_INCLUDE_DIRS}) target_include_directories(squawk PRIVATE ${LMDB_INCLUDE_DIRS})
add_subdirectory(handlers) add_subdirectory(handlers)
add_subdirectory(storage)
add_subdirectory(passwordStorageEngines) add_subdirectory(passwordStorageEngines)

View File

@ -22,7 +22,7 @@
using namespace Core; using namespace Core;
Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, NetworkAccess* p_net, QObject* parent): Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, bool p_active, NetworkAccess* p_net, QObject* parent):
QObject(parent), QObject(parent),
name(p_name), name(p_name),
archiveQueries(), archiveQueries(),
@ -41,13 +41,15 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
rcpm(new QXmppMessageReceiptManager()), rcpm(new QXmppMessageReceiptManager()),
reconnectScheduled(false), reconnectScheduled(false),
reconnectTimer(new QTimer), reconnectTimer(new QTimer),
avatarHash(),
avatarType(),
ownVCardRequestInProgress(false),
network(p_net), network(p_net),
passwordType(Shared::AccountPassword::plain), passwordType(Shared::AccountPassword::plain),
lastError(Error::none),
pepSupport(false),
active(p_active),
notReadyPassword(false),
mh(new MessageHandler(this)), mh(new MessageHandler(this)),
rh(new RosterHandler(this)) rh(new RosterHandler(this)),
vh(new VCardHandler(this))
{ {
config.setUser(p_login); config.setUser(p_login);
config.setDomain(p_server); config.setDomain(p_server);
@ -73,10 +75,6 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
client.addExtension(mm); client.addExtension(mm);
client.addExtension(bm); client.addExtension(bm);
QObject::connect(vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived);
//QObject::connect(&vm, &QXmppVCardManager::clientVCardReceived, this, &Account::onOwnVCardReceived); //for some reason it doesn't work, launching from common handler
client.addExtension(um); client.addExtension(um);
QObject::connect(um, &QXmppUploadRequestManager::slotReceived, mh, &MessageHandler::onUploadSlotReceived); QObject::connect(um, &QXmppUploadRequestManager::slotReceived, mh, &MessageHandler::onUploadSlotReceived);
QObject::connect(um, &QXmppUploadRequestManager::requestFailed, mh, &MessageHandler::onUploadSlotRequestFailed); QObject::connect(um, &QXmppUploadRequestManager::requestFailed, mh, &MessageHandler::onUploadSlotRequestFailed);
@ -91,62 +89,18 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
client.addExtension(rcpm); client.addExtension(rcpm);
QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived); QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived);
QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
path += "/" + name;
QDir dir(path);
if (!dir.exists()) {
bool res = dir.mkpath(path);
if (!res) {
qDebug() << "Couldn't create a cache directory for account" << name;
throw 22;
}
}
QFile* avatar = new QFile(path + "/avatar.png");
QString type = "png";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpg");
type = "jpg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpeg");
type = "jpeg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.gif");
type = "gif";
}
}
}
if (avatar->exists()) {
if (avatar->open(QFile::ReadOnly)) {
QCryptographicHash sha1(QCryptographicHash::Sha1);
sha1.addData(avatar);
avatarHash = sha1.result();
avatarType = type;
}
}
if (avatarType.size() != 0) {
presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
presence.setPhotoHash(avatarHash.toUtf8());
} else {
presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady);
}
reconnectTimer->setSingleShot(true); reconnectTimer->setSingleShot(true);
QObject::connect(reconnectTimer, &QTimer::timeout, this, &Account::onReconnectTimer); QObject::connect(reconnectTimer, &QTimer::timeout, this, &Account::onReconnectTimer);
// QXmppLogger* logger = new QXmppLogger(this); if (name == "Test") {
// logger->setLoggingType(QXmppLogger::SignalLogging); QXmppLogger* logger = new QXmppLogger(this);
// client.setLogger(logger); logger->setLoggingType(QXmppLogger::SignalLogging);
// client.setLogger(logger);
// QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){
// qDebug() << text; QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){
// }); qDebug() << text;
});
}
} }
Account::~Account() Account::~Account()
@ -160,6 +114,7 @@ Account::~Account()
QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete);
QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError);
delete vh;
delete mh; delete mh;
delete rh; delete rh;
@ -185,7 +140,12 @@ void Core::Account::connect()
reconnectTimer->stop(); reconnectTimer->stop();
} }
if (state == Shared::ConnectionState::disconnected) { if (state == Shared::ConnectionState::disconnected) {
client.connectToServer(config, presence); if (notReadyPassword) {
emit needPassword();
} else {
client.connectToServer(config, presence);
}
} else { } else {
qDebug("An attempt to connect an account which is already connected, skipping"); qDebug("An attempt to connect an account which is already connected, skipping");
} }
@ -224,6 +184,7 @@ void Core::Account::onClientStateChange(QXmppClient::State st)
dm->requestItems(getServer()); dm->requestItems(getServer());
dm->requestInfo(getServer()); dm->requestInfo(getServer());
} }
lastError = Error::none;
emit connectionStateChanged(state); emit connectionStateChanged(state);
} }
} else { } else {
@ -255,45 +216,17 @@ void Core::Account::onClientStateChange(QXmppClient::State st)
void Core::Account::reconnect() void Core::Account::reconnect()
{ {
if (state == Shared::ConnectionState::connected && !reconnectScheduled) { if (!reconnectScheduled) { //TODO define behavior if It was connection or disconnecting
reconnectScheduled = true; if (state == Shared::ConnectionState::connected) {
reconnectTimer->start(500); reconnectScheduled = true;
client.disconnectFromServer(); reconnectTimer->start(500);
} else { client.disconnectFromServer();
qDebug() << "An attempt to reconnect account" << getName() << "which was not connected"; } else {
qDebug() << "An attempt to reconnect account" << getName() << "which was not connected";
}
} }
} }
QString Core::Account::getName() const {
return name;}
QString Core::Account::getLogin() const {
return config.user();}
QString Core::Account::getPassword() const {
return config.password();}
QString Core::Account::getServer() const {
return config.domain();}
Shared::AccountPassword Core::Account::getPasswordType() const {
return passwordType;}
void Core::Account::setPasswordType(Shared::AccountPassword pt) {
passwordType = pt; }
void Core::Account::setLogin(const QString& p_login) {
config.setUser(p_login);}
void Core::Account::setName(const QString& p_name) {
name = p_name;}
void Core::Account::setPassword(const QString& p_password) {
config.setPassword(p_password);}
void Core::Account::setServer(const QString& p_server) {
config.setDomain(p_server);}
Shared::Availability Core::Account::getAvailability() const Shared::Availability Core::Account::getAvailability() const
{ {
if (state == Shared::ConnectionState::connected) { if (state == Shared::ConnectionState::connected) {
@ -325,32 +258,11 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence)
QString jid = comps.front().toLower(); QString jid = comps.front().toLower();
QString resource = comps.back(); QString resource = comps.back();
QString myJid = getLogin() + "@" + getServer(); if (jid == getBareJid()) {
if (jid == myJid) {
if (resource == getResource()) { if (resource == getResource()) {
emit availabilityChanged(static_cast<Shared::Availability>(p_presence.availableStatusType())); emit availabilityChanged(static_cast<Shared::Availability>(p_presence.availableStatusType()));
} else { } else {
if (!ownVCardRequestInProgress) { vh->handleOtherPresenceOfMyAccountChange(p_presence);
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 { } else {
RosterItem* item = rh->getRosterItem(jid); RosterItem* item = rh->getRosterItem(jid);
@ -392,18 +304,6 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence)
} }
} }
QString Core::Account::getResource() const {
return config.resource();}
void Core::Account::setResource(const QString& p_resource) {
config.setResource(p_resource);}
QString Core::Account::getFullJid() const {
return getLogin() + "@" + getServer() + "/" + getResource();}
void Core::Account::sendMessage(const Shared::Message& data) {
mh->sendMessage(data);}
void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg) void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg)
{ {
if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) {
@ -517,6 +417,7 @@ void Core::Account::onClientError(QXmppClient::Error err)
qDebug() << "Error"; qDebug() << "Error";
QString errorText; QString errorText;
QString errorType; QString errorType;
lastError = Error::other;
switch (err) { switch (err) {
case QXmppClient::SocketError: case QXmppClient::SocketError:
errorText = client.socketErrorString(); errorText = client.socketErrorString();
@ -558,6 +459,7 @@ void Core::Account::onClientError(QXmppClient::Error err)
break; break;
case QXmppStanza::Error::NotAuthorized: case QXmppStanza::Error::NotAuthorized:
errorText = "Authentication error"; errorText = "Authentication error";
lastError = Error::authentication;
break; break;
#if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0) #if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0)
case QXmppStanza::Error::PaymentRequired: case QXmppStanza::Error::PaymentRequired:
@ -667,6 +569,166 @@ void Core::Account::setRoomJoined(const QString& jid, bool joined)
conf->setJoined(joined); conf->setJoined(joined);
} }
void Core::Account::onDiscoveryItemsReceived(const QXmppDiscoveryIq& items)
{
if (items.from() == getServer()) {
std::set<QString> needToRequest;
qDebug() << "Server items list received for account " << name << ":";
for (QXmppDiscoveryIq::Item item : items.items()) {
QString jid = item.jid();
if (jid != getServer()) {
qDebug() << " Node" << jid;
needToRequest.insert(jid);
} else {
qDebug() << " " << item.node().toStdString().c_str();
}
}
for (const QString& jid : needToRequest) {
dm->requestInfo(jid);
}
}
}
void Core::Account::onDiscoveryInfoReceived(const QXmppDiscoveryIq& info)
{
if (info.from() == getServer()) {
bool enableCC = false;
qDebug() << "Server info received for account" << name;
QStringList features = info.features();
qDebug() << "List of supported features of the server " << getServer() << ":";
for (const QString& feature : features) {
qDebug() << " " << feature.toStdString().c_str();
if (feature == "urn:xmpp:carbons:2") {
enableCC = true;
}
}
if (enableCC) {
qDebug() << "Enabling carbon copies for account" << name;
cm->setCarbonsEnabled(true);
}
qDebug() << "Requesting account" << name << "capabilities";
dm->requestInfo(getBareJid());
} else if (info.from() == getBareJid()) {
qDebug() << "Received capabilities for account" << name << ":";
QList<QXmppDiscoveryIq::Identity> identities = info.identities();
bool pepSupported = false;
for (const QXmppDiscoveryIq::Identity& identity : identities) {
QString type = identity.type();
qDebug() << " " << identity.category() << type;
if (type == "pep") {
pepSupported = true;
}
}
rh->setPepSupport(pepSupported);
} else {
qDebug() << "Received info for account" << name << "about" << info.from();
QList<QXmppDiscoveryIq::Identity> identities = info.identities();
for (const QXmppDiscoveryIq::Identity& identity : identities) {
qDebug() << " " << identity.name() << identity.category() << identity.type();
}
}
}
void Core::Account::handleDisconnection()
{
cm->setCarbonsEnabled(false);
rh->handleOffline();
vh->handleOffline();
archiveQueries.clear();
}
void Core::Account::onContactHistoryResponse(const std::list<Shared::Message>& list, bool last)
{
RosterItem* contact = static_cast<RosterItem*>(sender());
qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements";
if (last) {
qDebug() << "The response contains the first accounted message";
}
emit responseArchive(contact->jid, list, last);
}
bool Core::Account::getActive() const {
return active;}
void Core::Account::setActive(bool p_active) {
if (active != p_active) {
active = p_active;
emit changed({
{"active", active}
});
}
}
QString Core::Account::getResource() const {
return config.resource();}
void Core::Account::setResource(const QString& p_resource) {
config.setResource(p_resource);}
QString Core::Account::getBareJid() const {
return getLogin() + "@" + getServer();}
QString Core::Account::getFullJid() const {
return getBareJid() + "/" + getResource();}
QString Core::Account::getName() const {
return name;}
QString Core::Account::getLogin() const {
return config.user();}
QString Core::Account::getPassword() const {
return config.password();}
QString Core::Account::getServer() const {
return config.domain();}
Shared::AccountPassword Core::Account::getPasswordType() const {
return passwordType;}
void Core::Account::setPasswordType(Shared::AccountPassword pt) {
passwordType = pt; }
void Core::Account::setLogin(const QString& p_login) {
config.setUser(p_login);}
void Core::Account::setName(const QString& p_name) {
name = p_name;}
void Core::Account::setPassword(const QString& p_password) {
config.setPassword(p_password);
notReadyPassword = false;
}
void Core::Account::setServer(const QString& p_server) {
config.setDomain(p_server);}
void Core::Account::sendMessage(const Shared::Message& data) {
mh->sendMessage(data);}
void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data){
mh->requestChangeMessage(jid, messageId, data);}
void Core::Account::resendMessage(const QString& jid, const QString& id) {
mh->resendMessage(jid, id);}
void Core::Account::replaceMessage(const QString& originalId, const Shared::Message& data) {
mh->sendMessage(data, false, originalId);}
void Core::Account::requestVCard(const QString& jid) {
vh->requestVCard(jid);}
void Core::Account::uploadVCard(const Shared::VCard& card) {
vh->uploadVCard(card);}
QString Core::Account::getAvatarPath() const {
return vh->getAvatarPath();}
void Core::Account::removeRoomRequest(const QString& jid){ void Core::Account::removeRoomRequest(const QString& jid){
rh->removeRoomRequest(jid);} rh->removeRoomRequest(jid);}
@ -689,253 +751,9 @@ void Core::Account::renameContactRequest(const QString& jid, const QString& newN
} }
} }
void Core::Account::onVCardReceived(const QXmppVCardIq& card) void Core::Account::invalidatePassword() {
{ notReadyPassword = true;}
QString id = card.from();
QStringList comps = id.split("/");
QString jid = comps.front().toLower();
QString resource("");
if (comps.size() > 1) {
resource = comps.back();
}
pendingVCardRequests.erase(id);
RosterItem* item = rh->getRosterItem(jid);
if (item == 0) {
if (jid == getLogin() + "@" + getServer()) {
onOwnVCardReceived(card);
} else {
qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping";
}
return;
}
Shared::VCard vCard = item->handleResponseVCard(card, resource);
emit receivedVCard(jid, vCard);
}
void Core::Account::onOwnVCardReceived(const QXmppVCardIq& card) Core::Account::Error Core::Account::getLastError() const {
{ return lastError;}
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<QString, QVariant> change;
if (avatarType.size() > 0) {
presence.setPhotoHash(avatarHash.toUtf8());
presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
change.insert("avatarPath", path + "avatar." + avatarType);
} else {
presence.setPhotoHash("");
presence.setVCardUpdateType(QXmppPresence::VCardUpdateNoPhoto);
change.insert("avatarPath", "");
}
client.setClientPresence(presence);
emit changed(change);
}
ownVCardRequestInProgress = false;
Shared::VCard vCard;
initializeVCard(vCard, card);
if (avatarType.size() > 0) {
vCard.setAvatarType(Shared::Avatar::valid);
vCard.setAvatarPath(path + "avatar." + avatarType);
} else {
vCard.setAvatarType(Shared::Avatar::empty);
}
emit receivedVCard(getLogin() + "@" + getServer(), vCard);
}
QString Core::Account::getAvatarPath() const
{
if (avatarType.size() == 0) {
return "";
} else {
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/" + "avatar." + avatarType;
}
}
void Core::Account::requestVCard(const QString& jid)
{
if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) {
qDebug() << "requesting vCard" << jid;
if (jid == getLogin() + "@" + getServer()) {
if (!ownVCardRequestInProgress) {
vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
} else {
vm->requestVCard(jid);
pendingVCardRequests.insert(jid);
}
}
}
void Core::Account::uploadVCard(const Shared::VCard& card)
{
QXmppVCardIq iq;
initializeQXmppVCard(iq, card);
if (card.getAvatarType() != Shared::Avatar::empty) {
QString newPath = card.getAvatarPath();
QString oldPath = getAvatarPath();
QByteArray data;
QString type;
if (newPath != oldPath) {
QFile avatar(newPath);
if (!avatar.open(QFile::ReadOnly)) {
qDebug() << "An attempt to upload new vCard to account" << name
<< "but it wasn't possible to read file" << newPath
<< "which was supposed to be new avatar, uploading old avatar";
if (avatarType.size() > 0) {
QFile oA(oldPath);
if (!oA.open(QFile::ReadOnly)) {
qDebug() << "Couldn't read old avatar of account" << name << ", uploading empty avatar";
} else {
data = oA.readAll();
}
}
} else {
data = avatar.readAll();
}
} else {
if (avatarType.size() > 0) {
QFile oA(oldPath);
if (!oA.open(QFile::ReadOnly)) {
qDebug() << "Couldn't read old avatar of account" << name << ", uploading empty avatar";
} else {
data = oA.readAll();
}
}
}
if (data.size() > 0) {
QMimeDatabase db;
type = db.mimeTypeForData(data).name();
iq.setPhoto(data);
iq.setPhotoType(type);
}
}
vm->setClientVCard(iq);
onOwnVCardReceived(iq);
}
void Core::Account::onDiscoveryItemsReceived(const QXmppDiscoveryIq& items)
{
for (QXmppDiscoveryIq::Item item : items.items()) {
if (item.jid() != getServer()) {
dm->requestInfo(item.jid());
}
}
}
void Core::Account::onDiscoveryInfoReceived(const QXmppDiscoveryIq& info)
{
qDebug() << "Discovery info received for account" << name;
if (info.from() == getServer()) {
if (info.features().contains("urn:xmpp:carbons:2")) {
qDebug() << "Enabling carbon copies for account" << name;
cm->setCarbonsEnabled(true);
}
}
}
void Core::Account::handleDisconnection()
{
cm->setCarbonsEnabled(false);
rh->handleOffline();
archiveQueries.clear();
pendingVCardRequests.clear();
Shared::VCard vCard; //just to show, that there is now more pending request
for (const QString& jid : pendingVCardRequests) {
emit receivedVCard(jid, vCard); //need to show it better in the future, like with an error
}
pendingVCardRequests.clear();
ownVCardRequestInProgress = false;
}
void Core::Account::onContactHistoryResponse(const std::list<Shared::Message>& list, bool last)
{
RosterItem* contact = static_cast<RosterItem*>(sender());
qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements";
if (last) {
qDebug() << "The response contains the first accounted message";
}
emit responseArchive(contact->jid, list, last);
}
void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data){
mh->requestChangeMessage(jid, messageId, data);}
void Core::Account::resendMessage(const QString& jid, const QString& id) {
mh->resendMessage(jid, id);}
void Core::Account::replaceMessage(const QString& originalId, const Shared::Message& data) {
mh->sendMessage(data, false, originalId);}

View File

@ -39,7 +39,6 @@
#include <QXmppBookmarkManager.h> #include <QXmppBookmarkManager.h>
#include <QXmppBookmarkSet.h> #include <QXmppBookmarkSet.h>
#include <QXmppUploadRequestManager.h> #include <QXmppUploadRequestManager.h>
#include <QXmppVCardIq.h>
#include <QXmppVCardManager.h> #include <QXmppVCardManager.h>
#include <QXmppMessageReceiptManager.h> #include <QXmppMessageReceiptManager.h>
@ -50,6 +49,7 @@
#include "handlers/messagehandler.h" #include "handlers/messagehandler.h"
#include "handlers/rosterhandler.h" #include "handlers/rosterhandler.h"
#include "handlers/vcardhandler.h"
namespace Core namespace Core
{ {
@ -59,12 +59,20 @@ class Account : public QObject
Q_OBJECT Q_OBJECT
friend class MessageHandler; friend class MessageHandler;
friend class RosterHandler; friend class RosterHandler;
friend class VCardHandler;
public: public:
enum class Error {
authentication,
other,
none
};
Account( Account(
const QString& p_login, const QString& p_login,
const QString& p_server, const QString& p_server,
const QString& p_password, const QString& p_password,
const QString& p_name, const QString& p_name,
bool p_active,
NetworkAccess* p_net, NetworkAccess* p_net,
QObject* parent = 0); QObject* parent = 0);
~Account(); ~Account();
@ -76,8 +84,12 @@ public:
QString getPassword() const; QString getPassword() const;
QString getResource() const; QString getResource() const;
QString getAvatarPath() const; QString getAvatarPath() const;
QString getBareJid() const;
QString getFullJid() const;
Shared::Availability getAvailability() const; Shared::Availability getAvailability() const;
Shared::AccountPassword getPasswordType() const; Shared::AccountPassword getPasswordType() const;
Error getLastError() const;
bool getActive() const;
void setName(const QString& p_name); void setName(const QString& p_name);
void setLogin(const QString& p_login); void setLogin(const QString& p_login);
@ -86,8 +98,8 @@ public:
void setResource(const QString& p_resource); void setResource(const QString& p_resource);
void setAvailability(Shared::Availability avail); void setAvailability(Shared::Availability avail);
void setPasswordType(Shared::AccountPassword pt); void setPasswordType(Shared::AccountPassword pt);
QString getFullJid() const;
void sendMessage(const Shared::Message& data); void sendMessage(const Shared::Message& data);
void setActive(bool p_active);
void requestArchive(const QString& jid, int count, const QString& before); void requestArchive(const QString& jid, int count, const QString& before);
void subscribeToContact(const QString& jid, const QString& reason); void subscribeToContact(const QString& jid, const QString& reason);
void unsubscribeFromContact(const QString& jid, const QString& reason); void unsubscribeFromContact(const QString& jid, const QString& reason);
@ -105,6 +117,7 @@ public:
void uploadVCard(const Shared::VCard& card); void uploadVCard(const Shared::VCard& card);
void resendMessage(const QString& jid, const QString& id); void resendMessage(const QString& jid, const QString& id);
void replaceMessage(const QString& originalId, const Shared::Message& data); void replaceMessage(const QString& originalId, const Shared::Message& data);
void invalidatePassword();
public slots: public slots:
void connect(); void connect();
@ -137,6 +150,7 @@ signals:
void receivedVCard(const QString& jid, const Shared::VCard& card); void receivedVCard(const QString& jid, const Shared::VCard& card);
void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap<QString, QString> headers); void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap<QString, QString> headers);
void uploadFileError(const QString& jid, const QString& messageId, const QString& error); void uploadFileError(const QString& jid, const QString& messageId, const QString& error);
void needPassword();
private: private:
QString name; QString name;
@ -157,16 +171,16 @@ private:
bool reconnectScheduled; bool reconnectScheduled;
QTimer* reconnectTimer; QTimer* reconnectTimer;
std::set<QString> pendingVCardRequests;
QString avatarHash;
QString avatarType;
bool ownVCardRequestInProgress;
NetworkAccess* network; NetworkAccess* network;
Shared::AccountPassword passwordType; Shared::AccountPassword passwordType;
Error lastError;
bool pepSupport;
bool active;
bool notReadyPassword;
MessageHandler* mh; MessageHandler* mh;
RosterHandler* rh; RosterHandler* rh;
VCardHandler* vh;
private slots: private slots:
void onClientStateChange(QXmppClient::State state); void onClientStateChange(QXmppClient::State state);
@ -179,9 +193,6 @@ private slots:
void onMamResultsReceived(const QString &queryId, const QXmppResultSetReply &resultSetReply, bool complete); void onMamResultsReceived(const QString &queryId, const QXmppResultSetReply &resultSetReply, bool complete);
void onMamLog(QXmppLogger::MessageType type, const QString &msg); void onMamLog(QXmppLogger::MessageType type, const QString &msg);
void onVCardReceived(const QXmppVCardIq& card);
void onOwnVCardReceived(const QXmppVCardIq& card);
void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items); void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items);
void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info); void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info);
@ -191,9 +202,6 @@ private:
void handleDisconnection(); void handleDisconnection();
void onReconnectTimer(); void onReconnectTimer();
}; };
void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card);
void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Squawk messenger. * Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me> * Copyright (C) 2019 Yury Gubich <blue@macaw.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -15,10 +15,8 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef CORE_ADAPTER_FUNCTIONS_H
#define CORE_ADAPTER_FUNCTIONS_H
#include "account.h" #include "adapterfunctions.h"
void Core::initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card) void Core::initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card)
{ {
@ -271,5 +269,3 @@ void Core::initializeQXmppVCard(QXmppVCardIq& iq, const Shared::VCard& card) {
iq.setEmails(emails); iq.setEmails(emails);
iq.setPhones(phs); iq.setPhones(phs);
} }
#endif // CORE_ADAPTER_FUNCTIONS_H

32
core/adapterfunctions.h Normal file
View File

@ -0,0 +1,32 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef CORE_ADAPTER_FUNCTIONS_H
#define CORE_ADAPTER_FUNCTIONS_H
#include <QXmppVCardIq.h>
#include <shared/vcard.h>
namespace Core {
void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card);
void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard);
}
#endif // CORE_ADAPTER_FUNCTIONS_H

View File

@ -3,4 +3,6 @@ target_sources(squawk PRIVATE
messagehandler.h messagehandler.h
rosterhandler.cpp rosterhandler.cpp
rosterhandler.h rosterhandler.h
vcardhandler.cpp
vcardhandler.h
) )

View File

@ -176,7 +176,7 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
target.setForwarded(forwarded); target.setForwarded(forwarded);
if (guessing) { if (guessing) {
if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) { if (target.getFromJid() == acc->getBareJid()) {
outgoing = true; outgoing = true;
} else { } else {
outgoing = false; outgoing = false;

View File

@ -26,7 +26,8 @@ Core::RosterHandler::RosterHandler(Core::Account* account):
conferences(), conferences(),
groups(), groups(),
queuedContacts(), queuedContacts(),
outOfRosterContacts() outOfRosterContacts(),
pepSupport(false)
{ {
connect(acc->rm, &QXmppRosterManager::rosterReceived, this, &RosterHandler::onRosterReceived); connect(acc->rm, &QXmppRosterManager::rosterReceived, this, &RosterHandler::onRosterReceived);
connect(acc->rm, &QXmppRosterManager::itemAdded, this, &RosterHandler::onRosterItemAdded); connect(acc->rm, &QXmppRosterManager::itemAdded, this, &RosterHandler::onRosterItemAdded);
@ -51,8 +52,7 @@ Core::RosterHandler::~RosterHandler()
void Core::RosterHandler::onRosterReceived() void Core::RosterHandler::onRosterReceived()
{ {
acc->vm->requestClientVCard(); //TODO need to make sure server actually supports vCards acc->requestVCard(acc->getBareJid()); //TODO need to make sure server actually supports vCards
acc->ownVCardRequestInProgress = true;
QStringList bj = acc->rm->getRosterBareJids(); QStringList bj = acc->rm->getRosterBareJids();
for (int i = 0; i < bj.size(); ++i) { for (int i = 0; i < bj.size(); ++i) {
@ -588,4 +588,13 @@ void Core::RosterHandler::handleOffline()
pair.second->clearArchiveRequests(); pair.second->clearArchiveRequests();
pair.second->downgradeDatabaseState(); pair.second->downgradeDatabaseState();
} }
setPepSupport(false);
}
void Core::RosterHandler::setPepSupport(bool support)
{
if (pepSupport != support) {
pepSupport = support;
}
} }

View File

@ -64,6 +64,7 @@ public:
void storeConferences(); void storeConferences();
void clearConferences(); void clearConferences();
void setPepSupport(bool support);
private slots: private slots:
void onRosterReceived(); void onRosterReceived();
@ -107,6 +108,7 @@ private:
std::map<QString, std::set<QString>> groups; std::map<QString, std::set<QString>> groups;
std::map<QString, QString> queuedContacts; std::map<QString, QString> queuedContacts;
std::set<QString> outOfRosterContacts; std::set<QString> outOfRosterContacts;
bool pepSupport;
}; };
} }

View File

@ -0,0 +1,312 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#include "vcardhandler.h"
#include "core/account.h"
Core::VCardHandler::VCardHandler(Account* account):
QObject(),
acc(account),
ownVCardRequestInProgress(false),
pendingVCardRequests(),
avatarHash(),
avatarType()
{
connect(acc->vm, &QXmppVCardManager::vCardReceived, this, &VCardHandler::onVCardReceived);
//for some reason it doesn't work, launching from common handler
//connect(acc->vm, &QXmppVCardManager::clientVCardReceived, this, &VCardHandler::onOwnVCardReceived);
QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
path += "/" + acc->name;
QDir dir(path);
if (!dir.exists()) {
bool res = dir.mkpath(path);
if (!res) {
qDebug() << "Couldn't create a cache directory for account" << acc->name;
throw 22;
}
}
QFile* avatar = new QFile(path + "/avatar.png");
QString type = "png";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpg");
type = "jpg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpeg");
type = "jpeg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.gif");
type = "gif";
}
}
}
if (avatar->exists()) {
if (avatar->open(QFile::ReadOnly)) {
QCryptographicHash sha1(QCryptographicHash::Sha1);
sha1.addData(avatar);
avatarHash = sha1.result();
avatarType = type;
}
}
if (avatarType.size() != 0) {
acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
acc->presence.setPhotoHash(avatarHash.toUtf8());
} else {
acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady);
}
}
Core::VCardHandler::~VCardHandler()
{
}
void Core::VCardHandler::onVCardReceived(const QXmppVCardIq& card)
{
QString id = card.from();
QStringList comps = id.split("/");
QString jid = comps.front().toLower();
QString resource("");
if (comps.size() > 1) {
resource = comps.back();
}
pendingVCardRequests.erase(id);
RosterItem* item = acc->rh->getRosterItem(jid);
if (item == 0) {
if (jid == acc->getBareJid()) {
onOwnVCardReceived(card);
} else {
qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping";
}
return;
}
Shared::VCard vCard = item->handleResponseVCard(card, resource);
emit acc->receivedVCard(jid, vCard);
}
void Core::VCardHandler::onOwnVCardReceived(const QXmppVCardIq& card)
{
QByteArray ava = card.photo();
bool avaChanged = false;
QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + acc->name + "/";
if (ava.size() > 0) {
QCryptographicHash sha1(QCryptographicHash::Sha1);
sha1.addData(ava);
QString newHash(sha1.result());
QMimeDatabase db;
QMimeType newType = db.mimeTypeForData(ava);
if (avatarType.size() > 0) {
if (avatarHash != newHash) {
QString oldPath = path + "avatar." + avatarType;
QFile oldAvatar(oldPath);
bool oldToRemove = false;
if (oldAvatar.exists()) {
if (oldAvatar.rename(oldPath + ".bak")) {
oldToRemove = true;
} else {
qDebug() << "Received new avatar for account" << acc->name << "but can't get rid of the old one, doing nothing";
}
}
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
if (newAvatar.open(QFile::WriteOnly)) {
newAvatar.write(ava);
newAvatar.close();
avatarHash = newHash;
avatarType = newType.preferredSuffix();
avaChanged = true;
} else {
qDebug() << "Received new avatar for account" << acc->name << "but can't save it";
if (oldToRemove) {
qDebug() << "rolling back to the old avatar";
if (!oldAvatar.rename(oldPath)) {
qDebug() << "Couldn't roll back to the old avatar in account" << acc->name;
}
}
}
}
} else {
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
if (newAvatar.open(QFile::WriteOnly)) {
newAvatar.write(ava);
newAvatar.close();
avatarHash = newHash;
avatarType = newType.preferredSuffix();
avaChanged = true;
} else {
qDebug() << "Received new avatar for account" << acc->name << "but can't save it";
}
}
} else {
if (avatarType.size() > 0) {
QFile oldAvatar(path + "avatar." + avatarType);
if (!oldAvatar.remove()) {
qDebug() << "Received vCard for account" << acc->name << "without avatar, but can't get rid of the file, doing nothing";
} else {
avatarType = "";
avatarHash = "";
avaChanged = true;
}
}
}
if (avaChanged) {
QMap<QString, QVariant> change;
if (avatarType.size() > 0) {
acc->presence.setPhotoHash(avatarHash.toUtf8());
acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
change.insert("avatarPath", path + "avatar." + avatarType);
} else {
acc->presence.setPhotoHash("");
acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateNoPhoto);
change.insert("avatarPath", "");
}
acc->client.setClientPresence(acc->presence);
emit acc->changed(change);
}
ownVCardRequestInProgress = false;
Shared::VCard vCard;
initializeVCard(vCard, card);
if (avatarType.size() > 0) {
vCard.setAvatarType(Shared::Avatar::valid);
vCard.setAvatarPath(path + "avatar." + avatarType);
} else {
vCard.setAvatarType(Shared::Avatar::empty);
}
emit acc->receivedVCard(acc->getBareJid(), vCard);
}
void Core::VCardHandler::handleOffline()
{
pendingVCardRequests.clear();
Shared::VCard vCard; //just to show, that there is now more pending request
for (const QString& jid : pendingVCardRequests) {
emit acc->receivedVCard(jid, vCard); //need to show it better in the future, like with an error
}
pendingVCardRequests.clear();
ownVCardRequestInProgress = false;
}
void Core::VCardHandler::requestVCard(const QString& jid)
{
if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) {
qDebug() << "requesting vCard" << jid;
if (jid == acc->getBareJid()) {
if (!ownVCardRequestInProgress) {
acc->vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
} else {
acc->vm->requestVCard(jid);
pendingVCardRequests.insert(jid);
}
}
}
void Core::VCardHandler::handleOtherPresenceOfMyAccountChange(const QXmppPresence& p_presence)
{
if (!ownVCardRequestInProgress) {
switch (p_presence.vCardUpdateType()) {
case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo
break;
case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here
break;
case QXmppPresence::VCardUpdateNoPhoto: //there is no photo, need to drop if any
if (avatarType.size() > 0) {
acc->vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
break;
case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load
if (avatarHash != p_presence.photoHash()) {
acc->vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
break;
}
}
}
void Core::VCardHandler::uploadVCard(const Shared::VCard& card)
{
QXmppVCardIq iq;
initializeQXmppVCard(iq, card);
if (card.getAvatarType() != Shared::Avatar::empty) {
QString newPath = card.getAvatarPath();
QString oldPath = getAvatarPath();
QByteArray data;
QString type;
if (newPath != oldPath) {
QFile avatar(newPath);
if (!avatar.open(QFile::ReadOnly)) {
qDebug() << "An attempt to upload new vCard to account" << acc->name
<< "but it wasn't possible to read file" << newPath
<< "which was supposed to be new avatar, uploading old avatar";
if (avatarType.size() > 0) {
QFile oA(oldPath);
if (!oA.open(QFile::ReadOnly)) {
qDebug() << "Couldn't read old avatar of account" << acc->name << ", uploading empty avatar";
} else {
data = oA.readAll();
}
}
} else {
data = avatar.readAll();
}
} else {
if (avatarType.size() > 0) {
QFile oA(oldPath);
if (!oA.open(QFile::ReadOnly)) {
qDebug() << "Couldn't read old avatar of account" << acc->name << ", uploading empty avatar";
} else {
data = oA.readAll();
}
}
}
if (data.size() > 0) {
QMimeDatabase db;
type = db.mimeTypeForData(data).name();
iq.setPhoto(data);
iq.setPhotoType(type);
}
}
acc->vm->setClientVCard(iq);
onOwnVCardReceived(iq);
}
QString Core::VCardHandler::getAvatarPath() const
{
if (avatarType.size() == 0) {
return "";
} else {
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + acc->name + "/" + "avatar." + avatarType;
}
}

View File

@ -0,0 +1,65 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#ifndef CORE_VCARDHANDLER_H
#define CORE_VCARDHANDLER_H
#include <set>
#include <QXmppVCardIq.h>
#include <QXmppPresence.h>
#include <QObject>
#include <shared/vcard.h>
#include <core/adapterfunctions.h>
/**
* @todo write docs
*/
namespace Core {
class Account;
class VCardHandler : public QObject
{
Q_OBJECT
public:
VCardHandler(Account* account);
~VCardHandler();
void handleOffline();
void requestVCard(const QString& jid);
void handleOtherPresenceOfMyAccountChange(const QXmppPresence& p_presence);
void uploadVCard(const Shared::VCard& card);
QString getAvatarPath() const;
private slots:
void onVCardReceived(const QXmppVCardIq& card);
void onOwnVCardReceived(const QXmppVCardIq& card);
private:
Account* acc;
bool ownVCardRequestInProgress;
std::set<QString> pendingVCardRequests;
QString avatarHash;
QString avatarType;
};
}
#endif // CORE_VCARDHANDLER_H

View File

@ -1,209 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "../shared/global.h"
#include "../shared/messageinfo.h"
#include "../shared/pathcheck.h"
#include "../ui/squawk.h"
#include "signalcatcher.h"
#include "squawk.h"
#include <QLibraryInfo>
#include <QSettings>
#include <QStandardPaths>
#include <QTranslator>
#include <QtCore/QObject>
#include <QtCore/QThread>
#include <QtWidgets/QApplication>
#include <QDir>
int main(int argc, char *argv[])
{
qRegisterMetaType<Shared::Message>("Shared::Message");
qRegisterMetaType<Shared::MessageInfo>("Shared::MessageInfo");
qRegisterMetaType<Shared::VCard>("Shared::VCard");
qRegisterMetaType<std::list<Shared::Message>>("std::list<Shared::Message>");
qRegisterMetaType<std::list<Shared::MessageInfo>>("std::list<Shared::MessageInfo>");
qRegisterMetaType<QSet<QString>>("QSet<QString>");
qRegisterMetaType<Shared::ConnectionState>("Shared::ConnectionState");
qRegisterMetaType<Shared::Availability>("Shared::Availability");
QApplication app(argc, argv);
SignalCatcher sc(&app);
#ifdef Q_OS_WIN
// Windows need an organization name for QSettings to work
// https://doc.qt.io/qt-5/qsettings.html#basic-usage
{
const QString& orgName = QApplication::organizationName();
if (orgName.isNull() || orgName.isEmpty()) {
QApplication::setOrganizationName("squawk");
}
}
#endif
QApplication::setApplicationName("squawk");
QApplication::setApplicationDisplayName("Squawk");
QApplication::setApplicationVersion("0.2.1");
QTranslator qtTranslator;
qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
app.installTranslator(&qtTranslator);
QTranslator myappTranslator;
QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
bool found = false;
for (QString share : shares) {
found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n");
if (found) {
break;
}
}
if (!found) {
myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", QCoreApplication::applicationDirPath());
}
app.installTranslator(&myappTranslator);
QIcon icon;
icon.addFile(":images/logo.svg", QSize(16, 16));
icon.addFile(":images/logo.svg", QSize(24, 24));
icon.addFile(":images/logo.svg", QSize(32, 32));
icon.addFile(":images/logo.svg", QSize(48, 48));
icon.addFile(":images/logo.svg", QSize(64, 64));
icon.addFile(":images/logo.svg", QSize(96, 96));
icon.addFile(":images/logo.svg", QSize(128, 128));
icon.addFile(":images/logo.svg", QSize(256, 256));
icon.addFile(":images/logo.svg", QSize(512, 512));
QApplication::setWindowIcon(icon);
new Shared::Global(); //translates enums
QSettings settings;
QVariant vs = settings.value("style");
if (vs.isValid()) {
QString style = vs.toString().toLower();
if (style != "system") {
Shared::Global::setStyle(style);
}
}
if (Shared::Global::supported("colorSchemeTools")) {
QVariant vt = settings.value("theme");
if (vt.isValid()) {
QString theme = vt.toString();
if (theme.toLower() != "system") {
Shared::Global::setTheme(theme);
}
}
}
QString path = Shared::downloadsPathCheck();
if (path.size() > 0) {
settings.setValue("downloadsPath", path);
} else {
qDebug() << "couldn't initialize directory for downloads, quitting";
return -1;
}
Squawk w;
w.show();
Core::Squawk* squawk = new Core::Squawk();
QThread* coreThread = new QThread();
squawk->moveToThread(coreThread);
QObject::connect(&sc, &SignalCatcher::interrupt, &app, &QApplication::closeAllWindows);
QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start);
QObject::connect(&app, &QApplication::lastWindowClosed, squawk, &Core::Squawk::stop);
QObject::connect(&app, &QApplication::lastWindowClosed, &w, &Squawk::writeSettings);
//QObject::connect(&app, &QApplication::aboutToQuit, &w, &QMainWindow::close);
QObject::connect(squawk, &Core::Squawk::quit, squawk, &Core::Squawk::deleteLater);
QObject::connect(squawk, &Core::Squawk::quit, coreThread, &QThread::quit, Qt::QueuedConnection);
QObject::connect(coreThread, &QThread::finished, &app, &QApplication::quit, Qt::QueuedConnection);
QObject::connect(&w, &Squawk::newAccountRequest, squawk, &Core::Squawk::newAccountRequest);
QObject::connect(&w, &Squawk::modifyAccountRequest, squawk, &Core::Squawk::modifyAccountRequest);
QObject::connect(&w, &Squawk::removeAccountRequest, squawk, &Core::Squawk::removeAccountRequest);
QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount);
QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount);
QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState);
QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage);
QObject::connect(&w, &Squawk::replaceMessage, squawk,&Core::Squawk::replaceMessage);
QObject::connect(&w, &Squawk::resendMessage, squawk,&Core::Squawk::resendMessage);
QObject::connect(&w, &Squawk::requestArchive, squawk, &Core::Squawk::requestArchive);
QObject::connect(&w, &Squawk::subscribeContact, squawk, &Core::Squawk::subscribeContact);
QObject::connect(&w, &Squawk::unsubscribeContact, squawk, &Core::Squawk::unsubscribeContact);
QObject::connect(&w, &Squawk::addContactRequest, squawk, &Core::Squawk::addContactRequest);
QObject::connect(&w, &Squawk::removeContactRequest, squawk, &Core::Squawk::removeContactRequest);
QObject::connect(&w, &Squawk::setRoomJoined, squawk, &Core::Squawk::setRoomJoined);
QObject::connect(&w, &Squawk::setRoomAutoJoin, squawk, &Core::Squawk::setRoomAutoJoin);
QObject::connect(&w, &Squawk::removeRoomRequest, squawk, &Core::Squawk::removeRoomRequest);
QObject::connect(&w, &Squawk::addRoomRequest, squawk, &Core::Squawk::addRoomRequest);
QObject::connect(&w, &Squawk::fileDownloadRequest, squawk, &Core::Squawk::fileDownloadRequest);
QObject::connect(&w, &Squawk::addContactToGroupRequest, squawk, &Core::Squawk::addContactToGroupRequest);
QObject::connect(&w, &Squawk::removeContactFromGroupRequest, squawk, &Core::Squawk::removeContactFromGroupRequest);
QObject::connect(&w, &Squawk::renameContactRequest, squawk, &Core::Squawk::renameContactRequest);
QObject::connect(&w, &Squawk::requestVCard, squawk, &Core::Squawk::requestVCard);
QObject::connect(&w, &Squawk::uploadVCard, squawk, &Core::Squawk::uploadVCard);
QObject::connect(&w, &Squawk::responsePassword, squawk, &Core::Squawk::responsePassword);
QObject::connect(&w, &Squawk::localPathInvalid, squawk, &Core::Squawk::onLocalPathInvalid);
QObject::connect(&w, &Squawk::changeDownloadsPath, squawk, &Core::Squawk::changeDownloadsPath);
QObject::connect(squawk, &Core::Squawk::newAccount, &w, &Squawk::newAccount);
QObject::connect(squawk, &Core::Squawk::addContact, &w, &Squawk::addContact);
QObject::connect(squawk, &Core::Squawk::changeAccount, &w, &Squawk::changeAccount);
QObject::connect(squawk, &Core::Squawk::removeAccount, &w, &Squawk::removeAccount);
QObject::connect(squawk, &Core::Squawk::addGroup, &w, &Squawk::addGroup);
QObject::connect(squawk, &Core::Squawk::removeGroup, &w, &Squawk::removeGroup);
QObject::connect(squawk, qOverload<const QString&, const QString&>(&Core::Squawk::removeContact),
&w, qOverload<const QString&, const QString&>(&Squawk::removeContact));
QObject::connect(squawk, qOverload<const QString&, const QString&, const QString&>(&Core::Squawk::removeContact),
&w, qOverload<const QString&, const QString&, const QString&>(&Squawk::removeContact));
QObject::connect(squawk, &Core::Squawk::changeContact, &w, &Squawk::changeContact);
QObject::connect(squawk, &Core::Squawk::addPresence, &w, &Squawk::addPresence);
QObject::connect(squawk, &Core::Squawk::removePresence, &w, &Squawk::removePresence);
QObject::connect(squawk, &Core::Squawk::stateChanged, &w, &Squawk::stateChanged);
QObject::connect(squawk, &Core::Squawk::accountMessage, &w, &Squawk::accountMessage);
QObject::connect(squawk, &Core::Squawk::changeMessage, &w, &Squawk::changeMessage);
QObject::connect(squawk, &Core::Squawk::responseArchive, &w, &Squawk::responseArchive);
QObject::connect(squawk, &Core::Squawk::addRoom, &w, &Squawk::addRoom);
QObject::connect(squawk, &Core::Squawk::changeRoom, &w, &Squawk::changeRoom);
QObject::connect(squawk, &Core::Squawk::removeRoom, &w, &Squawk::removeRoom);
QObject::connect(squawk, &Core::Squawk::addRoomParticipant, &w, &Squawk::addRoomParticipant);
QObject::connect(squawk, &Core::Squawk::changeRoomParticipant, &w, &Squawk::changeRoomParticipant);
QObject::connect(squawk, &Core::Squawk::removeRoomParticipant, &w, &Squawk::removeRoomParticipant);
QObject::connect(squawk, &Core::Squawk::fileDownloadComplete, &w, &Squawk::fileDownloadComplete);
QObject::connect(squawk, &Core::Squawk::fileUploadComplete, &w, &Squawk::fileUploadComplete);
QObject::connect(squawk, &Core::Squawk::fileProgress, &w, &Squawk::fileProgress);
QObject::connect(squawk, &Core::Squawk::fileError, &w, &Squawk::fileError);
QObject::connect(squawk, &Core::Squawk::responseVCard, &w, &Squawk::responseVCard);
QObject::connect(squawk, &Core::Squawk::requestPassword, &w, &Squawk::requestPassword);
QObject::connect(squawk, &Core::Squawk::ready, &w, &Squawk::readSettings);
coreThread->start();
int result = app.exec();
if (coreThread->isRunning()) {
//coreThread->wait();
//todo if I uncomment that, the app will no quit if it has reconnected at least once
//it feels like a symptom of something badly desinged in the core coreThread
//need to investigate;
}
return result;
}

View File

@ -30,7 +30,7 @@
#include <set> #include <set>
#include "urlstorage.h" #include "storage/urlstorage.h"
#include "shared/pathcheck.h" #include "shared/pathcheck.h"
namespace Core { namespace Core {

View File

@ -124,15 +124,19 @@ void Core::RosterItem::nextRequest()
if (requestedCount != -1) { if (requestedCount != -1) {
bool last = false; bool last = false;
if (archiveState == beginning || archiveState == complete) { if (archiveState == beginning || archiveState == complete) {
QString firstId = archive->oldestId(); try {
if (responseCache.size() == 0) { QString firstId = archive->oldestId();
if (requestedBefore == firstId) { if (responseCache.size() == 0) {
last = true; if (requestedBefore == firstId) {
} last = true;
} else { }
if (responseCache.front().getId() == firstId) { } else {
last = true; if (responseCache.front().getId() == firstId) {
last = true;
}
} }
} catch (const Archive::Empty& e) {
last = true;
} }
} else if (archiveState == empty && responseCache.size() == 0) { } else if (archiveState == empty && responseCache.size() == 0) {
last = true; last = true;
@ -171,8 +175,12 @@ void Core::RosterItem::performRequest(int count, const QString& before)
requestCache.emplace_back(requestedCount, before); requestCache.emplace_back(requestedCount, before);
requestedCount = -1; requestedCount = -1;
} }
Shared::Message msg = archive->newest(); try {
emit needHistory("", getId(msg), msg.getTime()); Shared::Message msg = archive->newest();
emit needHistory("", getId(msg), msg.getTime());
} catch (const Archive::Empty& e) { //this can happen when the only message in archive is not server stored (error, for example)
emit needHistory(before, "");
}
} }
break; break;
case end: case end:

View File

@ -34,7 +34,8 @@
#include "shared/enums.h" #include "shared/enums.h"
#include "shared/message.h" #include "shared/message.h"
#include "shared/vcard.h" #include "shared/vcard.h"
#include "archive.h" #include "storage/archive.h"
#include "adapterfunctions.h"
namespace Core { namespace Core {

View File

@ -26,8 +26,8 @@ Core::Squawk::Squawk(QObject* parent):
QObject(parent), QObject(parent),
accounts(), accounts(),
amap(), amap(),
state(Shared::Availability::offline),
network(), network(),
waitingForAccounts(0),
isInitialized(false) isInitialized(false)
#ifdef WITH_KWALLET #ifdef WITH_KWALLET
,kwallet() ,kwallet()
@ -42,7 +42,7 @@ Core::Squawk::Squawk(QObject* parent):
if (kwallet.supportState() == PSE::KWallet::success) { if (kwallet.supportState() == PSE::KWallet::success) {
connect(&kwallet, &PSE::KWallet::opened, this, &Squawk::onWalletOpened); connect(&kwallet, &PSE::KWallet::opened, this, &Squawk::onWalletOpened);
connect(&kwallet, &PSE::KWallet::rejectPassword, this, &Squawk::onWalletRejectPassword); connect(&kwallet, &PSE::KWallet::rejectPassword, this, &Squawk::onWalletRejectPassword);
connect(&kwallet, &PSE::KWallet::responsePassword, this, &Squawk::onWalletResponsePassword); connect(&kwallet, &PSE::KWallet::responsePassword, this, &Squawk::responsePassword);
Shared::Global::setSupported("KWallet", true); Shared::Global::setSupported("KWallet", true);
} }
@ -97,6 +97,7 @@ void Core::Squawk::stop()
settings.setValue("password", password); settings.setValue("password", password);
settings.setValue("resource", acc->getResource()); settings.setValue("resource", acc->getResource());
settings.setValue("passwordType", static_cast<int>(ap)); settings.setValue("passwordType", static_cast<int>(ap));
settings.setValue("active", acc->getActive());
} }
settings.endArray(); settings.endArray();
settings.endGroup(); settings.endGroup();
@ -124,8 +125,9 @@ void Core::Squawk::newAccountRequest(const QMap<QString, QVariant>& map)
QString password = map.value("password").toString(); QString password = map.value("password").toString();
QString resource = map.value("resource").toString(); QString resource = map.value("resource").toString();
int passwordType = map.value("passwordType").toInt(); int passwordType = map.value("passwordType").toInt();
bool active = map.value("active").toBool();
addAccount(login, server, password, name, resource, Shared::Global::fromInt<Shared::AccountPassword>(passwordType)); addAccount(login, server, password, name, resource, active, Shared::Global::fromInt<Shared::AccountPassword>(passwordType));
} }
void Core::Squawk::addAccount( void Core::Squawk::addAccount(
@ -133,13 +135,15 @@ void Core::Squawk::addAccount(
const QString& server, const QString& server,
const QString& password, const QString& password,
const QString& name, const QString& name,
const QString& resource, const QString& resource,
Shared::AccountPassword passwordType bool active,
) Shared::AccountPassword passwordType)
{ {
QSettings settings; if (amap.count(name) > 0) {
qDebug() << "An attempt to add account" << name << "but an account with such name already exist, ignoring";
Account* acc = new Account(login, server, password, name, &network); return;
}
Account* acc = new Account(login, server, password, name, active, &network);
acc->setResource(resource); acc->setResource(resource);
acc->setPasswordType(passwordType); acc->setPasswordType(passwordType);
accounts.push_back(acc); accounts.push_back(acc);
@ -148,6 +152,8 @@ void Core::Squawk::addAccount(
connect(acc, &Account::connectionStateChanged, this, &Squawk::onAccountConnectionStateChanged); connect(acc, &Account::connectionStateChanged, this, &Squawk::onAccountConnectionStateChanged);
connect(acc, &Account::changed, this, &Squawk::onAccountChanged); connect(acc, &Account::changed, this, &Squawk::onAccountChanged);
connect(acc, &Account::error, this, &Squawk::onAccountError); connect(acc, &Account::error, this, &Squawk::onAccountError);
connect(acc, &Account::needPassword, this, &Squawk::onAccountNeedPassword);
connect(acc, &Account::availabilityChanged, this, &Squawk::onAccountAvailabilityChanged); connect(acc, &Account::availabilityChanged, this, &Squawk::onAccountAvailabilityChanged);
connect(acc, &Account::addContact, this, &Squawk::onAccountAddContact); connect(acc, &Account::addContact, this, &Squawk::onAccountAddContact);
connect(acc, &Account::addGroup, this, &Squawk::onAccountAddGroup); connect(acc, &Account::addGroup, this, &Squawk::onAccountAddGroup);
@ -185,20 +191,44 @@ void Core::Squawk::addAccount(
{"offline", QVariant::fromValue(Shared::Availability::offline)}, {"offline", QVariant::fromValue(Shared::Availability::offline)},
{"error", ""}, {"error", ""},
{"avatarPath", acc->getAvatarPath()}, {"avatarPath", acc->getAvatarPath()},
{"passwordType", QVariant::fromValue(passwordType)} {"passwordType", QVariant::fromValue(passwordType)},
{"active", active}
}; };
emit newAccount(map); emit newAccount(map);
switch (passwordType) {
case Shared::AccountPassword::alwaysAsk:
case Shared::AccountPassword::kwallet:
if (password == "") {
acc->invalidatePassword();
break;
}
default:
break;
}
if (state != Shared::Availability::offline) {
acc->setAvailability(state);
if (acc->getActive()) {
acc->connect();
}
}
} }
void Core::Squawk::changeState(Shared::Availability p_state) void Core::Squawk::changeState(Shared::Availability p_state)
{ {
if (state != p_state) { if (state != p_state) {
for (std::deque<Account*>::iterator itr = accounts.begin(), end = accounts.end(); itr != end; ++itr) {
Account* acc = *itr;
acc->setAvailability(p_state);
if (state == Shared::Availability::offline && acc->getActive()) {
acc->connect();
}
}
state = p_state; state = p_state;
}
emit stateChanged(p_state);
for (std::deque<Account*>::iterator itr = accounts.begin(), end = accounts.end(); itr != end; ++itr) {
(*itr)->setAvailability(state);
} }
} }
@ -209,7 +239,10 @@ void Core::Squawk::connectAccount(const QString& account)
qDebug("An attempt to connect non existing account, skipping"); qDebug("An attempt to connect non existing account, skipping");
return; return;
} }
itr->second->connect(); itr->second->setActive(true);
if (state != Shared::Availability::offline) {
itr->second->connect();
}
} }
void Core::Squawk::disconnectAccount(const QString& account) void Core::Squawk::disconnectAccount(const QString& account)
@ -220,14 +253,18 @@ void Core::Squawk::disconnectAccount(const QString& account)
return; return;
} }
itr->second->setActive(false);
itr->second->disconnect(); itr->second->disconnect();
} }
void Core::Squawk::onAccountConnectionStateChanged(Shared::ConnectionState p_state) void Core::Squawk::onAccountConnectionStateChanged(Shared::ConnectionState p_state)
{ {
Account* acc = static_cast<Account*>(sender()); Account* acc = static_cast<Account*>(sender());
emit changeAccount(acc->getName(), {{"state", QVariant::fromValue(p_state)}}); emit changeAccount(acc->getName(), {
{"state", QVariant::fromValue(p_state)},
{"error", ""}
});
#ifdef WITH_KWALLET #ifdef WITH_KWALLET
if (p_state == Shared::ConnectionState::connected) { if (p_state == Shared::ConnectionState::connected) {
if (acc->getPasswordType() == Shared::AccountPassword::kwallet && kwallet.supportState() == PSE::KWallet::success) { if (acc->getPasswordType() == Shared::AccountPassword::kwallet && kwallet.supportState() == PSE::KWallet::success) {
@ -235,33 +272,6 @@ void Core::Squawk::onAccountConnectionStateChanged(Shared::ConnectionState p_sta
} }
} }
#endif #endif
Accounts::const_iterator itr = accounts.begin();
bool es = true;
bool ea = true;
Shared::ConnectionState cs = (*itr)->getState();
Shared::Availability av = (*itr)->getAvailability();
itr++;
for (Accounts::const_iterator end = accounts.end(); itr != end; itr++) {
Account* item = *itr;
if (item->getState() != cs) {
es = false;
}
if (item->getAvailability() != av) {
ea = false;
}
}
if (es) {
if (cs == Shared::ConnectionState::disconnected) {
state = Shared::Availability::offline;
emit stateChanged(state);
} else if (ea) {
state = av;
emit stateChanged(state);
}
}
} }
void Core::Squawk::onAccountAddContact(const QString& jid, const QString& group, const QMap<QString, QVariant>& data) void Core::Squawk::onAccountAddContact(const QString& jid, const QString& group, const QMap<QString, QVariant>& data)
@ -391,6 +401,7 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMap<QString,
Shared::ConnectionState st = acc->getState(); Shared::ConnectionState st = acc->getState();
QMap<QString, QVariant>::const_iterator mItr; QMap<QString, QVariant>::const_iterator mItr;
bool needToReconnect = false; bool needToReconnect = false;
bool wentReconnecting = false;
mItr = map.find("login"); mItr = map.find("login");
if (mItr != map.end()) { if (mItr != map.end()) {
@ -416,8 +427,16 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMap<QString,
} }
} }
if (needToReconnect && st != Shared::ConnectionState::disconnected) { bool activeChanged = false;
acc->reconnect(); mItr = map.find("active");
if (mItr == map.end() || mItr->toBool() == acc->getActive()) {
if (needToReconnect && st != Shared::ConnectionState::disconnected) {
acc->reconnect();
wentReconnecting = true;
}
} else {
acc->setActive(mItr->toBool());
activeChanged = true;
} }
mItr = map.find("login"); mItr = map.find("login");
@ -454,6 +473,14 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMap<QString,
} }
#endif #endif
if (state != Shared::Availability::offline) {
if (activeChanged && acc->getActive()) {
acc->connect();
} else if (!wentReconnecting && acc->getActive() && acc->getLastError() == Account::Error::authentication) {
acc->connect();
}
}
emit changeAccount(name, map); emit changeAccount(name, map);
} }
@ -461,6 +488,10 @@ void Core::Squawk::onAccountError(const QString& text)
{ {
Account* acc = static_cast<Account*>(sender()); Account* acc = static_cast<Account*>(sender());
emit changeAccount(acc->getName(), {{"error", text}}); emit changeAccount(acc->getName(), {{"error", text}});
if (acc->getLastError() == Account::Error::authentication) {
emit requestPassword(acc->getName(), true);
}
} }
void Core::Squawk::removeAccountRequest(const QString& name) void Core::Squawk::removeAccountRequest(const QString& name)
@ -675,6 +706,70 @@ void Core::Squawk::uploadVCard(const QString& account, const Shared::VCard& card
itr->second->uploadVCard(card); itr->second->uploadVCard(card);
} }
void Core::Squawk::readSettings()
{
QSettings settings;
settings.beginGroup("core");
int size = settings.beginReadArray("accounts");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
Shared::AccountPassword passwordType =
Shared::Global::fromInt<Shared::AccountPassword>(
settings.value("passwordType", static_cast<int>(Shared::AccountPassword::plain)).toInt()
);
QString password = settings.value("password", "").toString();
if (passwordType == Shared::AccountPassword::jammed) {
SimpleCrypt crypto(passwordHash);
password = crypto.decryptToString(password);
}
addAccount(
settings.value("login").toString(),
settings.value("server").toString(),
password,
settings.value("name").toString(),
settings.value("resource").toString(),
settings.value("active").toBool(),
passwordType
);
}
settings.endArray();
settings.endGroup();
qDebug() << "Squawk core is ready";
emit ready();
}
void Core::Squawk::onAccountNeedPassword()
{
Account* acc = static_cast<Account*>(sender());
switch (acc->getPasswordType()) {
case Shared::AccountPassword::alwaysAsk:
emit requestPassword(acc->getName(), false);
break;
case Shared::AccountPassword::kwallet: {
#ifdef WITH_KWALLET
if (kwallet.supportState() == PSE::KWallet::success) {
kwallet.requestReadPassword(acc->getName());
} else {
#endif
emit requestPassword(acc->getName(), false);
#ifdef WITH_KWALLET
}
#endif
break;
}
default:
break; //should never happen;
}
}
void Core::Squawk::onWalletRejectPassword(const QString& login)
{
emit requestPassword(login, false);
}
void Core::Squawk::responsePassword(const QString& account, const QString& password) void Core::Squawk::responsePassword(const QString& account, const QString& password)
{ {
AccountsMap::const_iterator itr = amap.find(account); AccountsMap::const_iterator itr = amap.find(account);
@ -682,96 +777,12 @@ void Core::Squawk::responsePassword(const QString& account, const QString& passw
qDebug() << "An attempt to set password to non existing account" << account << ", skipping"; qDebug() << "An attempt to set password to non existing account" << account << ", skipping";
return; return;
} }
itr->second->setPassword(password); Account* acc = itr->second;
acc->setPassword(password);
emit changeAccount(account, {{"password", password}}); emit changeAccount(account, {{"password", password}});
accountReady(); if (state != Shared::Availability::offline && acc->getActive()) {
} acc->connect();
void Core::Squawk::readSettings()
{
QSettings settings;
settings.beginGroup("core");
int size = settings.beginReadArray("accounts");
waitingForAccounts = size;
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
parseAccount(
settings.value("login").toString(),
settings.value("server").toString(),
settings.value("password", "").toString(),
settings.value("name").toString(),
settings.value("resource").toString(),
Shared::Global::fromInt<Shared::AccountPassword>(settings.value("passwordType", static_cast<int>(Shared::AccountPassword::plain)).toInt())
);
} }
settings.endArray();
settings.endGroup();
}
void Core::Squawk::accountReady()
{
--waitingForAccounts;
if (waitingForAccounts == 0) {
emit ready();
}
}
void Core::Squawk::parseAccount(
const QString& login,
const QString& server,
const QString& password,
const QString& name,
const QString& resource,
Shared::AccountPassword passwordType
)
{
switch (passwordType) {
case Shared::AccountPassword::plain:
addAccount(login, server, password, name, resource, passwordType);
accountReady();
break;
case Shared::AccountPassword::jammed: {
SimpleCrypt crypto(passwordHash);
QString decrypted = crypto.decryptToString(password);
addAccount(login, server, decrypted, name, resource, passwordType);
accountReady();
}
break;
case Shared::AccountPassword::alwaysAsk:
addAccount(login, server, QString(), name, resource, passwordType);
emit requestPassword(name);
break;
case Shared::AccountPassword::kwallet: {
addAccount(login, server, QString(), name, resource, passwordType);
#ifdef WITH_KWALLET
if (kwallet.supportState() == PSE::KWallet::success) {
kwallet.requestReadPassword(name);
} else {
#endif
emit requestPassword(name);
#ifdef WITH_KWALLET
}
#endif
}
}
}
void Core::Squawk::onWalletRejectPassword(const QString& login)
{
emit requestPassword(login);
}
void Core::Squawk::onWalletResponsePassword(const QString& login, const QString& password)
{
AccountsMap::const_iterator itr = amap.find(login);
if (itr == amap.end()) {
qDebug() << "An attempt to set password to non existing account" << login << ", skipping";
return;
}
itr->second->setPassword(password);
emit changeAccount(login, {{"password", password}});
accountReady();
} }
void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText) void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText)

View File

@ -86,7 +86,7 @@ signals:
void responseVCard(const QString& jid, const Shared::VCard& card); void responseVCard(const QString& jid, const Shared::VCard& card);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void requestPassword(const QString& account); void requestPassword(const QString& account, bool authernticationError);
public slots: public slots:
void start(); void start();
@ -134,7 +134,6 @@ private:
AccountsMap amap; AccountsMap amap;
Shared::Availability state; Shared::Availability state;
NetworkAccess network; NetworkAccess network;
uint8_t waitingForAccounts;
bool isInitialized; bool isInitialized;
#ifdef WITH_KWALLET #ifdef WITH_KWALLET
@ -148,6 +147,7 @@ private slots:
const QString& password, const QString& password,
const QString& name, const QString& name,
const QString& resource, const QString& resource,
bool active,
Shared::AccountPassword passwordType Shared::AccountPassword passwordType
); );
@ -172,22 +172,22 @@ private slots:
void onAccountChangeRoomPresence(const QString& jid, const QString& nick, const QMap<QString, QVariant>& data); void onAccountChangeRoomPresence(const QString& jid, const QString& nick, const QMap<QString, QVariant>& data);
void onAccountRemoveRoomPresence(const QString& jid, const QString& nick); void onAccountRemoveRoomPresence(const QString& jid, const QString& nick);
void onAccountChangeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data); void onAccountChangeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void onAccountNeedPassword();
void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText); void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText);
void onWalletOpened(bool success); void onWalletOpened(bool success);
void onWalletResponsePassword(const QString& login, const QString& password);
void onWalletRejectPassword(const QString& login); void onWalletRejectPassword(const QString& login);
private: private:
void readSettings(); void readSettings();
void accountReady();
void parseAccount( void parseAccount(
const QString& login, const QString& login,
const QString& server, const QString& server,
const QString& password, const QString& password,
const QString& name, const QString& name,
const QString& resource, const QString& resource,
bool active,
Shared::AccountPassword passwordType Shared::AccountPassword passwordType
); );

View File

@ -0,0 +1,8 @@
target_sources(squawk PRIVATE
archive.cpp
archive.h
storage.cpp
storage.h
urlstorage.cpp
urlstorage.h
)

7
main/CMakeLists.txt Normal file
View File

@ -0,0 +1,7 @@
target_sources(squawk PRIVATE
main.cpp
application.cpp
application.h
dialogqueue.cpp
dialogqueue.h
)

566
main/application.cpp Normal file
View File

@ -0,0 +1,566 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#include "application.h"
Application::Application(Core::Squawk* p_core):
QObject(),
availability(Shared::Availability::offline),
core(p_core),
squawk(nullptr),
notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"),
roster(),
conversations(),
dialogueQueue(roster),
nowQuitting(false),
destroyingSquawk(false),
storage()
{
connect(&roster, &Models::Roster::unnoticedMessage, this, &Application::notify);
connect(&roster, &Models::Roster::unreadMessagesCountChanged, this, &Application::unreadMessagesCountChanged);
//connecting myself to the backed
connect(this, &Application::changeState, core, &Core::Squawk::changeState);
connect(this, &Application::setRoomJoined, core, &Core::Squawk::setRoomJoined);
connect(this, &Application::setRoomAutoJoin, core, &Core::Squawk::setRoomAutoJoin);
connect(this, &Application::subscribeContact, core, &Core::Squawk::subscribeContact);
connect(this, &Application::unsubscribeContact, core, &Core::Squawk::unsubscribeContact);
connect(this, &Application::replaceMessage, core, &Core::Squawk::replaceMessage);
connect(this, &Application::sendMessage, core, &Core::Squawk::sendMessage);
connect(this, &Application::resendMessage, core, &Core::Squawk::resendMessage);
connect(&roster, &Models::Roster::requestArchive,
std::bind(&Core::Squawk::requestArchive, core, std::placeholders::_1, std::placeholders::_2, 20, std::placeholders::_3));
connect(&dialogueQueue, &DialogQueue::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
connect(&dialogueQueue, &DialogQueue::responsePassword, core, &Core::Squawk::responsePassword);
connect(&dialogueQueue, &DialogQueue::disconnectAccount, core, &Core::Squawk::disconnectAccount);
connect(&roster, &Models::Roster::fileDownloadRequest, core, &Core::Squawk::fileDownloadRequest);
connect(&roster, &Models::Roster::localPathInvalid, core, &Core::Squawk::onLocalPathInvalid);
//coonecting backend to myself
connect(core, &Core::Squawk::stateChanged, this, &Application::stateChanged);
connect(core, &Core::Squawk::accountMessage, &roster, &Models::Roster::addMessage);
connect(core, &Core::Squawk::responseArchive, &roster, &Models::Roster::responseArchive);
connect(core, &Core::Squawk::changeMessage, &roster, &Models::Roster::changeMessage);
connect(core, &Core::Squawk::newAccount, &roster, &Models::Roster::addAccount);
connect(core, &Core::Squawk::changeAccount, this, &Application::changeAccount);
connect(core, &Core::Squawk::removeAccount, this, &Application::removeAccount);
connect(core, &Core::Squawk::addContact, this, &Application::addContact);
connect(core, &Core::Squawk::addGroup, this, &Application::addGroup);
connect(core, &Core::Squawk::removeGroup, &roster, &Models::Roster::removeGroup);
connect(core, qOverload<const QString&, const QString&>(&Core::Squawk::removeContact),
&roster, qOverload<const QString&, const QString&>(&Models::Roster::removeContact));
connect(core, qOverload<const QString&, const QString&, const QString&>(&Core::Squawk::removeContact),
&roster, qOverload<const QString&, const QString&, const QString&>(&Models::Roster::removeContact));
connect(core, &Core::Squawk::changeContact, &roster, &Models::Roster::changeContact);
connect(core, &Core::Squawk::addPresence, &roster, &Models::Roster::addPresence);
connect(core, &Core::Squawk::removePresence, &roster, &Models::Roster::removePresence);
connect(core, &Core::Squawk::addRoom, &roster, &Models::Roster::addRoom);
connect(core, &Core::Squawk::changeRoom, &roster, &Models::Roster::changeRoom);
connect(core, &Core::Squawk::removeRoom, &roster, &Models::Roster::removeRoom);
connect(core, &Core::Squawk::addRoomParticipant, &roster, &Models::Roster::addRoomParticipant);
connect(core, &Core::Squawk::changeRoomParticipant, &roster, &Models::Roster::changeRoomParticipant);
connect(core, &Core::Squawk::removeRoomParticipant, &roster, &Models::Roster::removeRoomParticipant);
connect(core, &Core::Squawk::fileDownloadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, false));
connect(core, &Core::Squawk::fileUploadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, true));
connect(core, &Core::Squawk::fileProgress, &roster, &Models::Roster::fileProgress);
connect(core, &Core::Squawk::fileError, &roster, &Models::Roster::fileError);
connect(core, &Core::Squawk::requestPassword, this, &Application::requestPassword);
connect(core, &Core::Squawk::ready, this, &Application::readSettings);
QDBusConnection sys = QDBusConnection::sessionBus();
sys.connect(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"NotificationClosed",
this,
SLOT(onNotificationClosed(quint32, quint32))
);
sys.connect(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"ActionInvoked",
this,
SLOT(onNotificationInvoked(quint32, const QString&))
);
}
Application::~Application() {}
void Application::quit()
{
if (!nowQuitting) {
nowQuitting = true;
emit quitting();
writeSettings();
unreadMessagesCountChanged(0); //this notification persist in the desktop, for now I'll zero it on quit not to confuse people
for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) {
disconnect(itr->second, &Conversation::destroyed, this, &Application::onConversationClosed);
itr->second->close();
}
conversations.clear();
dialogueQueue.quit();
if (squawk != nullptr) {
squawk->close();
}
if (!destroyingSquawk) {
checkForTheLastWindow();
}
}
}
void Application::checkForTheLastWindow()
{
if (QApplication::topLevelWidgets().size() > 0) {
emit readyToQuit();
} else {
connect(qApp, &QApplication::lastWindowClosed, this, &Application::readyToQuit);
}
}
void Application::createMainWindow()
{
if (squawk == nullptr) {
squawk = new Squawk(roster);
connect(squawk, &Squawk::notify, this, &Application::notify);
connect(squawk, &Squawk::changeSubscription, this, &Application::changeSubscription);
connect(squawk, &Squawk::openedConversation, this, &Application::onSquawkOpenedConversation);
connect(squawk, &Squawk::openConversation, this, &Application::openConversation);
connect(squawk, &Squawk::changeState, this, &Application::setState);
connect(squawk, &Squawk::closing, this, &Application::onSquawkClosing);
connect(squawk, &Squawk::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
connect(squawk, &Squawk::newAccountRequest, core, &Core::Squawk::newAccountRequest);
connect(squawk, &Squawk::removeAccountRequest, core, &Core::Squawk::removeAccountRequest);
connect(squawk, &Squawk::connectAccount, core, &Core::Squawk::connectAccount);
connect(squawk, &Squawk::disconnectAccount, core, &Core::Squawk::disconnectAccount);
connect(squawk, &Squawk::addContactRequest, core, &Core::Squawk::addContactRequest);
connect(squawk, &Squawk::removeContactRequest, core, &Core::Squawk::removeContactRequest);
connect(squawk, &Squawk::removeRoomRequest, core, &Core::Squawk::removeRoomRequest);
connect(squawk, &Squawk::addRoomRequest, core, &Core::Squawk::addRoomRequest);
connect(squawk, &Squawk::addContactToGroupRequest, core, &Core::Squawk::addContactToGroupRequest);
connect(squawk, &Squawk::removeContactFromGroupRequest, core, &Core::Squawk::removeContactFromGroupRequest);
connect(squawk, &Squawk::renameContactRequest, core, &Core::Squawk::renameContactRequest);
connect(squawk, &Squawk::requestVCard, core, &Core::Squawk::requestVCard);
connect(squawk, &Squawk::uploadVCard, core, &Core::Squawk::uploadVCard);
connect(squawk, &Squawk::changeDownloadsPath, core, &Core::Squawk::changeDownloadsPath);
connect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard);
dialogueQueue.setParentWidnow(squawk);
squawk->stateChanged(availability);
squawk->show();
}
}
void Application::onSquawkClosing()
{
dialogueQueue.setParentWidnow(nullptr);
if (!nowQuitting) {
disconnect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard);
}
destroyingSquawk = true;
squawk->deleteLater();
squawk = nullptr;
//for now
quit();
}
void Application::onSquawkDestroyed() {
destroyingSquawk = false;
if (nowQuitting) {
checkForTheLastWindow();
}
}
void Application::notify(const QString& account, const Shared::Message& msg)
{
QString jid = msg.getPenPalJid();
QString name = QString(roster.getContactName(account, jid));
QString path = QString(roster.getContactIconPath(account, jid, msg.getPenPalResource()));
QVariantList args;
args << QString();
uint32_t notificationId = qHash(msg.getId());
args << notificationId;
if (path.size() > 0) {
args << path;
} else {
args << QString("mail-message"); //TODO should here better be unknown user icon?
}
if (msg.getType() == Shared::Message::groupChat) {
args << msg.getFromResource() + tr(" from ") + name;
} else {
args << name;
}
QString body(msg.getBody());
QString oob(msg.getOutOfBandUrl());
if (body == oob) {
body = tr("Attached file");
}
args << body;
args << QStringList({
"markAsRead", tr("Mark as Read"),
"openConversation", tr("Open conversation")
});
args << QVariantMap({
{"desktop-entry", qApp->desktopFileName()},
{"category", QString("message")},
{"urgency", 1},
// {"sound-file", "/path/to/macaw/squawk"},
{"sound-name", QString("message-new-instant")}
});
args << -1;
notifications.callWithArgumentList(QDBus::AutoDetect, "Notify", args);
storage.insert(std::make_pair(notificationId, std::make_pair(Models::Roster::ElId(account, name), msg.getId())));
if (squawk != nullptr) {
QApplication::alert(squawk);
}
}
void Application::onNotificationClosed(quint32 id, quint32 reason)
{
Notifications::const_iterator itr = storage.find(id);
if (itr != storage.end()) {
if (reason == 2) { //dissmissed by user (https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html)
//TODO may ba also mark as read?
}
if (reason != 1) { //just expired, can be activated again from history, so no removing for now
storage.erase(id);
qDebug() << "Notification" << id << "was closed";
}
}
}
void Application::onNotificationInvoked(quint32 id, const QString& action)
{
qDebug() << "Notification" << id << action << "request";
Notifications::const_iterator itr = storage.find(id);
if (itr != storage.end()) {
if (action == "markAsRead") {
roster.markMessageAsRead(itr->second.first, itr->second.second);
} else if (action == "openConversation") {
focusConversation(itr->second.first, "", itr->second.second);
}
}
}
void Application::unreadMessagesCountChanged(int count)
{
QDBusMessage signal = QDBusMessage::createSignal("/", "com.canonical.Unity.LauncherEntry", "Update");
signal << qApp->desktopFileName() + QLatin1String(".desktop");
signal << QVariantMap ({
{"count-visible", count != 0},
{"count", count}
});
QDBusConnection::sessionBus().send(signal);
}
void Application::focusConversation(const Models::Roster::ElId& id, const QString& resource, const QString& messageId)
{
if (squawk != nullptr) {
if (squawk->currentConversationId() != id) {
QModelIndex index = roster.getContactIndex(id.account, id.name, resource);
squawk->select(index);
}
if (squawk->isMinimized()) {
squawk->showNormal();
} else {
squawk->show();
}
squawk->raise();
squawk->activateWindow();
} else {
openConversation(id, resource);
}
//TODO focus messageId;
}
void Application::setState(Shared::Availability p_availability)
{
if (availability != p_availability) {
availability = p_availability;
emit changeState(availability);
}
}
void Application::stateChanged(Shared::Availability state)
{
availability = state;
if (squawk != nullptr) {
squawk->stateChanged(state);
}
}
void Application::readSettings()
{
QSettings settings;
settings.beginGroup("ui");
int avail;
if (settings.contains("availability")) {
avail = settings.value("availability").toInt();
} else {
avail = static_cast<int>(Shared::Availability::online);
}
settings.endGroup();
setState(Shared::Global::fromInt<Shared::Availability>(avail));
createMainWindow();
}
void Application::writeSettings()
{
QSettings settings;
settings.setValue("availability", static_cast<int>(availability));
}
void Application::requestPassword(const QString& account, bool authenticationError) {
if (authenticationError) {
dialogueQueue.addAction(account, DialogQueue::askCredentials);
} else {
dialogueQueue.addAction(account, DialogQueue::askPassword);
}
}
void Application::onConversationClosed()
{
Conversation* conv = static_cast<Conversation*>(sender());
Models::Roster::ElId id(conv->getAccount(), conv->getJid());
Conversations::const_iterator itr = conversations.find(id);
if (itr != conversations.end()) {
conversations.erase(itr);
}
if (conv->isMuc) {
Room* room = static_cast<Room*>(conv);
if (!room->autoJoined()) {
emit setRoomJoined(id.account, id.name, false);
}
}
}
void Application::changeSubscription(const Models::Roster::ElId& id, bool subscribe)
{
Models::Item::Type type = roster.getContactType(id);
switch (type) {
case Models::Item::contact:
if (subscribe) {
emit subscribeContact(id.account, id.name, "");
} else {
emit unsubscribeContact(id.account, id.name, "");
}
break;
case Models::Item::room:
setRoomAutoJoin(id.account, id.name, subscribe);
if (!isConverstationOpened(id)) {
emit setRoomJoined(id.account, id.name, subscribe);
}
break;
default:
break;
}
}
void Application::subscribeConversation(Conversation* conv)
{
connect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
connect(conv, &Conversation::sendMessage, this, &Application::onConversationMessage);
connect(conv, &Conversation::replaceMessage, this, &Application::onConversationReplaceMessage);
connect(conv, &Conversation::resendMessage, this, &Application::onConversationResend);
connect(conv, &Conversation::notifyableMessage, this, &Application::notify);
}
void Application::openConversation(const Models::Roster::ElId& id, const QString& resource)
{
Conversations::const_iterator itr = conversations.find(id);
Models::Account* acc = roster.getAccount(id.account);
Conversation* conv = nullptr;
bool created = false;
if (itr != conversations.end()) {
conv = itr->second;
} else {
Models::Element* el = roster.getElement(id);
if (el != nullptr) {
if (el->type == Models::Item::room) {
created = true;
Models::Room* room = static_cast<Models::Room*>(el);
conv = new Room(acc, room);
if (!room->getJoined()) {
emit setRoomJoined(id.account, id.name, true);
}
} else if (el->type == Models::Item::contact) {
created = true;
conv = new Chat(acc, static_cast<Models::Contact*>(el));
}
}
}
if (conv != nullptr) {
if (created) {
conv->setAttribute(Qt::WA_DeleteOnClose);
subscribeConversation(conv);
conversations.insert(std::make_pair(id, conv));
}
conv->show();
conv->raise();
conv->activateWindow();
if (resource.size() > 0) {
conv->setPalResource(resource);
}
}
}
void Application::onConversationMessage(const Shared::Message& msg)
{
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
roster.addMessage(acc, msg);
emit sendMessage(acc, msg);
}
void Application::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg)
{
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
roster.changeMessage(acc, msg.getPenPalJid(), originalId, {
{"state", static_cast<uint>(Shared::Message::State::pending)}
});
emit replaceMessage(acc, originalId, msg);
}
void Application::onConversationResend(const QString& id)
{
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
QString jid = conv->getJid();
emit resendMessage(acc, jid, id);
}
void Application::onSquawkOpenedConversation() {
subscribeConversation(squawk->currentConversation);
Models::Roster::ElId id = squawk->currentConversationId();
const Models::Element* el = roster.getElementConst(id);
if (el != nullptr && el->isRoom() && !static_cast<const Models::Room*>(el)->getJoined()) {
emit setRoomJoined(id.account, id.name, true);
}
}
void Application::removeAccount(const QString& account)
{
Conversations::const_iterator itr = conversations.begin();
while (itr != conversations.end()) {
if (itr->first.account == account) {
Conversations::const_iterator lItr = itr;
++itr;
Conversation* conv = lItr->second;
disconnect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
conv->close();
conversations.erase(lItr);
} else {
++itr;
}
}
if (squawk != nullptr && squawk->currentConversationId().account == account) {
squawk->closeCurrentConversation();
}
roster.removeAccount(account);
}
void Application::changeAccount(const QString& account, const QMap<QString, QVariant>& data)
{
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
QString attr = itr.key();
roster.updateAccount(account, attr, *itr);
}
}
void Application::addContact(const QString& account, const QString& jid, const QString& group, const QMap<QString, QVariant>& data)
{
roster.addContact(account, jid, group, data);
if (squawk != nullptr) {
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("roster");
settings.beginGroup(account);
if (settings.value("expanded", false).toBool()) {
QModelIndex ind = roster.getAccountIndex(account);
squawk->expand(ind);
}
settings.endGroup();
settings.endGroup();
settings.endGroup();
}
}
void Application::addGroup(const QString& account, const QString& name)
{
roster.addGroup(account, name);
if (squawk != nullptr) {
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("roster");
settings.beginGroup(account);
if (settings.value("expanded", false).toBool()) {
QModelIndex ind = roster.getAccountIndex(account);
squawk->expand(ind);
if (settings.value(name + "/expanded", false).toBool()) {
squawk->expand(roster.getGroupIndex(account, name));
}
}
settings.endGroup();
settings.endGroup();
settings.endGroup();
}
}
bool Application::isConverstationOpened(const Models::Roster::ElId& id) const {
return (conversations.count(id) > 0) || (squawk != nullptr && squawk->currentConversationId() == id);}

118
main/application.h Normal file
View File

@ -0,0 +1,118 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#ifndef APPLICATION_H
#define APPLICATION_H
#include <map>
#include <QObject>
#include <QDBusInterface>
#include "dialogqueue.h"
#include "core/squawk.h"
#include "ui/squawk.h"
#include "ui/models/roster.h"
#include "ui/widgets/conversation.h"
#include "shared/message.h"
#include "shared/enums.h"
/**
* @todo write docs
*/
class Application : public QObject
{
Q_OBJECT
public:
Application(Core::Squawk* core);
~Application();
bool isConverstationOpened(const Models::Roster::ElId& id) const;
signals:
void sendMessage(const QString& account, const Shared::Message& data);
void replaceMessage(const QString& account, const QString& originalId, const Shared::Message& data);
void resendMessage(const QString& account, const QString& jid, const QString& id);
void changeState(Shared::Availability state);
void setRoomJoined(const QString& account, const QString& jid, bool joined);
void setRoomAutoJoin(const QString& account, const QString& jid, bool joined);
void subscribeContact(const QString& account, const QString& jid, const QString& reason);
void unsubscribeContact(const QString& account, const QString& jid, const QString& reason);
void quitting();
void readyToQuit();
public slots:
void readSettings();
void quit();
protected slots:
void notify(const QString& account, const Shared::Message& msg);
void unreadMessagesCountChanged(int count);
void setState(Shared::Availability availability);
void changeAccount(const QString& account, const QMap<QString, QVariant>& data);
void removeAccount(const QString& account);
void openConversation(const Models::Roster::ElId& id, const QString& resource = "");
void addGroup(const QString& account, const QString& name);
void addContact(const QString& account, const QString& jid, const QString& group, const QMap<QString, QVariant>& data);
void requestPassword(const QString& account, bool authenticationError);
void writeSettings();
private slots:
void onConversationClosed();
void changeSubscription(const Models::Roster::ElId& id, bool subscribe);
void onSquawkOpenedConversation();
void onConversationMessage(const Shared::Message& msg);
void onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg);
void onConversationResend(const QString& id);
void stateChanged(Shared::Availability state);
void onSquawkClosing();
void onSquawkDestroyed();
void onNotificationClosed(quint32 id, quint32 reason);
void onNotificationInvoked(quint32 id, const QString& action);
private:
void createMainWindow();
void subscribeConversation(Conversation* conv);
void checkForTheLastWindow();
void focusConversation(const Models::Roster::ElId& id, const QString& resource = "", const QString& messageId = "");
private:
typedef std::map<Models::Roster::ElId, Conversation*> Conversations;
typedef std::map<uint32_t, std::pair<Models::Roster::ElId, QString>> Notifications;
Shared::Availability availability;
Core::Squawk* core;
Squawk* squawk;
QDBusInterface notifications;
Models::Roster roster;
Conversations conversations;
DialogQueue dialogueQueue;
bool nowQuitting;
bool destroyingSquawk;
Notifications storage;
};
#endif // APPLICATION_H

187
main/dialogqueue.cpp Normal file
View File

@ -0,0 +1,187 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#include "dialogqueue.h"
#include <QDebug>
DialogQueue::DialogQueue(const Models::Roster& p_roster):
QObject(),
currentSource(),
currentAction(none),
queue(),
collection(queue.get<0>()),
sequence(queue.get<1>()),
prompt(nullptr),
parent(nullptr),
roster(p_roster)
{
}
DialogQueue::~DialogQueue()
{
}
void DialogQueue::quit()
{
queue.clear();
if (currentAction != none) {
actionDone();
}
}
void DialogQueue::setParentWidnow(QMainWindow* p_parent)
{
parent = p_parent;
}
bool DialogQueue::addAction(const QString& source, DialogQueue::Action action)
{
if (action == none) {
return false;
}
if (currentAction == none) {
currentAction = action;
currentSource = source;
performNextAction();
return true;
} else {
if (currentAction != action || currentSource != source) {
std::pair<Queue::iterator, bool> result = queue.emplace(source, action);
return result.second;
} else {
return false;
}
}
}
bool DialogQueue::cancelAction(const QString& source, DialogQueue::Action action)
{
if (source == currentSource && action == currentAction) {
actionDone();
return true;
} else {
Collection::iterator itr = collection.find(ActionId{source, action});
if (itr != collection.end()) {
collection.erase(itr);
return true;
} else {
return false;
}
}
}
void DialogQueue::performNextAction()
{
switch (currentAction) {
case none:
actionDone();
break;
case askPassword: {
QInputDialog* dialog = new QInputDialog(parent);
prompt = dialog;
connect(dialog, &QDialog::accepted, this, &DialogQueue::onPropmtAccepted);
connect(dialog, &QDialog::rejected, this, &DialogQueue::onPropmtRejected);
dialog->setInputMode(QInputDialog::TextInput);
dialog->setTextEchoMode(QLineEdit::Password);
dialog->setLabelText(tr("Input the password for account %1").arg(currentSource));
dialog->setWindowTitle(tr("Password for account %1").arg(currentSource));
dialog->setTextValue("");
dialog->exec();
}
break;
case askCredentials: {
CredentialsPrompt* dialog = new CredentialsPrompt(parent);
prompt = dialog;
connect(dialog, &QDialog::accepted, this, &DialogQueue::onPropmtAccepted);
connect(dialog, &QDialog::rejected, this, &DialogQueue::onPropmtRejected);
const Models::Account* acc = roster.getAccountConst(currentSource);
dialog->setAccount(currentSource);
dialog->setLogin(acc->getLogin());
dialog->setPassword(acc->getPassword());
dialog->exec();
}
break;
}
}
void DialogQueue::onPropmtAccepted()
{
switch (currentAction) {
case none:
break;
case askPassword: {
QInputDialog* dialog = static_cast<QInputDialog*>(prompt);
emit responsePassword(currentSource, dialog->textValue());
}
break;
case askCredentials: {
CredentialsPrompt* dialog = static_cast<CredentialsPrompt*>(prompt);
emit modifyAccountRequest(currentSource, {
{"login", dialog->getLogin()},
{"password", dialog->getPassword()}
});
}
break;
}
actionDone();
}
void DialogQueue::onPropmtRejected()
{
switch (currentAction) {
case none:
break;
case askPassword:
case askCredentials:
emit disconnectAccount(currentSource);
break;
}
actionDone();
}
void DialogQueue::actionDone()
{
prompt->deleteLater();
prompt = nullptr;
if (queue.empty()) {
currentAction = none;
currentSource = "";
} else {
Sequence::iterator itr = sequence.begin();
currentAction = itr->action;
currentSource = itr->source;
sequence.erase(itr);
performNextAction();
}
}
DialogQueue::ActionId::ActionId(const QString& p_source, DialogQueue::Action p_action):
source(p_source),
action(p_action) {}
bool DialogQueue::ActionId::operator < (const DialogQueue::ActionId& other) const
{
if (action == other.action) {
return source < other.source;
} else {
return action < other.action;
}
}
DialogQueue::ActionId::ActionId(const DialogQueue::ActionId& other):
source(other.source),
action(other.action) {}

101
main/dialogqueue.h Normal file
View File

@ -0,0 +1,101 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#ifndef DIALOGQUEUE_H
#define DIALOGQUEUE_H
#include <QObject>
#include <QInputDialog>
#include <QMainWindow>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <ui/widgets/accounts/credentialsprompt.h>
#include <ui/models/roster.h>
class DialogQueue : public QObject
{
Q_OBJECT
public:
enum Action {
none,
askPassword,
askCredentials
};
DialogQueue(const Models::Roster& roster);
~DialogQueue();
bool addAction(const QString& source, Action action);
bool cancelAction(const QString& source, Action action);
signals:
void modifyAccountRequest(const QString&, const QMap<QString, QVariant>&);
void responsePassword(const QString& account, const QString& password);
void disconnectAccount(const QString&);
public:
void setParentWidnow(QMainWindow* parent);
void quit();
private:
void performNextAction();
void actionDone();
private slots:
void onPropmtAccepted();
void onPropmtRejected();
private:
QString currentSource;
Action currentAction;
struct ActionId {
public:
ActionId(const QString& p_source, Action p_action);
ActionId(const ActionId& other);
const QString source;
const Action action;
bool operator < (const ActionId& other) const;
};
typedef boost::multi_index_container <
ActionId,
boost::multi_index::indexed_by <
boost::multi_index::ordered_unique <
boost::multi_index::identity <ActionId>
>,
boost::multi_index::sequenced<>
>
> Queue;
typedef Queue::nth_index<0>::type Collection;
typedef Queue::nth_index<1>::type Sequence;
Queue queue;
Collection& collection;
Sequence& sequence;
QDialog* prompt;
QMainWindow* parent;
const Models::Roster& roster;
};
#endif // DIALOGQUEUE_H

140
main/main.cpp Normal file
View File

@ -0,0 +1,140 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "shared/global.h"
#include "shared/messageinfo.h"
#include "shared/pathcheck.h"
#include "main/application.h"
#include "core/signalcatcher.h"
#include "core/squawk.h"
#include <QLibraryInfo>
#include <QSettings>
#include <QStandardPaths>
#include <QTranslator>
#include <QtCore/QObject>
#include <QtCore/QThread>
#include <QtWidgets/QApplication>
#include <QDir>
int main(int argc, char *argv[])
{
qRegisterMetaType<Shared::Message>("Shared::Message");
qRegisterMetaType<Shared::MessageInfo>("Shared::MessageInfo");
qRegisterMetaType<Shared::VCard>("Shared::VCard");
qRegisterMetaType<std::list<Shared::Message>>("std::list<Shared::Message>");
qRegisterMetaType<std::list<Shared::MessageInfo>>("std::list<Shared::MessageInfo>");
qRegisterMetaType<QSet<QString>>("QSet<QString>");
qRegisterMetaType<Shared::ConnectionState>("Shared::ConnectionState");
qRegisterMetaType<Shared::Availability>("Shared::Availability");
QApplication app(argc, argv);
SignalCatcher sc(&app);
QApplication::setApplicationName("squawk");
QApplication::setOrganizationName("macaw.me");
QApplication::setApplicationDisplayName("Squawk");
QApplication::setApplicationVersion("0.2.2");
app.setDesktopFileName("squawk");
QTranslator qtTranslator;
qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
app.installTranslator(&qtTranslator);
QTranslator myappTranslator;
QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
bool found = false;
for (QString share : shares) {
found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n");
if (found) {
break;
}
}
if (!found) {
myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", QCoreApplication::applicationDirPath());
}
app.installTranslator(&myappTranslator);
QIcon icon;
icon.addFile(":images/logo.svg", QSize(16, 16));
icon.addFile(":images/logo.svg", QSize(24, 24));
icon.addFile(":images/logo.svg", QSize(32, 32));
icon.addFile(":images/logo.svg", QSize(48, 48));
icon.addFile(":images/logo.svg", QSize(64, 64));
icon.addFile(":images/logo.svg", QSize(96, 96));
icon.addFile(":images/logo.svg", QSize(128, 128));
icon.addFile(":images/logo.svg", QSize(256, 256));
icon.addFile(":images/logo.svg", QSize(512, 512));
QApplication::setWindowIcon(icon);
new Shared::Global(); //translates enums
QSettings settings;
QVariant vs = settings.value("style");
if (vs.isValid()) {
QString style = vs.toString().toLower();
if (style != "system") {
Shared::Global::setStyle(style);
}
}
if (Shared::Global::supported("colorSchemeTools")) {
QVariant vt = settings.value("theme");
if (vt.isValid()) {
QString theme = vt.toString();
if (theme.toLower() != "system") {
Shared::Global::setTheme(theme);
}
}
}
QString path = Shared::downloadsPathCheck();
if (path.size() > 0) {
settings.setValue("downloadsPath", path);
} else {
qDebug() << "couldn't initialize directory for downloads, quitting";
return -1;
}
Core::Squawk* squawk = new Core::Squawk();
QThread* coreThread = new QThread();
squawk->moveToThread(coreThread);
Application application(squawk);
QObject::connect(&sc, &SignalCatcher::interrupt, &application, &Application::quit);
QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start);
QObject::connect(&application, &Application::quitting, squawk, &Core::Squawk::stop);
//QObject::connect(&app, &QApplication::aboutToQuit, &w, &QMainWindow::close);
QObject::connect(squawk, &Core::Squawk::quit, squawk, &Core::Squawk::deleteLater);
QObject::connect(squawk, &Core::Squawk::destroyed, coreThread, &QThread::quit, Qt::QueuedConnection);
QObject::connect(coreThread, &QThread::finished, &app, &QApplication::quit, Qt::QueuedConnection);
coreThread->start();
int result = app.exec();
if (coreThread->isRunning()) {
//coreThread->wait();
//todo if I uncomment that, the app will not quit if it has reconnected at least once
//it feels like a symptom of something badly desinged in the core thread
//need to investigate;
}
return result;
}

View File

@ -1,6 +1,6 @@
# Maintainer: Yury Gubich <blue@macaw.me> # Maintainer: Yury Gubich <blue@macaw.me>
pkgname=squawk pkgname=squawk
pkgver=0.2.0 pkgver=0.2.2
pkgrel=1 pkgrel=1
pkgdesc="An XMPP desktop messenger, written on pure c++ (qt)" pkgdesc="An XMPP desktop messenger, written on pure c++ (qt)"
arch=('i686' 'x86_64') arch=('i686' 'x86_64')
@ -14,7 +14,7 @@ optdepends=('kwallet: secure password storage (requires rebuild)'
'kio: better show in folder action (requires rebuild)') 'kio: better show in folder action (requires rebuild)')
source=("$pkgname-$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz")
sha256sums=('8e93d3dbe1fc35cfecb7783af409c6a264244d11609b2241d4fe77d43d068419') sha256sums=('e4fa2174a3ba95159cc3b0bac3f00550c9e0ce971c55334e2662696a4543fc7e')
build() { build() {
cd "$srcdir/squawk" cd "$srcdir/squawk"
cmake . -D CMAKE_INSTALL_PREFIX=/usr -D CMAKE_BUILD_TYPE=Release cmake . -D CMAKE_INSTALL_PREFIX=/usr -D CMAKE_BUILD_TYPE=Release
@ -22,5 +22,5 @@ build() {
} }
package() { package() {
cd "$srcdir/squawk" cd "$srcdir/squawk"
DESTDIR="$pkgdir/" cmake --build . --target install DESTDIR="$pkgdir/" cmake --build . --target install
} }

View File

@ -19,6 +19,7 @@
#include "global.h" #include "global.h"
#include "enums.h" #include "enums.h"
#include "ui/models/roster.h"
Shared::Global* Shared::Global::instance = 0; Shared::Global* Shared::Global::instance = 0;
const std::set<QString> Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; const std::set<QString> Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"};

View File

@ -47,7 +47,6 @@ namespace Shared {
class Global { class Global {
Q_DECLARE_TR_FUNCTIONS(Global) Q_DECLARE_TR_FUNCTIONS(Global)
public: public:
struct FileInfo { struct FileInfo {
enum class Preview { enum class Preview {
@ -64,7 +63,7 @@ namespace Shared {
}; };
Global(); Global();
static Global* getInstance(); static Global* getInstance();
static QString getName(Availability av); static QString getName(Availability av);
static QString getName(ConnectionState cs); static QString getName(ConnectionState cs);

View File

@ -40,5 +40,5 @@ QString Shared::processMessageBody(const QString& msg)
{ {
QString processed = msg.toHtmlEscaped(); QString processed = msg.toHtmlEscaped();
processed.replace(urlReg, "<a href=\"\\1\">\\1</a>"); processed.replace(urlReg, "<a href=\"\\1\">\\1</a>");
return "<p style=\"white-space: pre-wrap;\">" + processed + "</p>"; return "<p style=\"white-space: pre-wrap; line-height: 1em;\">" + processed + "</p>";
} }

View File

@ -69,6 +69,12 @@ static const std::vector<QColor> colorPalette = {
QColor(17, 17, 80), QColor(17, 17, 80),
QColor(54, 54, 94) QColor(54, 54, 94)
}; };
enum class Hover {
nothing,
text,
anchor
};
} }
#endif // SHARED_UTILS_H #endif // SHARED_UTILS_H

View File

@ -1,11 +1,12 @@
find_package(Qt5LinguistTools) find_package(Qt5LinguistTools)
set(TS_FILES set(TS_FILES
squawk.en.ts
squawk.ru.ts squawk.ru.ts
squawk.pt_BR.ts squawk.pt_BR.ts
) )
qt5_add_translation(QM_FILES ${TS_FILES}) qt5_add_translation(QM_FILES ${TS_FILES})
add_custom_target(translations ALL DEPENDS ${QM_FILES}) add_custom_target(translations ALL DEPENDS ${QM_FILES})
install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk/l10n)
add_dependencies(${CMAKE_PROJECT_NAME} translations) add_dependencies(${CMAKE_PROJECT_NAME} translations)

1420
translations/squawk.en.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,108 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="pt_BR"> <TS version="2.1" language="pt_BR">
<context>
<name>About</name>
<message>
<source>About Squawk</source>
<translation>Sorbe Squawk</translation>
</message>
<message>
<source>Squawk</source>
<translation>Squawk</translation>
</message>
<message>
<source>About</source>
<translatorcomment>Tab title</translatorcomment>
<translation>Sobre</translation>
</message>
<message>
<source>XMPP (jabber) messenger</source>
<translation>XMPP (jabber) mensageiro</translation>
</message>
<message>
<source>(c) 2019 - 2022, Yury Gubich</source>
<translation>(c) 2019 - 2022, Yury Gubich</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk&quot;&gt;Project site&lt;/a&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk&quot;&gt;Site do projeto&lt;/a&gt;</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md&quot;&gt;License: GNU General Public License version 3&lt;/a&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md&quot;&gt;Licença: GNU General Public License versão 3&lt;/a&gt;</translation>
</message>
<message>
<source>Components</source>
<translation>Componentes</translation>
</message>
<message>
<source>Version</source>
<translation>Versão</translation>
</message>
<message>
<source>0.0.0</source>
<translation>0.0.0</translation>
</message>
<message>
<source>Report Bugs</source>
<translation>Relatório de erros</translation>
</message>
<message>
<source>Please report any bug you find!
To report bugs you can use:</source>
<translation>Por favor reportar qualquer erro que você encontrar!
Para relatar bugs você pode usar:</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk/issues&quot;&gt;Project bug tracker&lt;/&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk/issues&quot;&gt;Rastreador de bugs do projeto&lt;/&gt;</translation>
</message>
<message>
<source>XMPP (&lt;a href=&quot;xmpp:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</source>
<translation>XMPP (&lt;a href=&quot;xmpp:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</translation>
</message>
<message>
<source>E-Mail (&lt;a href=&quot;mailto:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</source>
<translation>E-Mail (&lt;a href=&quot;mailto:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</translation>
</message>
<message>
<source>Thanks To</source>
<translation>Graças ao</translation>
</message>
<message>
<source>Vae</source>
<translation>Vae</translation>
</message>
<message>
<source>Major refactoring, bug fixes, constructive criticism</source>
<translation>Refatoração importante, correção de erros, críticas construtivas</translation>
</message>
<message>
<source>Shunf4</source>
<translation>Shunf4</translation>
</message>
<message>
<source>Major refactoring, bug fixes, build adaptations for Windows and MacOS</source>
<translation>Refatoração importante, correção de erros, adaptações de construção para Windows e MacOS</translation>
</message>
<message>
<source>Bruno F. Fontes</source>
<translation>Bruno F. Fontes</translation>
</message>
<message>
<source>Brazilian Portuguese translation</source>
<translation>Tradução para o português do Brasil</translation>
</message>
<message>
<source>(built against %1)</source>
<translation>(Versão durante a compilação %1)</translation>
</message>
<message>
<source>License</source>
<translation>Licença</translation>
</message>
</context>
<context> <context>
<name>Account</name> <name>Account</name>
<message> <message>
@ -10,10 +112,12 @@
</message> </message>
<message> <message>
<source>Your account login</source> <source>Your account login</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Suas informações de login</translation> <translation>Suas informações de login</translation>
</message> </message>
<message> <message>
<source>john_smith1987</source> <source>john_smith1987</source>
<translatorcomment>Login placeholder</translatorcomment>
<translation>josé_silva1987</translation> <translation>josé_silva1987</translation>
</message> </message>
<message> <message>
@ -22,10 +126,12 @@
</message> </message>
<message> <message>
<source>A server address of your account. Like 404.city or macaw.me</source> <source>A server address of your account. Like 404.city or macaw.me</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>O endereço do servidor da sua conta, como o 404.city ou o macaw.me</translation> <translation>O endereço do servidor da sua conta, como o 404.city ou o macaw.me</translation>
</message> </message>
<message> <message>
<source>macaw.me</source> <source>macaw.me</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>macaw.me</translation> <translation>macaw.me</translation>
</message> </message>
<message> <message>
@ -38,6 +144,7 @@
</message> </message>
<message> <message>
<source>Password of your account</source> <source>Password of your account</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Senha da sua conta</translation> <translation>Senha da sua conta</translation>
</message> </message>
<message> <message>
@ -46,10 +153,11 @@
</message> </message>
<message> <message>
<source>Just a name how would you call this account, doesn&apos;t affect anything</source> <source>Just a name how would you call this account, doesn&apos;t affect anything</source>
<translation>Apenas um nome para identificar esta conta. Não influencia em nada</translation> <translation>Apenas um nome para identificar esta conta. Não influencia em nada (não pode ser mudado)</translation>
</message> </message>
<message> <message>
<source>John</source> <source>John</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>José</translation> <translation>José</translation>
</message> </message>
<message> <message>
@ -58,6 +166,7 @@
</message> </message>
<message> <message>
<source>A resource name like &quot;Home&quot; or &quot;Work&quot;</source> <source>A resource name like &quot;Home&quot; or &quot;Work&quot;</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Um nome de recurso como &quot;Casa&quot; ou &quot;Trabalho&quot;</translation> <translation>Um nome de recurso como &quot;Casa&quot; ou &quot;Trabalho&quot;</translation>
</message> </message>
<message> <message>
@ -69,6 +178,14 @@
<source>Password storage</source> <source>Password storage</source>
<translation>Armazenamento de senha</translation> <translation>Armazenamento de senha</translation>
</message> </message>
<message>
<source>Active</source>
<translation>Ativo</translation>
</message>
<message>
<source>enable</source>
<translation>habilitar</translation>
</message>
</context> </context>
<context> <context>
<name>Accounts</name> <name>Accounts</name>
@ -98,30 +215,135 @@
</message> </message>
<message> <message>
<source>Disconnect</source> <source>Disconnect</source>
<translation>Desconectar</translation> <translation type="vanished">Desconectar</translation>
</message>
<message>
<source>Deactivate</source>
<translation>Desativar</translation>
</message>
<message>
<source>Activate</source>
<translation>Ativar</translation>
</message>
</context>
<context>
<name>Application</name>
<message>
<source> from </source>
<translation> de </translation>
</message>
<message>
<source>Attached file</source>
<translation>Arquivo anexado</translation>
</message>
<message>
<source>Mark as Read</source>
<translation>Marcar como lido</translation>
</message>
<message>
<source>Open conversation</source>
<translation>Abrir conversa</translation>
</message> </message>
</context> </context>
<context> <context>
<name>Conversation</name> <name>Conversation</name>
<message> <message>
<source>Type your message here...</source> <source>Type your message here...</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Digite sua mensagem aqui...</translation> <translation>Digite sua mensagem aqui...</translation>
</message> </message>
<message> <message>
<source>Chose a file to send</source> <source>Chose a file to send</source>
<translation>Escolha um arquivo para enviar</translation> <translation>Escolha um arquivo para enviar</translation>
</message> </message>
<message>
<source>Drop files here to attach them to your message</source>
<translation>Arraste seus arquivos aqui para anexá-los a sua mensagem</translation>
</message>
<message> <message>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Liberation Sans&apos;; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Noto Sans&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation></translation> <translation>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Noto Sans&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message> </message>
<message> <message>
<source>Drop files here to attach them to your message</source> <source>Paste Image</source>
<translation>Arraste seus arquivos aqui para anexá-los a sua mensagem</translation> <translation>Colar imagem</translation>
</message>
<message>
<source>Try sending again</source>
<translation>Tente enviar de novo</translation>
</message>
<message>
<source>Copy selected</source>
<translation>Copiar selecionado</translation>
</message>
<message>
<source>Copy message</source>
<translation>Copiar mensagem</translation>
</message>
<message>
<source>Open</source>
<translation>Abrir</translation>
</message>
<message>
<source>Show in folder</source>
<translation>Show in explorer</translation>
</message>
<message>
<source>Edit</source>
<translation>Editar</translation>
</message>
<message>
<source>Editing message...</source>
<translation>Messae está sendo editado...</translation>
</message>
</context>
<context>
<name>CredentialsPrompt</name>
<message>
<source>Authentication error: %1</source>
<translatorcomment>Window title</translatorcomment>
<translation>Erro de autenticação: %1</translation>
</message>
<message>
<source>Couldn&apos;t authenticate account %1: login or password is icorrect.
Would you like to check them and try again?</source>
<translation>Não foi possível autenticar a conta %1: login ou senha incorretos.
Deseja verificá-los e tentar novamente?</translation>
</message>
<message>
<source>Login</source>
<translation>Usuário</translation>
</message>
<message>
<source>Your account login (without @server.domain)</source>
<translation>Suas informações de login (sem @server.domain)</translation>
</message>
<message>
<source>Password</source>
<translation>Senha</translation>
</message>
<message>
<source>Your password</source>
<translation>Senha da sua conta</translation>
</message>
</context>
<context>
<name>DialogQueue</name>
<message>
<source>Input the password for account %1</source>
<translation>Digite a senha para a conta %1</translation>
</message>
<message>
<source>Password for account %1</source>
<translation>Senha para a conta %1</translation>
</message> </message>
</context> </context>
<context> <context>
@ -370,14 +592,14 @@ p, li { white-space: pre-wrap; }
<name>Message</name> <name>Message</name>
<message> <message>
<source>Open</source> <source>Open</source>
<translation>Abrir</translation> <translation type="vanished">Abrir</translation>
</message> </message>
</context> </context>
<context> <context>
<name>MessageLine</name> <name>MessageLine</name>
<message> <message>
<source>Downloading...</source> <source>Downloading...</source>
<translation>Baixando...</translation> <translation type="vanished">Baixando...</translation>
</message> </message>
<message> <message>
<source>Download</source> <source>Download</source>
@ -386,28 +608,28 @@ p, li { white-space: pre-wrap; }
<message> <message>
<source>Error uploading file: %1 <source>Error uploading file: %1
You can try again</source> You can try again</source>
<translation>Error fazendo upload do arquivo: <translation type="vanished">Error fazendo upload do arquivo:
%1 %1
Você pode tentar novamente</translation> Você pode tentar novamente</translation>
</message> </message>
<message> <message>
<source>Upload</source> <source>Upload</source>
<translation>Upload</translation> <translation type="vanished">Upload</translation>
</message> </message>
<message> <message>
<source>Error downloading file: %1 <source>Error downloading file: %1
You can try again</source> You can try again</source>
<translation>Erro baixando arquivo: <translation type="vanished">Erro baixando arquivo:
%1 %1
Você pode tentar novamente</translation> Você pode tentar novamente</translation>
</message> </message>
<message> <message>
<source>Uploading...</source> <source>Uploading...</source>
<translation>Fazendo upload...</translation> <translation type="vanished">Fazendo upload...</translation>
</message> </message>
<message> <message>
<source>Push the button to download the file</source> <source>Push the button to download the file</source>
<translation>Pressione o botão para baixar o arquivo</translation> <translation type="vanished">Pressione o botão para baixar o arquivo</translation>
</message> </message>
</context> </context>
<context> <context>
@ -481,7 +703,7 @@ Você pode tentar novamente</translation>
<name>NewContact</name> <name>NewContact</name>
<message> <message>
<source>Add new contact</source> <source>Add new contact</source>
<translatorcomment>Заголовок окна</translatorcomment> <translatorcomment>Window title</translatorcomment>
<translation>Adicionar novo contato</translation> <translation>Adicionar novo contato</translation>
</message> </message>
<message> <message>
@ -502,7 +724,7 @@ Você pode tentar novamente</translation>
</message> </message>
<message> <message>
<source>name@server.dmn</source> <source>name@server.dmn</source>
<translatorcomment>Placeholder поля ввода JID</translatorcomment> <translatorcomment>Placeholder</translatorcomment>
<translation>nome@servidor.com.br</translation> <translation>nome@servidor.com.br</translation>
</message> </message>
<message> <message>
@ -518,6 +740,66 @@ Você pode tentar novamente</translation>
<translation>José Silva</translation> <translation>José Silva</translation>
</message> </message>
</context> </context>
<context>
<name>PageAppearance</name>
<message>
<source>Theme</source>
<translation>Estilo</translation>
</message>
<message>
<source>Color scheme</source>
<translation>Esquema de cores</translation>
</message>
<message>
<source>System</source>
<translation>Do sistema</translation>
</message>
</context>
<context>
<name>PageGeneral</name>
<message>
<source>Downloads path</source>
<translation>Pasta de downloads</translation>
</message>
<message>
<source>Browse</source>
<translation>6 / 5,000
Translation results
Navegar</translation>
</message>
<message>
<source>Select where downloads folder is going to be</source>
<translation>Selecione onde a pasta de downloads ficará</translation>
</message>
</context>
<context>
<name>Settings</name>
<message>
<source>Preferences</source>
<translatorcomment>Window title</translatorcomment>
<translation>Preferências</translation>
</message>
<message>
<source>General</source>
<translation>Geral</translation>
</message>
<message>
<source>Appearance</source>
<translation>Aparência</translation>
</message>
<message>
<source>Apply</source>
<translation>Aplicar</translation>
</message>
<message>
<source>Cancel</source>
<translation>Cancelar</translation>
</message>
<message>
<source>Ok</source>
<translation>Feito</translation>
</message>
</context>
<context> <context>
<name>Squawk</name> <name>Squawk</name>
<message> <message>
@ -530,6 +812,7 @@ Você pode tentar novamente</translation>
</message> </message>
<message> <message>
<source>Squawk</source> <source>Squawk</source>
<translatorcomment>Menu bar entry</translatorcomment>
<translation>Squawk</translation> <translation>Squawk</translation>
</message> </message>
<message> <message>
@ -550,11 +833,11 @@ Você pode tentar novamente</translation>
</message> </message>
<message> <message>
<source>Disconnect</source> <source>Disconnect</source>
<translation>Desconectar</translation> <translation type="vanished">Desconectar</translation>
</message> </message>
<message> <message>
<source>Connect</source> <source>Connect</source>
<translation>Conectar</translation> <translation type="vanished">Conectar</translation>
</message> </message>
<message> <message>
<source>VCard</source> <source>VCard</source>
@ -627,26 +910,46 @@ ser exibido com
</message> </message>
<message> <message>
<source>Attached file</source> <source>Attached file</source>
<translation>Arquivo anexado</translation> <translation type="vanished">Arquivo anexado</translation>
</message> </message>
<message> <message>
<source>Input the password for account %1</source> <source>Input the password for account %1</source>
<translation>Digite a senha para a conta %1</translation> <translation type="vanished">Digite a senha para a conta %1</translation>
</message> </message>
<message> <message>
<source>Password for account %1</source> <source>Password for account %1</source>
<translation>Senha para a conta %1</translation> <translation type="vanished">Senha para a conta %1</translation>
</message> </message>
<message> <message>
<source>Please select a contact to start chatting</source> <source>Please select a contact to start chatting</source>
<translation>Por favor selecione um contato para começar a conversar</translation> <translation>Por favor selecione um contato para começar a conversar</translation>
</message> </message>
<message>
<source>Help</source>
<translation>Ajuda</translation>
</message>
<message>
<source>Preferences</source>
<translation>Preferências</translation>
</message>
<message>
<source>About Squawk</source>
<translation>Sorbe Squawk</translation>
</message>
<message>
<source>Deactivate</source>
<translation>Desativar</translation>
</message>
<message>
<source>Activate</source>
<translation>Ativar</translation>
</message>
</context> </context>
<context> <context>
<name>VCard</name> <name>VCard</name>
<message> <message>
<source>Received 12.07.2007 at 17.35</source> <source>Received 12.07.2007 at 17.35</source>
<translation>Recebido 12/07/2007 às 17:35</translation> <translation>Nunca atualizado</translation>
</message> </message>
<message> <message>
<source>General</source> <source>General</source>
@ -682,7 +985,7 @@ ser exibido com
</message> </message>
<message> <message>
<source>Unit / Department</source> <source>Unit / Department</source>
<translation>Unidade/Departamento</translation> <translation>Unidade / Departamento</translation>
</message> </message>
<message> <message>
<source>Role / Profession</source> <source>Role / Profession</source>

View File

@ -1,6 +1,108 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="ru_RU"> <TS version="2.1" language="ru_RU">
<context>
<name>About</name>
<message>
<source>About Squawk</source>
<translation>О Программе Squawk</translation>
</message>
<message>
<source>Squawk</source>
<translation>Squawk</translation>
</message>
<message>
<source>About</source>
<translatorcomment>Tab title</translatorcomment>
<translation>Общее</translation>
</message>
<message>
<source>XMPP (jabber) messenger</source>
<translation>XMPP (jabber) мессенджер</translation>
</message>
<message>
<source>(c) 2019 - 2022, Yury Gubich</source>
<translation>(c) 2019 - 2022, Юрий Губич</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk&quot;&gt;Project site&lt;/a&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk&quot;&gt;Сайт проекта&lt;/a&gt;</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md&quot;&gt;License: GNU General Public License version 3&lt;/a&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md&quot;&gt;Лицензия: GNU General Public License версия 3&lt;/a&gt;</translation>
</message>
<message>
<source>Components</source>
<translation>Компоненты</translation>
</message>
<message>
<source>Version</source>
<translation>Версия</translation>
</message>
<message>
<source>0.0.0</source>
<translation>0.0.0</translation>
</message>
<message>
<source>Report Bugs</source>
<translation>Сообщать об ошибках</translation>
</message>
<message>
<source>Please report any bug you find!
To report bugs you can use:</source>
<translation>Пожалуйста, сообщайте о любых ошибках!
Способы сообщить об ошибках:</translation>
</message>
<message>
<source>&lt;a href=&quot;https://git.macaw.me/blue/squawk/issues&quot;&gt;Project bug tracker&lt;/&gt;</source>
<translation>&lt;a href=&quot;https://git.macaw.me/blue/squawk/issues&quot;&gt;Баг-трекер проекта&lt;/&gt;</translation>
</message>
<message>
<source>XMPP (&lt;a href=&quot;xmpp:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</source>
<translation>XMPP (&lt;a href=&quot;xmpp:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</translation>
</message>
<message>
<source>E-Mail (&lt;a href=&quot;mailto:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</source>
<translation>E-Mail (&lt;a href=&quot;mailto:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</translation>
</message>
<message>
<source>Thanks To</source>
<translation>Благодарности</translation>
</message>
<message>
<source>Vae</source>
<translation>Vae</translation>
</message>
<message>
<source>Major refactoring, bug fixes, constructive criticism</source>
<translation>Крупный рефакторинг, исправление ошибок, конструктивная критика</translation>
</message>
<message>
<source>Shunf4</source>
<translation>Shunf4</translation>
</message>
<message>
<source>Major refactoring, bug fixes, build adaptations for Windows and MacOS</source>
<translation>Крупный рефакторинг, исправление ошибок, адаптация сборки под Windows and MacOS</translation>
</message>
<message>
<source>Bruno F. Fontes</source>
<translation>Bruno F. Fontes</translation>
</message>
<message>
<source>Brazilian Portuguese translation</source>
<translation>Перевод на Португальский (Бразилия)</translation>
</message>
<message>
<source>(built against %1)</source>
<translation>(версия при сборке %1)</translation>
</message>
<message>
<source>License</source>
<translation>Лицензия</translation>
</message>
</context>
<context> <context>
<name>Account</name> <name>Account</name>
<message> <message>
@ -10,10 +112,12 @@
</message> </message>
<message> <message>
<source>Your account login</source> <source>Your account login</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Имя пользователя Вашей учетной записи</translation> <translation>Имя пользователя Вашей учетной записи</translation>
</message> </message>
<message> <message>
<source>john_smith1987</source> <source>john_smith1987</source>
<translatorcomment>Login placeholder</translatorcomment>
<translation>ivan_ivanov1987</translation> <translation>ivan_ivanov1987</translation>
</message> </message>
<message> <message>
@ -22,10 +126,12 @@
</message> </message>
<message> <message>
<source>A server address of your account. Like 404.city or macaw.me</source> <source>A server address of your account. Like 404.city or macaw.me</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Адресс сервера вашей учетной записи (выглядит как 404.city или macaw.me)</translation> <translation>Адресс сервера вашей учетной записи (выглядит как 404.city или macaw.me)</translation>
</message> </message>
<message> <message>
<source>macaw.me</source> <source>macaw.me</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>macaw.me</translation> <translation>macaw.me</translation>
</message> </message>
<message> <message>
@ -38,6 +144,7 @@
</message> </message>
<message> <message>
<source>Password of your account</source> <source>Password of your account</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Пароль вашей учетной записи</translation> <translation>Пароль вашей учетной записи</translation>
</message> </message>
<message> <message>
@ -46,10 +153,11 @@
</message> </message>
<message> <message>
<source>Just a name how would you call this account, doesn&apos;t affect anything</source> <source>Just a name how would you call this account, doesn&apos;t affect anything</source>
<translation>Просто имя, то как Вы называете свою учетную запись, может быть любым</translation> <translation>Просто имя, то как Вы называете свою учетную запись, может быть любым (нельзя поменять)</translation>
</message> </message>
<message> <message>
<source>John</source> <source>John</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Иван</translation> <translation>Иван</translation>
</message> </message>
<message> <message>
@ -58,6 +166,7 @@
</message> </message>
<message> <message>
<source>A resource name like &quot;Home&quot; or &quot;Work&quot;</source> <source>A resource name like &quot;Home&quot; or &quot;Work&quot;</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Имя этой программы для ваших контактов, может быть &quot;Home&quot; или &quot;Phone&quot;</translation> <translation>Имя этой программы для ваших контактов, может быть &quot;Home&quot; или &quot;Phone&quot;</translation>
</message> </message>
<message> <message>
@ -69,6 +178,14 @@
<source>Password storage</source> <source>Password storage</source>
<translation>Хранение пароля</translation> <translation>Хранение пароля</translation>
</message> </message>
<message>
<source>Active</source>
<translation>Активен</translation>
</message>
<message>
<source>enable</source>
<translation>включен</translation>
</message>
</context> </context>
<context> <context>
<name>Accounts</name> <name>Accounts</name>
@ -98,30 +215,139 @@
</message> </message>
<message> <message>
<source>Disconnect</source> <source>Disconnect</source>
<translation>Отключить</translation> <translation type="vanished">Отключить</translation>
</message>
<message>
<source>Deactivate</source>
<translation>Деактивировать</translation>
</message>
<message>
<source>Activate</source>
<translation>Активировать</translation>
</message>
</context>
<context>
<name>Application</name>
<message>
<source> from </source>
<translation> от </translation>
</message>
<message>
<source>Attached file</source>
<translation>Прикрепленный файл</translation>
</message>
<message>
<source>Mark as Read</source>
<translation>Пометить прочитанным</translation>
</message>
<message>
<source>Open conversation</source>
<translation>Открыть окно беседы</translation>
</message> </message>
</context> </context>
<context> <context>
<name>Conversation</name> <name>Conversation</name>
<message> <message>
<source>Type your message here...</source> <source>Type your message here...</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Введите сообщение...</translation> <translation>Введите сообщение...</translation>
</message> </message>
<message> <message>
<source>Chose a file to send</source> <source>Chose a file to send</source>
<translation>Выберите файл для отправки</translation> <translation>Выберите файл для отправки</translation>
</message> </message>
<message>
<source>Drop files here to attach them to your message</source>
<translation>Бросьте файлы сюда для того что бы прикрепить их к сообщению</translation>
</message>
<message> <message>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Liberation Sans&apos;; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Noto Sans&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation></translation> <translation>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;Noto Sans&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message> </message>
<message> <message>
<source>Drop files here to attach them to your message</source> <source>Paste Image</source>
<translation>Бросьте файлы сюда для того что бы прикрепить их к сообщению</translation> <translation>Вставить изображение</translation>
</message>
<message>
<source>Try sending again</source>
<translation>Отправить снова</translation>
</message>
<message>
<source>Copy selected</source>
<translation>Скопировать выделенное</translation>
</message>
<message>
<source>Copy message</source>
<translation>Скопировать сообщение</translation>
</message>
<message>
<source>Open</source>
<translation>Открыть</translation>
</message>
<message>
<source>Show in folder</source>
<translation>Показать в проводнике</translation>
</message>
<message>
<source>Edit</source>
<translation>Редактировать</translation>
</message>
<message>
<source>Editing message...</source>
<translation>Сообщение редактируется...</translation>
</message>
</context>
<context>
<name>CredentialsPrompt</name>
<message>
<source>Authentication error: %1</source>
<translatorcomment>Window title</translatorcomment>
<translation>Ошибка аутентификации: %1</translation>
</message>
<message>
<source>Couldn&apos;t authenticate account %1: login or password is icorrect.
Would you like to check them and try again?</source>
<translation>Не получилось аутентифицировать
учетную запись %1:
имя пользователя или пароль введены неверно.
Желаете ли проверить их и
попробовать аутентифицироваться еще раз?</translation>
</message>
<message>
<source>Login</source>
<translation>Имя учетной записи</translation>
</message>
<message>
<source>Your account login (without @server.domain)</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Имя вашей учтетной записи (без @server.domain)</translation>
</message>
<message>
<source>Password</source>
<translation>Пароль</translation>
</message>
<message>
<source>Your password</source>
<translation>Ваш пароль</translation>
</message>
</context>
<context>
<name>DialogQueue</name>
<message>
<source>Input the password for account %1</source>
<translation>Введите пароль для учетной записи %1</translation>
</message>
<message>
<source>Password for account %1</source>
<translation>Пароль для учетной записи %1</translation>
</message> </message>
</context> </context>
<context> <context>
@ -370,14 +596,14 @@ p, li { white-space: pre-wrap; }
<name>Message</name> <name>Message</name>
<message> <message>
<source>Open</source> <source>Open</source>
<translation>Открыть</translation> <translation type="vanished">Открыть</translation>
</message> </message>
</context> </context>
<context> <context>
<name>MessageLine</name> <name>MessageLine</name>
<message> <message>
<source>Downloading...</source> <source>Downloading...</source>
<translation>Скачивается...</translation> <translation type="vanished">Скачивается...</translation>
</message> </message>
<message> <message>
<source>Download</source> <source>Download</source>
@ -386,28 +612,28 @@ p, li { white-space: pre-wrap; }
<message> <message>
<source>Error uploading file: %1 <source>Error uploading file: %1
You can try again</source> You can try again</source>
<translation>Ошибка загрузки файла на сервер: <translation type="vanished">Ошибка загрузки файла на сервер:
%1 %1
Для того, что бы попробовать снова нажмите на кнопку</translation> Для того, что бы попробовать снова нажмите на кнопку</translation>
</message> </message>
<message> <message>
<source>Upload</source> <source>Upload</source>
<translation>Загрузить</translation> <translation type="vanished">Загрузить</translation>
</message> </message>
<message> <message>
<source>Error downloading file: %1 <source>Error downloading file: %1
You can try again</source> You can try again</source>
<translation>Ошибка скачивания файла: <translation type="vanished">Ошибка скачивания файла:
%1 %1
Вы можете попробовать снова</translation> Вы можете попробовать снова</translation>
</message> </message>
<message> <message>
<source>Uploading...</source> <source>Uploading...</source>
<translation>Загружается...</translation> <translation type="vanished">Загружается...</translation>
</message> </message>
<message> <message>
<source>Push the button to download the file</source> <source>Push the button to download the file</source>
<translation>Нажмите на кнопку что бы загрузить файл</translation> <translation type="vanished">Нажмите на кнопку что бы загрузить файл</translation>
</message> </message>
</context> </context>
<context> <context>
@ -481,7 +707,7 @@ You can try again</source>
<name>NewContact</name> <name>NewContact</name>
<message> <message>
<source>Add new contact</source> <source>Add new contact</source>
<translatorcomment>Заголовок окна</translatorcomment> <translatorcomment>Window title</translatorcomment>
<translation>Добавление нового контакта</translation> <translation>Добавление нового контакта</translation>
</message> </message>
<message> <message>
@ -502,7 +728,7 @@ You can try again</source>
</message> </message>
<message> <message>
<source>name@server.dmn</source> <source>name@server.dmn</source>
<translatorcomment>Placeholder поля ввода JID</translatorcomment> <translatorcomment>Placeholder</translatorcomment>
<translation>name@server.dmn</translation> <translation>name@server.dmn</translation>
</message> </message>
<message> <message>
@ -518,6 +744,64 @@ You can try again</source>
<translation>Иван Иванов</translation> <translation>Иван Иванов</translation>
</message> </message>
</context> </context>
<context>
<name>PageAppearance</name>
<message>
<source>Theme</source>
<translation>Оформление</translation>
</message>
<message>
<source>Color scheme</source>
<translation>Цветовая схема</translation>
</message>
<message>
<source>System</source>
<translation>Системная</translation>
</message>
</context>
<context>
<name>PageGeneral</name>
<message>
<source>Downloads path</source>
<translation>Папка для сохраненных файлов</translation>
</message>
<message>
<source>Browse</source>
<translation>Выбрать</translation>
</message>
<message>
<source>Select where downloads folder is going to be</source>
<translation>Выберете папку, в которую будут сохраняться файлы</translation>
</message>
</context>
<context>
<name>Settings</name>
<message>
<source>Preferences</source>
<translatorcomment>Window title</translatorcomment>
<translation>Настройки</translation>
</message>
<message>
<source>General</source>
<translation>Общее</translation>
</message>
<message>
<source>Appearance</source>
<translation>Внешний вид</translation>
</message>
<message>
<source>Apply</source>
<translation>Применить</translation>
</message>
<message>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<source>Ok</source>
<translation>Готово</translation>
</message>
</context>
<context> <context>
<name>Squawk</name> <name>Squawk</name>
<message> <message>
@ -530,6 +814,7 @@ You can try again</source>
</message> </message>
<message> <message>
<source>Squawk</source> <source>Squawk</source>
<translatorcomment>Menu bar entry</translatorcomment>
<translation>Squawk</translation> <translation>Squawk</translation>
</message> </message>
<message> <message>
@ -550,11 +835,11 @@ You can try again</source>
</message> </message>
<message> <message>
<source>Disconnect</source> <source>Disconnect</source>
<translation>Отключить</translation> <translation type="vanished">Отключить</translation>
</message> </message>
<message> <message>
<source>Connect</source> <source>Connect</source>
<translation>Подключить</translation> <translation type="vanished">Подключить</translation>
</message> </message>
<message> <message>
<source>VCard</source> <source>VCard</source>
@ -627,20 +912,40 @@ to be displayed as %1</source>
</message> </message>
<message> <message>
<source>Attached file</source> <source>Attached file</source>
<translation>Прикрепленный файл</translation> <translation type="vanished">Прикрепленный файл</translation>
</message> </message>
<message> <message>
<source>Input the password for account %1</source> <source>Input the password for account %1</source>
<translation>Введите пароль для учетной записи %1</translation> <translation type="vanished">Введите пароль для учетной записи %1</translation>
</message> </message>
<message> <message>
<source>Password for account %1</source> <source>Password for account %1</source>
<translation>Пароль для учетной записи %1</translation> <translation type="vanished">Пароль для учетной записи %1</translation>
</message> </message>
<message> <message>
<source>Please select a contact to start chatting</source> <source>Please select a contact to start chatting</source>
<translation>Выберите контакт или группу что бы начать переписку</translation> <translation>Выберите контакт или группу что бы начать переписку</translation>
</message> </message>
<message>
<source>Help</source>
<translation>Помощь</translation>
</message>
<message>
<source>Preferences</source>
<translation>Настройки</translation>
</message>
<message>
<source>About Squawk</source>
<translation>О Программе Squawk</translation>
</message>
<message>
<source>Deactivate</source>
<translation>Деактивировать</translation>
</message>
<message>
<source>Activate</source>
<translation>Активировать</translation>
</message>
</context> </context>
<context> <context>
<name>VCard</name> <name>VCard</name>

View File

@ -1,4 +1,8 @@
target_sources(squawk PRIVATE squawk.cpp squawk.h squawk.ui) target_sources(squawk PRIVATE
squawk.cpp
squawk.h
squawk.ui
)
add_subdirectory(models) add_subdirectory(models)
add_subdirectory(utils) add_subdirectory(utils)

View File

@ -32,7 +32,8 @@ Models::Account::Account(const QMap<QString, QVariant>& data, Models::Item* pare
state(Shared::ConnectionState::disconnected), state(Shared::ConnectionState::disconnected),
availability(Shared::Availability::offline), availability(Shared::Availability::offline),
passwordType(Shared::AccountPassword::plain), passwordType(Shared::AccountPassword::plain),
wasEverConnected(false) wasEverConnected(false),
active(false)
{ {
QMap<QString, QVariant>::const_iterator sItr = data.find("state"); QMap<QString, QVariant>::const_iterator sItr = data.find("state");
if (sItr != data.end()) { if (sItr != data.end()) {
@ -46,6 +47,10 @@ Models::Account::Account(const QMap<QString, QVariant>& data, Models::Item* pare
if (pItr != data.end()) { if (pItr != data.end()) {
setPasswordType(pItr.value().toUInt()); setPasswordType(pItr.value().toUInt());
} }
QMap<QString, QVariant>::const_iterator acItr = data.find("active");
if (acItr != data.end()) {
setActive(acItr.value().toBool());
}
} }
Models::Account::~Account() Models::Account::~Account()
@ -176,6 +181,8 @@ QVariant Models::Account::data(int column) const
return avatarPath; return avatarPath;
case 9: case 9:
return Shared::Global::getName(passwordType); return Shared::Global::getName(passwordType);
case 10:
return active;
default: default:
return QVariant(); return QVariant();
} }
@ -183,7 +190,7 @@ QVariant Models::Account::data(int column) const
int Models::Account::columnCount() const int Models::Account::columnCount() const
{ {
return 10; return 11;
} }
void Models::Account::update(const QString& field, const QVariant& value) void Models::Account::update(const QString& field, const QVariant& value)
@ -208,6 +215,8 @@ void Models::Account::update(const QString& field, const QVariant& value)
setAvatarPath(value.toString()); setAvatarPath(value.toString());
} else if (field == "passwordType") { } else if (field == "passwordType") {
setPasswordType(value.toUInt()); setPasswordType(value.toUInt());
} else if (field == "active") {
setActive(value.toBool());
} }
} }
@ -281,3 +290,16 @@ void Models::Account::setPasswordType(unsigned int pt)
{ {
setPasswordType(Shared::Global::fromInt<Shared::AccountPassword>(pt)); setPasswordType(Shared::Global::fromInt<Shared::AccountPassword>(pt));
} }
bool Models::Account::getActive() const
{
return active;
}
void Models::Account::setActive(bool p_active)
{
if (active != p_active) {
active = p_active;
changed(10);
}
}

View File

@ -58,6 +58,9 @@ namespace Models {
void setAvatarPath(const QString& path); void setAvatarPath(const QString& path);
QString getAvatarPath() const; QString getAvatarPath() const;
void setActive(bool active);
bool getActive() const;
void setAvailability(Shared::Availability p_avail); void setAvailability(Shared::Availability p_avail);
void setAvailability(unsigned int p_avail); void setAvailability(unsigned int p_avail);
@ -91,6 +94,7 @@ namespace Models {
Shared::Availability availability; Shared::Availability availability;
Shared::AccountPassword passwordType; Shared::AccountPassword passwordType;
bool wasEverConnected; bool wasEverConnected;
bool active;
protected slots: protected slots:
void toOfflineState() override; void toOfflineState() override;

View File

@ -48,6 +48,10 @@ QVariant Models::Accounts::data (const QModelIndex& index, int role) const
answer = Shared::connectionStateIcon(accs[index.row()]->getState()); answer = Shared::connectionStateIcon(accs[index.row()]->getState());
} }
break; break;
case Qt::ForegroundRole:
if (!accs[index.row()]->getActive()) {
answer = qApp->palette().brush(QPalette::Disabled, QPalette::Text);
}
default: default:
break; break;
} }

View File

@ -155,6 +155,16 @@ void Models::Contact::removePresence(const QString& name)
} }
} }
Models::Presence * Models::Contact::getPresence(const QString& name)
{
QMap<QString, Presence*>::iterator itr = presences.find(name);
if (itr == presences.end()) {
return nullptr;
} else {
return itr.value();
}
}
void Models::Contact::refresh() void Models::Contact::refresh()
{ {
QDateTime lastActivity; QDateTime lastActivity;

View File

@ -51,6 +51,7 @@ public:
void addPresence(const QString& name, const QMap<QString, QVariant>& data); void addPresence(const QString& name, const QMap<QString, QVariant>& data);
void removePresence(const QString& name); void removePresence(const QString& name);
Presence* getPresence(const QString& name);
QString getContactName() const; QString getContactName() const;
QString getStatus() const; QString getStatus() const;

View File

@ -134,6 +134,11 @@ unsigned int Models::Element::getMessagesCount() const
return feed->unreadMessagesCount(); return feed->unreadMessagesCount();
} }
bool Models::Element::markMessageAsRead(const QString& id) const
{
return feed->markMessageAsRead(id);
}
void Models::Element::addMessage(const Shared::Message& data) void Models::Element::addMessage(const Shared::Message& data)
{ {
feed->addMessage(data); feed->addMessage(data);
@ -171,6 +176,7 @@ void Models::Element::fileError(const QString& messageId, const QString& error,
void Models::Element::onFeedUnreadMessagesCountChanged() void Models::Element::onFeedUnreadMessagesCountChanged()
{ {
emit unreadMessagesCountChanged();
if (type == contact) { if (type == contact) {
changed(4); changed(4);
} else if (type == room) { } else if (type == room) {

View File

@ -42,6 +42,7 @@ public:
void addMessage(const Shared::Message& data); void addMessage(const Shared::Message& data);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data); void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
unsigned int getMessagesCount() const; unsigned int getMessagesCount() const;
bool markMessageAsRead(const QString& id) const;
void responseArchive(const std::list<Shared::Message> list, bool last); void responseArchive(const std::list<Shared::Message> list, bool last);
bool isRoom() const; bool isRoom() const;
void fileProgress(const QString& messageId, qreal value, bool up); void fileProgress(const QString& messageId, qreal value, bool up);
@ -52,6 +53,7 @@ signals:
void requestArchive(const QString& before); void requestArchive(const QString& before);
void fileDownloadRequest(const QString& url); void fileDownloadRequest(const QString& url);
void unnoticedMessage(const QString& account, const Shared::Message& msg); void unnoticedMessage(const QString& account, const Shared::Message& msg);
void unreadMessagesCountChanged();
void localPathInvalid(const QString& path); void localPathInvalid(const QString& path);
protected: protected:

View File

@ -264,6 +264,16 @@ void Models::Room::removeParticipant(const QString& p_name)
} }
} }
Models::Participant * Models::Room::getParticipant(const QString& p_name)
{
std::map<QString, Participant*>::const_iterator itr = participants.find(p_name);
if (itr == participants.end()) {
return nullptr;
} else {
return itr->second;
}
}
void Models::Room::handleParticipantUpdate(std::map<QString, Participant*>::const_iterator itr, const QMap<QString, QVariant>& data) void Models::Room::handleParticipantUpdate(std::map<QString, Participant*>::const_iterator itr, const QMap<QString, QVariant>& data)
{ {
Participant* part = itr->second; Participant* part = itr->second;

View File

@ -58,6 +58,7 @@ public:
void addParticipant(const QString& name, const QMap<QString, QVariant>& data); void addParticipant(const QString& name, const QMap<QString, QVariant>& data);
void changeParticipant(const QString& name, const QMap<QString, QVariant>& data); void changeParticipant(const QString& name, const QMap<QString, QVariant>& data);
void removeParticipant(const QString& name); void removeParticipant(const QString& name);
Participant* getParticipant(const QString& name);
void toOfflineState() override; void toOfflineState() override;
QString getDisplayedName() const override; QString getDisplayedName() const override;

View File

@ -276,6 +276,18 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
break; break;
} }
break; break;
case Qt::ForegroundRole:
switch (item->type) {
case Item::account: {
Account* acc = static_cast<Account*>(item);
if (!acc->getActive()) {
result = qApp->palette().brush(QPalette::Disabled, QPalette::Text);
}
}
break;
default:
break;
}
default: default:
break; break;
} }
@ -451,6 +463,7 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons
connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid); connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid);
connect(contact, &Contact::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages);
contacts.insert(std::make_pair(id, contact)); contacts.insert(std::make_pair(id, contact));
} else { } else {
contact = itr->second; contact = itr->second;
@ -536,8 +549,8 @@ void Models::Roster::removeGroup(const QString& account, const QString& name)
void Models::Roster::changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data) void Models::Roster::changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data)
{ {
Element* el = getElement({account, jid}); Element* el = getElement(ElId(account, jid));
if (el != NULL) { if (el != nullptr) {
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
el->update(itr.key(), itr.value()); el->update(itr.key(), itr.value());
} }
@ -546,8 +559,8 @@ void Models::Roster::changeContact(const QString& account, const QString& jid, c
void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data) void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data)
{ {
Element* el = getElement({account, jid}); Element* el = getElement(ElId(account, jid));
if (el != NULL) { if (el != nullptr) {
el->changeMessage(id, data); el->changeMessage(id, data);
} else { } else {
qDebug() << "A request to change a message of the contact " << jid << " in the account " << account << " but it wasn't found"; qDebug() << "A request to change a message of the contact " << jid << " in the account " << account << " but it wasn't found";
@ -694,8 +707,8 @@ void Models::Roster::removePresence(const QString& account, const QString& jid,
void Models::Roster::addMessage(const QString& account, const Shared::Message& data) void Models::Roster::addMessage(const QString& account, const Shared::Message& data)
{ {
Element* el = getElement({account, data.getPenPalJid()}); Element* el = getElement(ElId(account, data.getPenPalJid()));
if (el != NULL) { if (el != nullptr) {
el->addMessage(data); el->addMessage(data);
} }
} }
@ -751,7 +764,7 @@ void Models::Roster::removeAccount(const QString& account)
acc->deleteLater(); acc->deleteLater();
} }
QString Models::Roster::getContactName(const QString& account, const QString& jid) QString Models::Roster::getContactName(const QString& account, const QString& jid) const
{ {
ElId id(account, jid); ElId id(account, jid);
std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id); std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id);
@ -789,10 +802,11 @@ void Models::Roster::addRoom(const QString& account, const QString jid, const QM
} }
Room* room = new Room(acc, jid, data); Room* room = new Room(acc, jid, data);
connect(room, &Contact::requestArchive, this, &Roster::onElementRequestArchive); connect(room, &Room::requestArchive, this, &Roster::onElementRequestArchive);
connect(room, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); connect(room, &Room::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(room, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); connect(room, &Room::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(room, &Contact::localPathInvalid, this, &Roster::localPathInvalid); connect(room, &Room::localPathInvalid, this, &Roster::localPathInvalid);
connect(room, &Room::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages);
rooms.insert(std::make_pair(id, room)); rooms.insert(std::make_pair(id, room));
acc->appendChild(room); acc->appendChild(room);
} }
@ -895,7 +909,7 @@ bool Models::Roster::groupHasContact(const QString& account, const QString& grou
} }
} }
QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource) QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource) const
{ {
ElId id(account, jid); ElId id(account, jid);
std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id); std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id);
@ -915,9 +929,36 @@ QString Models::Roster::getContactIconPath(const QString& account, const QString
return path; return path;
} }
Models::Account * Models::Roster::getAccount(const QString& name) Models::Account * Models::Roster::getAccount(const QString& name) {
return const_cast<Models::Account*>(getAccountConst(name));}
const Models::Account * Models::Roster::getAccountConst(const QString& name) const {
return accounts.at(name);}
const Models::Element * Models::Roster::getElementConst(const Models::Roster::ElId& id) const
{ {
return accounts.find(name)->second; std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id);
if (cItr != contacts.end()) {
return cItr->second;
} else {
std::map<ElId, Room*>::const_iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
return rItr->second;
}
}
return nullptr;
}
bool Models::Roster::markMessageAsRead(const Models::Roster::ElId& elementId, const QString& messageId)
{
const Element* el = getElementConst(elementId);
if (el != nullptr) {
return el->markMessageAsRead(messageId);
} else {
return false;
}
} }
QModelIndex Models::Roster::getAccountIndex(const QString& name) QModelIndex Models::Roster::getAccountIndex(const QString& name)
@ -936,7 +977,7 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString&
if (itr == accounts.end()) { if (itr == accounts.end()) {
return QModelIndex(); return QModelIndex();
} else { } else {
std::map<ElId, Group*>::const_iterator gItr = groups.find({account, name}); std::map<ElId, Group*>::const_iterator gItr = groups.find(ElId(account, name));
if (gItr == groups.end()) { if (gItr == groups.end()) {
return QModelIndex(); return QModelIndex();
} else { } else {
@ -946,6 +987,48 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString&
} }
} }
QModelIndex Models::Roster::getContactIndex(const QString& account, const QString& jid, const QString& resource)
{
std::map<QString, Account*>::const_iterator itr = accounts.find(account);
if (itr == accounts.end()) {
return QModelIndex();
} else {
Account* acc = itr->second;
QModelIndex accIndex = index(acc->row(), 0, QModelIndex());
std::map<ElId, Contact*>::const_iterator cItr = contacts.find(ElId(account, jid));
if (cItr != contacts.end()) {
QModelIndex contactIndex = index(acc->getContact(jid), 0, accIndex);
if (resource.size() == 0) {
return contactIndex;
} else {
Presence* pres = cItr->second->getPresence(resource);
if (pres != nullptr) {
return index(pres->row(), 0, contactIndex);
} else {
return contactIndex;
}
}
} else {
std::map<ElId, Room*>::const_iterator rItr = rooms.find(ElId(account, jid));
if (rItr != rooms.end()) {
QModelIndex roomIndex = index(rItr->second->row(), 0, accIndex);
if (resource.size() == 0) {
return roomIndex;
} else {
Participant* part = rItr->second->getParticipant(resource);
if (part != nullptr) {
return index(part->row(), 0, roomIndex);
} else {
return roomIndex;
}
}
} else {
return QModelIndex();
}
}
}
}
void Models::Roster::onElementRequestArchive(const QString& before) void Models::Roster::onElementRequestArchive(const QString& before)
{ {
Element* el = static_cast<Element*>(sender()); Element* el = static_cast<Element*>(sender());
@ -956,7 +1039,7 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid,
{ {
ElId id(account, jid); ElId id(account, jid);
Element* el = getElement(id); Element* el = getElement(id);
if (el != NULL) { if (el != nullptr) {
el->responseArchive(list, last); el->responseArchive(list, last);
} }
} }
@ -964,8 +1047,8 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid,
void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up) void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up)
{ {
for (const Shared::MessageInfo& info : msgs) { for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid}); Element* el = getElement(ElId(info.account, info.jid));
if (el != NULL) { if (el != nullptr) {
el->fileProgress(info.messageId, value, up); el->fileProgress(info.messageId, value, up);
} }
} }
@ -974,8 +1057,8 @@ void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qr
void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bool up) void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bool up)
{ {
for (const Shared::MessageInfo& info : msgs) { for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid}); Element* el = getElement(ElId(info.account, info.jid));
if (el != NULL) { if (el != nullptr) {
el->fileComplete(info.messageId, up); el->fileComplete(info.messageId, up);
} }
} }
@ -984,8 +1067,8 @@ void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bo
void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const QString& err, bool up) void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const QString& err, bool up)
{ {
for (const Shared::MessageInfo& info : msgs) { for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid}); Element* el = getElement(ElId(info.account, info.jid));
if (el != NULL) { if (el != nullptr) {
el->fileError(info.messageId, err, up); el->fileError(info.messageId, err, up);
} }
} }
@ -993,20 +1076,20 @@ void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const
Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id) Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id)
{ {
std::map<ElId, Contact*>::iterator cItr = contacts.find(id); return const_cast<Models::Element*>(getElementConst(id));
if (cItr != contacts.end()) {
return cItr->second;
} else {
std::map<ElId, Room*>::iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
return rItr->second;
}
}
return NULL;
} }
Models::Item::Type Models::Roster::getContactType(const Models::Roster::ElId& id) const
{
const Models::Element* el = getElementConst(id);
if (el == nullptr) {
return Item::root;
}
return el->type;
}
void Models::Roster::onAccountReconnected() void Models::Roster::onAccountReconnected()
{ {
Account* acc = static_cast<Account*>(sender()); Account* acc = static_cast<Account*>(sender());
@ -1019,3 +1102,14 @@ void Models::Roster::onAccountReconnected()
} }
} }
void Models::Roster::recalculateUnreadMessages()
{
int count(0);
for (const std::pair<const ElId, Contact*>& pair : contacts) {
count += pair.second->getMessagesCount();
}
for (const std::pair<const ElId, Room*>& pair : rooms) {
count += pair.second->getMessagesCount();
}
emit unreadMessagesCountChanged(count);
}

View File

@ -46,6 +46,7 @@ public:
Roster(QObject* parent = 0); Roster(QObject* parent = 0);
~Roster(); ~Roster();
public slots:
void addAccount(const QMap<QString, QVariant> &data); void addAccount(const QMap<QString, QVariant> &data);
void updateAccount(const QString& account, const QString& field, const QVariant& value); void updateAccount(const QString& account, const QString& field, const QVariant& value);
void removeAccount(const QString& account); void removeAccount(const QString& account);
@ -65,7 +66,12 @@ public:
void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data); void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data); void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void removeRoomParticipant(const QString& account, const QString& jid, const QString& name); void removeRoomParticipant(const QString& account, const QString& jid, const QString& name);
QString getContactName(const QString& account, const QString& jid);
public:
QString getContactName(const QString& account, const QString& jid) const;
Item::Type getContactType(const Models::Roster::ElId& id) const;
const Element* getElementConst(const ElId& id) const;
Element* getElement(const ElId& id);
QVariant data ( const QModelIndex& index, int role ) const override; QVariant data ( const QModelIndex& index, int role ) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override; Qt::ItemFlags flags(const QModelIndex &index) const override;
@ -77,10 +83,13 @@ public:
std::deque<QString> groupList(const QString& account) const; std::deque<QString> groupList(const QString& account) const;
bool groupHasContact(const QString& account, const QString& group, const QString& contactJID) const; bool groupHasContact(const QString& account, const QString& group, const QString& contactJID) const;
QString getContactIconPath(const QString& account, const QString& jid, const QString& resource); QString getContactIconPath(const QString& account, const QString& jid, const QString& resource) const;
Account* getAccount(const QString& name); Account* getAccount(const QString& name);
const Account* getAccountConst(const QString& name) const;
QModelIndex getAccountIndex(const QString& name); QModelIndex getAccountIndex(const QString& name);
QModelIndex getGroupIndex(const QString& account, const QString& name); QModelIndex getGroupIndex(const QString& account, const QString& name);
QModelIndex getContactIndex(const QString& account, const QString& jid, const QString& resource = "");
bool markMessageAsRead(const ElId& elementId, const QString& messageId);
void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last); void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last);
void fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up); void fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up);
@ -92,12 +101,10 @@ public:
signals: signals:
void requestArchive(const QString& account, const QString& jid, const QString& before); void requestArchive(const QString& account, const QString& jid, const QString& before);
void fileDownloadRequest(const QString& url); void fileDownloadRequest(const QString& url);
void unreadMessagesCountChanged(int count);
void unnoticedMessage(const QString& account, const Shared::Message& msg); void unnoticedMessage(const QString& account, const Shared::Message& msg);
void localPathInvalid(const QString& path); void localPathInvalid(const QString& path);
private:
Element* getElement(const ElId& id);
private slots: private slots:
void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector<int>& roles); void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector<int>& roles);
void onAccountReconnected(); void onAccountReconnected();
@ -109,7 +116,8 @@ private slots:
void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex); void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex);
void onChildMoved(); void onChildMoved();
void onElementRequestArchive(const QString& before); void onElementRequestArchive(const QString& before);
void recalculateUnreadMessages();
private: private:
Item* root; Item* root;
std::map<QString, Account*> accounts; std::map<QString, Account*> accounts;

File diff suppressed because it is too large Load Diff

View File

@ -22,16 +22,16 @@
#include <QMainWindow> #include <QMainWindow>
#include <QScopedPointer> #include <QScopedPointer>
#include <QCloseEvent> #include <QCloseEvent>
#include <QtDBus/QDBusInterface>
#include <QSettings> #include <QSettings>
#include <QInputDialog> #include <QInputDialog>
#include <QSystemTrayIcon>
#include <deque> #include <deque>
#include <map> #include <map>
#include <set> #include <set>
#include <list> #include <list>
#include "widgets/accounts.h" #include "widgets/accounts/accounts.h"
#include "widgets/chat.h" #include "widgets/chat.h"
#include "widgets/room.h" #include "widgets/room.h"
#include "widgets/newcontact.h" #include "widgets/newcontact.h"
@ -39,103 +39,79 @@
#include "models/roster.h" #include "models/roster.h"
#include "widgets/vcard/vcard.h" #include "widgets/vcard/vcard.h"
#include "widgets/settings/settings.h" #include "widgets/settings/settings.h"
#include "widgets/about.h"
#include "shared/shared.h" #include "shared/shared.h"
#include "shared/global.h"
namespace Ui { namespace Ui {
class Squawk; class Squawk;
} }
class Application;
class Squawk : public QMainWindow class Squawk : public QMainWindow
{ {
Q_OBJECT Q_OBJECT
friend class Application;
public: public:
explicit Squawk(QWidget *parent = nullptr); explicit Squawk(Models::Roster& rosterModel, QWidget *parent = nullptr);
~Squawk() override; ~Squawk() override;
signals: signals:
void closing();
void newAccountRequest(const QMap<QString, QVariant>&); void newAccountRequest(const QMap<QString, QVariant>&);
void modifyAccountRequest(const QString&, const QMap<QString, QVariant>&);
void removeAccountRequest(const QString&); void removeAccountRequest(const QString&);
void connectAccount(const QString&); void connectAccount(const QString&);
void disconnectAccount(const QString&); void disconnectAccount(const QString&);
void changeState(Shared::Availability state); void changeState(Shared::Availability state);
void sendMessage(const QString& account, const Shared::Message& data);
void replaceMessage(const QString& account, const QString& originalId, const Shared::Message& data);
void resendMessage(const QString& account, const QString& jid, const QString& id);
void requestArchive(const QString& account, const QString& jid, int count, const QString& before);
void subscribeContact(const QString& account, const QString& jid, const QString& reason);
void unsubscribeContact(const QString& account, const QString& jid, const QString& reason);
void removeContactRequest(const QString& account, const QString& jid); void removeContactRequest(const QString& account, const QString& jid);
void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet<QString>& groups); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet<QString>& groups);
void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName);
void removeContactFromGroupRequest(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 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 addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin);
void removeRoomRequest(const QString& account, const QString& jid); void removeRoomRequest(const QString& account, const QString& jid);
void fileDownloadRequest(const QString& url);
void requestVCard(const QString& account, const QString& jid); void requestVCard(const QString& account, const QString& jid);
void uploadVCard(const QString& account, const Shared::VCard& card); void uploadVCard(const QString& account, const Shared::VCard& card);
void responsePassword(const QString& account, const QString& password);
void localPathInvalid(const QString& path);
void changeDownloadsPath(const QString& path); void changeDownloadsPath(const QString& path);
void notify(const QString& account, const Shared::Message& msg);
void changeSubscription(const Models::Roster::ElId& id, bool subscribe);
void openedConversation();
void openConversation(const Models::Roster::ElId& id, const QString& resource = "");
void modifyAccountRequest(const QString&, const QMap<QString, QVariant>&);
public:
Models::Roster::ElId currentConversationId() const;
void closeCurrentConversation();
static QSystemTrayIcon *trayIcon;
public slots: public slots:
void writeSettings(); void writeSettings();
void readSettings();
void newAccount(const QMap<QString, QVariant>& account);
void changeAccount(const QString& account, const QMap<QString, QVariant>& data);
void removeAccount(const QString& account);
void addGroup(const QString& account, const QString& name);
void removeGroup(const QString& account, const QString& name);
void addContact(const QString& account, const QString& jid, const QString& group, const QMap<QString, QVariant>& data);
void removeContact(const QString& account, const QString& jid, const QString& group);
void removeContact(const QString& account, const QString& jid);
void changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data);
void addPresence(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void removePresence(const QString& account, const QString& jid, const QString& name);
void stateChanged(Shared::Availability state); void stateChanged(Shared::Availability state);
void accountMessage(const QString& account, const Shared::Message& data);
void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last);
void addRoom(const QString& account, const QString jid, const QMap<QString, QVariant>& data);
void changeRoom(const QString& account, const QString jid, const QMap<QString, QVariant>& data);
void removeRoom(const QString& account, const QString jid);
void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void removeRoomParticipant(const QString& account, const QString& jid, const QString& name);
void fileError(const std::list<Shared::MessageInfo> msgs, const QString& error, bool up);
void fileProgress(const std::list<Shared::MessageInfo> msgs, qreal value, bool up);
void fileDownloadComplete(const std::list<Shared::MessageInfo> msgs, const QString& path);
void fileUploadComplete(const std::list<Shared::MessageInfo> msgs, const QString& url, const QString& path);
void responseVCard(const QString& jid, const Shared::VCard& card); void responseVCard(const QString& jid, const Shared::VCard& card);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data); void select(QModelIndex index);
void requestPassword(const QString& account);
private: private:
typedef std::map<Models::Roster::ElId, Conversation*> Conversations; void createTrayIcon();
QScopedPointer<Ui::Squawk> m_ui; QScopedPointer<Ui::Squawk> m_ui;
Accounts* accounts; Accounts* accounts;
Settings* preferences; Settings* preferences;
Models::Roster rosterModel; About* about;
Conversations conversations; Models::Roster& rosterModel;
QMenu* contextMenu; QMenu* contextMenu;
QDBusInterface dbus;
std::map<QString, VCard*> vCards; std::map<QString, VCard*> vCards;
std::deque<QString> requestedAccountsForPasswords;
QInputDialog* prompt;
Conversation* currentConversation; Conversation* currentConversation;
QModelIndex restoreSelection; QModelIndex restoreSelection;
bool needToRestore; bool needToRestore;
protected: protected:
void closeEvent(QCloseEvent * event) override; void closeEvent(QCloseEvent * event) override;
void expand(const QModelIndex& index);
protected slots:
void notify(const QString& account, const Shared::Message& msg);
private slots: private slots:
void onAccounts(); void onAccounts();
@ -147,29 +123,17 @@ private slots:
void onAccountsSizeChanged(unsigned int size); void onAccountsSizeChanged(unsigned int size);
void onAccountsClosed(); void onAccountsClosed();
void onPreferencesClosed(); void onPreferencesClosed();
void onConversationClosed(QObject* parent = 0);
void onVCardClosed(); void onVCardClosed();
void onVCardSave(const Shared::VCard& card, const QString& account); void onVCardSave(const Shared::VCard& card, const QString& account);
void onActivateVCard(const QString& account, const QString& jid, bool edition = false); void onActivateVCard(const QString& account, const QString& jid, bool edition = false);
void onComboboxActivated(int index); void onComboboxActivated(int index);
void onRosterItemDoubleClicked(const QModelIndex& item); void onRosterItemDoubleClicked(const QModelIndex& item);
void onConversationMessage(const Shared::Message& msg);
void onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg);
void onConversationResend(const QString& id);
void onRequestArchive(const QString& account, const QString& jid, const QString& before);
void onRosterContextMenu(const QPoint& point); void onRosterContextMenu(const QPoint& point);
void onItemCollepsed(const QModelIndex& index); void onItemCollepsed(const QModelIndex& index);
void onPasswordPromptAccepted();
void onPasswordPromptRejected();
void onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous); void onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
void onContextAboutToHide(); void onContextAboutToHide();
void onAboutSquawkCalled();
void onUnnoticedMessage(const QString& account, const Shared::Message& msg); void onAboutSquawkClosed();
private:
void checkNextAccountForPassword();
void onPasswordPromptDone();
void subscribeConversation(Conversation* conv);
}; };
#endif // SQUAWK_H #endif // SQUAWK_H

View File

@ -201,8 +201,15 @@
<addaction name="actionAddConference"/> <addaction name="actionAddConference"/>
<addaction name="actionQuit"/> <addaction name="actionQuit"/>
</widget> </widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAboutSquawk"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuSettings"/> <addaction name="menuSettings"/>
<addaction name="menuHelp"/>
</widget> </widget>
<action name="actionAccounts"> <action name="actionAccounts">
<property name="icon"> <property name="icon">
@ -248,12 +255,18 @@
</action> </action>
<action name="actionPreferences"> <action name="actionPreferences">
<property name="icon"> <property name="icon">
<iconset theme="settings-configure"/> <iconset theme="settings-configure">
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Preferences</string> <string>Preferences</string>
</property> </property>
</action> </action>
<action name="actionAboutSquawk">
<property name="text">
<string>About Squawk</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="../resources/resources.qrc"/> <include location="../resources/resources.qrc"/>

View File

@ -1,10 +1,4 @@
target_sources(squawk PRIVATE target_sources(squawk PRIVATE
account.cpp
account.h
account.ui
accounts.cpp
accounts.h
accounts.ui
chat.cpp chat.cpp
chat.h chat.h
conversation.cpp conversation.cpp
@ -18,8 +12,12 @@ target_sources(squawk PRIVATE
newcontact.ui newcontact.ui
room.cpp room.cpp
room.h room.h
about.cpp
about.h
about.ui
) )
add_subdirectory(vcard) add_subdirectory(vcard)
add_subdirectory(messageline) add_subdirectory(messageline)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(accounts)

108
ui/widgets/about.cpp Normal file
View File

@ -0,0 +1,108 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#include "about.h"
#include "ui_about.h"
#include <QXmppGlobal.h>
#include <QDebug>
static const std::string QXMPP_VERSION_PATCH(std::to_string(QXMPP_VERSION & 0xff));
static const std::string QXMPP_VERSION_MINOR(std::to_string((QXMPP_VERSION & 0xff00) >> 8));
static const std::string QXMPP_VERSION_MAJOR(std::to_string(QXMPP_VERSION >> 16));
static const QString QXMPP_VERSION_STRING = QString::fromStdString(QXMPP_VERSION_MAJOR + "." + QXMPP_VERSION_MINOR + "." + QXMPP_VERSION_PATCH);
About::About(QWidget* parent):
QWidget(parent),
m_ui(new Ui::About),
license(nullptr)
{
m_ui->setupUi(this);
m_ui->versionValue->setText(QApplication::applicationVersion());
m_ui->qtVersionValue->setText(qVersion());
m_ui->qtBuiltAgainstVersion->setText(tr("(built against %1)").arg(QT_VERSION_STR));
m_ui->qxmppVersionValue->setText(QXmppVersion());
m_ui->qxmppBuiltAgainstVersion->setText(tr("(built against %1)").arg(QXMPP_VERSION_STRING));
setWindowFlag(Qt::Tool);
connect(m_ui->licenceLink, &QLabel::linkActivated, this, &About::onLicenseActivated);
}
About::~About() {
if (license != nullptr) {
license->deleteLater();
}
};
void About::onLicenseActivated()
{
if (license == nullptr) {
QFile file;
bool found = false;
QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
for (const QString& path : shares) {
file.setFileName(path + "/LICENSE.md");
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
found = true;
break;
}
}
if (!found) {
qDebug() << "couldn't read license file, bailing";
return;
}
license = new QWidget();
license->setWindowTitle(tr("License"));
QVBoxLayout* layout = new QVBoxLayout(license);
QLabel* text = new QLabel(license);
QScrollArea* area = new QScrollArea(license);
text->setTextFormat(Qt::MarkdownText);
text->setWordWrap(true);
text->setOpenExternalLinks(true);
text->setMargin(5);
area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
layout->addWidget(area);
license->setAttribute(Qt::WA_DeleteOnClose);
connect(license, &QWidget::destroyed, this, &About::onLicenseClosed);
QTextStream in(&file);
QString line;
QString licenseText("");
while (!in.atEnd()) {
line = in.readLine();
licenseText.append(line + "\n");
}
text->setText(licenseText);
file.close();
area->setWidget(text);
} else {
license->raise();
license->activateWindow();
}
license->show();
}
void About::onLicenseClosed()
{
license = nullptr;
}

51
ui/widgets/about.h Normal file
View File

@ -0,0 +1,51 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#ifndef ABOUT_H
#define ABOUT_H
#include <QWidget>
#include <QScopedPointer>
#include <QApplication>
#include <QFile>
#include <QTextStream>
#include <QStandardPaths>
namespace Ui
{
class About;
}
/**
* @todo write docs
*/
class About : public QWidget
{
Q_OBJECT
public:
About(QWidget* parent = nullptr);
~About();
protected slots:
void onLicenseActivated();
void onLicenseClosed();
private:
QScopedPointer<Ui::About> m_ui;
QWidget* license;
};
#endif // ABOUT_H

680
ui/widgets/about.ui Normal file
View File

@ -0,0 +1,680 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>About</class>
<widget class="QWidget" name="About">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>375</width>
<height>290</height>
</rect>
</property>
<property name="windowTitle">
<string>About Squawk</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="2" column="0" colspan="4">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLabel" name="header">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Squawk</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QLabel" name="squawkIcon">
<property name="maximumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../resources/resources.qrc">:/images/logo.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="0" column="3" rowspan="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0" colspan="4">
<widget class="QTabWidget" name="tabs">
<property name="currentIndex">
<number>0</number>
</property>
<property name="usesScrollButtons">
<bool>false</bool>
</property>
<widget class="QWidget" name="aboutTab">
<attribute name="title">
<string>About</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="description">
<property name="text">
<string>XMPP (jabber) messenger</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="copyright">
<property name="text">
<string>(c) 2019 - 2022, Yury Gubich</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="siteLink">
<property name="text">
<string>&lt;a href=&quot;https://git.macaw.me/blue/squawk&quot;&gt;Project site&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="licenceLink">
<property name="text">
<string>&lt;a href=&quot;https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md&quot;&gt;License: GNU General Public License version 3&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QScrollArea" name="componentsTab">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<attribute name="title">
<string>Components</string>
</attribute>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>355</width>
<height>181</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="horizontalSpacing">
<number>2</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="qtVersionValue">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>0.0.0</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="qtBuiltAgainstVersion">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string notr="true">(built against 0.0.0)</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">Qt</string>
</property>
</widget>
</item>
<item row="0" column="3" rowspan="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="4">
<widget class="QLabel" name="label_3">
<property name="text">
<string notr="true">&lt;a href=&quot;https://www.qt.io/&quot;&gt;www.qt.io&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="horizontalSpacing">
<number>2</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="qxmppVersionValue">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>0.0.0</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="qxmppBuiltAgainstVersion">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string notr="true">(built against 0.0.0)</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="label_5">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">QXmpp</string>
</property>
</widget>
</item>
<item row="0" column="3" rowspan="2">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="4">
<widget class="QLabel" name="label_6">
<property name="text">
<string notr="true">&lt;a href=&quot;https://github.com/qxmpp-project/qxmpp&quot;&gt;github.com/qxmpp-project/qxmpp&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="reportTab">
<attribute name="title">
<string>Report Bugs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Please report any bug you find!
To report bugs you can use:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>&lt;a href=&quot;https://git.macaw.me/blue/squawk/issues&quot;&gt;Project bug tracker&lt;/&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>XMPP (&lt;a href=&quot;xmpp:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>E-Mail (&lt;a href=&quot;mailto:blue@macaw.me&quot;&gt;blue@macaw.me&lt;/a&gt;)</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QScrollArea" name="thanksTab">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<attribute name="title">
<string>Thanks To</string>
</attribute>
<widget class="QWidget" name="thanksTabContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>355</width>
<height>181</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_11">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Vae</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_12">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Major refactoring, bug fixes, constructive criticism</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_4" native="true">
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_13">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Shunf4</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Major refactoring, bug fixes, build adaptations for Windows and MacOS</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_5" native="true">
<layout class="QGridLayout" name="gridLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Bruno F. Fontes</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Brazilian Portuguese translation</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>Version</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="versionValue">
<property name="text">
<string>0.0.0</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,11 @@
target_sources(squawk PRIVATE
account.cpp
account.h
account.ui
accounts.cpp
accounts.h
accounts.ui
credentialsprompt.cpp
credentialsprompt.h
credentialsprompt.ui
)

View File

@ -53,6 +53,7 @@ QMap<QString, QVariant> Account::value() const
map["name"] = m_ui->name->text(); map["name"] = m_ui->name->text();
map["resource"] = m_ui->resource->text(); map["resource"] = m_ui->resource->text();
map["passwordType"] = m_ui->passwordType->currentIndex(); map["passwordType"] = m_ui->passwordType->currentIndex();
map["active"] = m_ui->active->isChecked();
return map; return map;
} }

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>438</width> <width>438</width>
<height>342</height> <height>345</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -34,7 +34,7 @@
<property name="verticalSpacing"> <property name="verticalSpacing">
<number>6</number> <number>6</number>
</property> </property>
<item row="1" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="login"> <widget class="QLineEdit" name="login">
<property name="toolTip"> <property name="toolTip">
<string>Your account login</string> <string>Your account login</string>
@ -44,14 +44,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Server</string> <string>Server</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="QLineEdit" name="server"> <widget class="QLineEdit" name="server">
<property name="toolTip"> <property name="toolTip">
<string>A server address of your account. Like 404.city or macaw.me</string> <string>A server address of your account. Like 404.city or macaw.me</string>
@ -61,21 +61,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Login</string> <string>Login</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Password</string> <string>Password</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="password"> <widget class="QLineEdit" name="password">
<property name="toolTip"> <property name="toolTip">
<string>Password of your account</string> <string>Password of your account</string>
@ -97,14 +97,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Name</string> <string>Name</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="name"> <widget class="QLineEdit" name="name">
<property name="toolTip"> <property name="toolTip">
<string>Just a name how would you call this account, doesn't affect anything</string> <string>Just a name how would you call this account, doesn't affect anything</string>
@ -114,14 +114,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Resource</string> <string>Resource</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<widget class="QLineEdit" name="resource"> <widget class="QLineEdit" name="resource">
<property name="toolTip"> <property name="toolTip">
<string>A resource name like &quot;Home&quot; or &quot;Work&quot;</string> <string>A resource name like &quot;Home&quot; or &quot;Work&quot;</string>
@ -131,17 +131,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>Password storage</string> <string>Password storage</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<widget class="QComboBox" name="passwordType"/> <widget class="QComboBox" name="passwordType"/>
</item> </item>
<item row="5" column="1"> <item row="6" column="1">
<widget class="QLabel" name="comment"> <widget class="QLabel" name="comment">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
@ -157,6 +157,23 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Active</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="active">
<property name="text">
<string>enable</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -83,7 +83,8 @@ void Accounts::onEditButton()
{"server", mAcc->getServer()}, {"server", mAcc->getServer()},
{"name", mAcc->getName()}, {"name", mAcc->getName()},
{"resource", mAcc->getResource()}, {"resource", mAcc->getResource()},
{"passwordType", QVariant::fromValue(mAcc->getPasswordType())} {"passwordType", QVariant::fromValue(mAcc->getPasswordType())},
{"active", mAcc->getActive()}
}); });
acc->lockId(); acc->lockId();
connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted);
@ -118,17 +119,17 @@ void Accounts::updateConnectButton()
bool allConnected = true; bool allConnected = true;
for (int i = 0; i < selectionSize && allConnected; ++i) { for (int i = 0; i < selectionSize && allConnected; ++i) {
const Models::Account* mAcc = model->getAccount(sm->selectedRows().at(i).row()); const Models::Account* mAcc = model->getAccount(sm->selectedRows().at(i).row());
allConnected = mAcc->getState() == Shared::ConnectionState::connected; allConnected = allConnected && mAcc->getActive();
} }
if (allConnected) { if (allConnected) {
toDisconnect = true; toDisconnect = true;
m_ui->connectButton->setText(tr("Disconnect")); m_ui->connectButton->setText(tr("Deactivate"));
} else { } else {
toDisconnect = false; toDisconnect = false;
m_ui->connectButton->setText(tr("Connect")); m_ui->connectButton->setText(tr("Activate"));
} }
} else { } else {
m_ui->connectButton->setText(tr("Connect")); m_ui->connectButton->setText(tr("Activate"));
toDisconnect = false; toDisconnect = false;
m_ui->connectButton->setEnabled(false); m_ui->connectButton->setEnabled(false);
} }

View File

@ -24,7 +24,7 @@
#include <QItemSelection> #include <QItemSelection>
#include "account.h" #include "account.h"
#include "../models/accounts.h" #include "ui/models/accounts.h"
namespace Ui namespace Ui
{ {

View File

@ -0,0 +1,60 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#include "credentialsprompt.h"
#include "ui_credentialsprompt.h"
CredentialsPrompt::CredentialsPrompt(QWidget* parent):
QDialog(parent),
m_ui(new Ui::CredentialsPrompt),
title(),
message()
{
m_ui->setupUi(this);
title = windowTitle();
message = m_ui->message->text();
}
CredentialsPrompt::~CredentialsPrompt()
{
}
void CredentialsPrompt::setAccount(const QString& account)
{
m_ui->message->setText(message.arg(account));
setWindowTitle(title.arg(account));
}
QString CredentialsPrompt::getLogin() const
{
return m_ui->login->text();
}
QString CredentialsPrompt::getPassword() const
{
return m_ui->password->text();
}
void CredentialsPrompt::setLogin(const QString& login)
{
m_ui->login->setText(login);
}
void CredentialsPrompt::setPassword(const QString& password)
{
m_ui->password->setText(password);
}

View File

@ -0,0 +1,52 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// 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 <http://www.gnu.org/licenses/>.
#ifndef CREDENTIALSPROMPT_H
#define CREDENTIALSPROMPT_H
#include <qdialog.h>
#include <QScopedPointer>
namespace Ui
{
class CredentialsPrompt;
}
/**
* @todo write docs
*/
class CredentialsPrompt : public QDialog
{
Q_OBJECT
public:
CredentialsPrompt(QWidget* parent = nullptr);
~CredentialsPrompt();
void setAccount(const QString& account);
void setLogin(const QString& login);
void setPassword(const QString& password);
QString getLogin() const;
QString getPassword() const;
private:
QScopedPointer<Ui::CredentialsPrompt> m_ui;
QString title;
QString message;
};
#endif // CREDENTIALSPROMPT_H

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CredentialsPrompt</class>
<widget class="QDialog" name="CredentialsPrompt">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>229</height>
</rect>
</property>
<property name="windowTitle">
<string>Authentication error: %1</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<item>
<widget class="QLabel" name="message">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Couldn't authenticate account %1: login or password is icorrect.
Would you like to check them and try again?</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="loginLabel">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="login">
<property name="toolTip">
<string>Your account login (without @server.domain)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="toolTip">
<string>Your password</string>
</property>
<property name="inputMethodHints">
<set>Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData</set>
</property>
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CredentialsPrompt</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CredentialsPrompt</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -26,7 +26,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QAbstractTextDocumentLayout> #include <QAbstractTextDocumentLayout>
#include <QCoreApplication> #include <QApplication>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QDir> #include <QDir>
#include <QMenu> #include <QMenu>
@ -498,6 +498,26 @@ void Conversation::onFeedContext(const QPoint& pos)
emit resendMessage(id); emit resendMessage(id);
}); });
} }
QString selected = feed->getSelectedText();
if (selected.size() > 0) {
showMenu = true;
QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected"));
connect(copy, &QAction::triggered, [selected] () {
QClipboard* cb = QApplication::clipboard();
cb->setText(selected);
});
}
QString body = item->getBody();
if (body.size() > 0) {
showMenu = true;
QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy message"));
connect(copy, &QAction::triggered, [body] () {
QClipboard* cb = QApplication::clipboard();
cb->setText(body);
});
}
QString path = Shared::resolvePath(item->getAttachPath()); QString path = Shared::resolvePath(item->getAttachPath());
if (path.size() > 0) { if (path.size() > 0) {

View File

@ -21,6 +21,8 @@
#include <QPaintEvent> #include <QPaintEvent>
#include <QPainter> #include <QPainter>
#include <QScrollBar> #include <QScrollBar>
#include <QApplication>
#include <QClipboard>
#include <QDebug> #include <QDebug>
#include "messagedelegate.h" #include "messagedelegate.h"
@ -50,7 +52,13 @@ FeedView::FeedView(QWidget* parent):
modelState(Models::MessageFeed::complete), modelState(Models::MessageFeed::complete),
progress(), progress(),
dividerFont(), dividerFont(),
dividerMetrics(dividerFont) dividerMetrics(dividerFont),
mousePressed(false),
dragging(false),
hovered(Shared::Hover::nothing),
dragStartPoint(),
dragEndPoint(),
selectedText()
{ {
horizontalScrollBar()->setRange(0, 0); horizontalScrollBar()->setRange(0, 0);
verticalScrollBar()->setSingleStep(approximateSingleMessageHeight); verticalScrollBar()->setSingleStep(approximateSingleMessageHeight);
@ -162,7 +170,7 @@ void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottom
void FeedView::updateGeometries() void FeedView::updateGeometries()
{ {
qDebug() << "updateGeometries"; //qDebug() << "updateGeometries";
QScrollBar* bar = verticalScrollBar(); QScrollBar* bar = verticalScrollBar();
const QStyle* st = style(); const QStyle* st = style();
@ -343,6 +351,7 @@ void FeedView::paintEvent(QPaintEvent* event)
QDateTime lastDate; QDateTime lastDate;
bool first = true; bool first = true;
QRect viewportRect = vp->rect();
for (const QModelIndex& index : toRener) { for (const QModelIndex& index : toRener) {
QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime();
option.rect = visualRect(index); option.rect = visualRect(index);
@ -356,7 +365,10 @@ void FeedView::paintEvent(QPaintEvent* event)
} }
first = false; first = false;
} }
bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor); QRect stripe = option.rect;
stripe.setLeft(0);
stripe.setWidth(viewportRect.width());
bool mouseOver = stripe.contains(cursor) && viewportRect.contains(cursor);
option.state.setFlag(QStyle::State_MouseOver, mouseOver); option.state.setFlag(QStyle::State_MouseOver, mouseOver);
itemDelegate(index)->paint(&painter, option, index); itemDelegate(index)->paint(&painter, option, index);
@ -403,13 +415,151 @@ void FeedView::verticalScrollbarValueChanged(int value)
QAbstractItemView::verticalScrollbarValueChanged(vo); QAbstractItemView::verticalScrollbarValueChanged(vo);
} }
void FeedView::setAnchorHovered(Shared::Hover type)
{
if (hovered != type) {
hovered = type;
switch (hovered) {
case Shared::Hover::nothing:
setCursor(Qt::ArrowCursor);
break;
case Shared::Hover::text:
setCursor(Qt::IBeamCursor);
break;
case Shared::Hover::anchor:
setCursor(Qt::PointingHandCursor);
break;
}
}
}
void FeedView::mouseMoveEvent(QMouseEvent* event) void FeedView::mouseMoveEvent(QMouseEvent* event)
{ {
if (!isVisible()) { if (!isVisible()) {
return; return;
} }
dragEndPoint = event->localPos().toPoint();
if (mousePressed) {
QPoint distance = dragStartPoint - dragEndPoint;
if (distance.manhattanLength() > 5) {
dragging = true;
}
}
QAbstractItemView::mouseMoveEvent(event); QAbstractItemView::mouseMoveEvent(event);
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
if (dragging) {
QModelIndex index = indexAt(dragStartPoint);
if (index.isValid()) {
QRect rect = visualRect(index);
if (rect.contains(dragStartPoint)) {
QString selected = del->mouseDrag(dragStartPoint, dragEndPoint, index, rect);
if (selectedText != selected) {
selectedText = selected;
setDirtyRegion(rect);
}
}
}
} else {
QModelIndex index = indexAt(dragEndPoint);
if (index.isValid()) {
QRect rect = visualRect(index);
if (rect.contains(dragEndPoint)) {
setAnchorHovered(del->hoverType(dragEndPoint, index, rect));
} else {
setAnchorHovered(Shared::Hover::nothing);
}
} else {
setAnchorHovered(Shared::Hover::nothing);
}
}
}
}
void FeedView::mousePressEvent(QMouseEvent* event)
{
QAbstractItemView::mousePressEvent(event);
mousePressed = event->button() == Qt::LeftButton;
if (mousePressed) {
dragStartPoint = event->localPos().toPoint();
if (specialDelegate && specialModel) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
QString lastSelectedId = del->clearSelection();
if (lastSelectedId.size()) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
QModelIndex index = feed->modelIndexById(lastSelectedId);
if (index.isValid()) {
setDirtyRegion(visualRect(index));
}
}
}
}
}
void FeedView::mouseDoubleClickEvent(QMouseEvent* event)
{
QAbstractItemView::mouseDoubleClickEvent(event);
mousePressed = event->button() == Qt::LeftButton;
if (mousePressed) {
dragStartPoint = event->localPos().toPoint();
if (specialDelegate && specialModel) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
QString lastSelectedId = del->clearSelection();
selectedText = "";
if (lastSelectedId.size()) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
QModelIndex index = feed->modelIndexById(lastSelectedId);
if (index.isValid()) {
setDirtyRegion(visualRect(index));
}
}
QModelIndex index = indexAt(dragStartPoint);
QRect rect = visualRect(index);
if (rect.contains(dragStartPoint)) {
selectedText = del->leftDoubleClick(dragStartPoint, index, rect);
if (selectedText.size() > 0) {
setDirtyRegion(rect);
}
}
}
}
}
void FeedView::mouseReleaseEvent(QMouseEvent* event)
{
QAbstractItemView::mouseReleaseEvent(event);
if (mousePressed) {
if (!dragging && specialDelegate) {
QPoint point = event->localPos().toPoint();
QModelIndex index = indexAt(point);
if (index.isValid()) {
QRect rect = visualRect(index);
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
if (rect.contains(point)) {
del->leftClick(point, index, rect);
}
}
}
dragging = false;
mousePressed = false;
}
}
void FeedView::keyPressEvent(QKeyEvent* event)
{
QKeyEvent *key_event = static_cast<QKeyEvent*>(event);
if (key_event->matches(QKeySequence::Copy)) {
if (selectedText.size() > 0) {
QClipboard* cb = QApplication::clipboard();
cb->setText(selectedText);
}
}
} }
void FeedView::resizeEvent(QResizeEvent* event) void FeedView::resizeEvent(QResizeEvent* event)
@ -456,6 +606,7 @@ void FeedView::setItemDelegate(QAbstractItemDelegate* delegate)
elementMargin = MessageDelegate::margin; elementMargin = MessageDelegate::margin;
connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
connect(del, &MessageDelegate::openLink, &QDesktopServices::openUrl);
} else { } else {
specialDelegate = false; specialDelegate = false;
elementMargin = 0; elementMargin = 0;
@ -520,3 +671,8 @@ void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state)
scheduleDelayedItemsLayout(); scheduleDelayedItemsLayout();
} }
} }
QString FeedView::getSelectedText() const
{
return selectedText;
}

View File

@ -20,12 +20,15 @@
#define FEEDVIEW_H #define FEEDVIEW_H
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QDesktopServices>
#include <QUrl>
#include <deque> #include <deque>
#include <set> #include <set>
#include <ui/widgets/messageline/messagefeed.h> #include <ui/widgets/messageline/messagefeed.h>
#include <ui/utils/progress.h> #include <ui/utils/progress.h>
#include <shared/utils.h>
/** /**
* @todo write docs * @todo write docs
@ -48,6 +51,7 @@ public:
void setModel(QAbstractItemModel * model) override; void setModel(QAbstractItemModel * model) override;
QFont getFont() const; QFont getFont() const;
QString getSelectedText() const;
signals: signals:
void resized(); void resized();
@ -68,12 +72,17 @@ protected:
void paintEvent(QPaintEvent * event) override; void paintEvent(QPaintEvent * event) override;
void updateGeometries() override; void updateGeometries() override;
void mouseMoveEvent(QMouseEvent * event) override; void mouseMoveEvent(QMouseEvent * event) override;
void mousePressEvent(QMouseEvent * event) override;
void mouseReleaseEvent(QMouseEvent * event) override;
void mouseDoubleClickEvent(QMouseEvent * event) override;
void keyPressEvent(QKeyEvent * event) override;
void resizeEvent(QResizeEvent * event) override; void resizeEvent(QResizeEvent * event) override;
private: private:
bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight); bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight);
void positionProgress(); void positionProgress();
void drawDateDevider(int top, const QDateTime& date, QPainter& painter); void drawDateDevider(int top, const QDateTime& date, QPainter& painter);
void setAnchorHovered(Shared::Hover type);
private: private:
struct Hint { struct Hint {
@ -93,6 +102,12 @@ private:
Progress progress; Progress progress;
QFont dividerFont; QFont dividerFont;
QFontMetrics dividerMetrics; QFontMetrics dividerMetrics;
bool mousePressed;
bool dragging;
Shared::Hover hovered;
QPoint dragStartPoint;
QPoint dragEndPoint;
QString selectedText;
static const std::set<int> geometryChangingRoles; static const std::set<int> geometryChangingRoles;

View File

@ -22,6 +22,9 @@
#include <QApplication> #include <QApplication>
#include <QMouseEvent> #include <QMouseEvent>
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QAbstractTextDocumentLayout>
#include <QTextBlock>
#include <cmath>
#include "messagedelegate.h" #include "messagedelegate.h"
#include "messagefeed.h" #include "messagefeed.h"
@ -41,7 +44,7 @@ MessageDelegate::MessageDelegate(QObject* parent):
bodyFont(), bodyFont(),
nickFont(), nickFont(),
dateFont(), dateFont(),
bodyMetrics(bodyFont), bodyRenderer(new QTextDocument()),
nickMetrics(nickFont), nickMetrics(nickFont),
dateMetrics(dateFont), dateMetrics(dateFont),
buttonHeight(0), buttonHeight(0),
@ -51,11 +54,14 @@ MessageDelegate::MessageDelegate(QObject* parent):
bars(new std::map<QString, QProgressBar*>()), bars(new std::map<QString, QProgressBar*>()),
statusIcons(new std::map<QString, QLabel*>()), statusIcons(new std::map<QString, QLabel*>()),
pencilIcons(new std::map<QString, QLabel*>()), pencilIcons(new std::map<QString, QLabel*>()),
bodies(new std::map<QString, QLabel*>()),
previews(new std::map<QString, Preview*>()), previews(new std::map<QString, Preview*>()),
idsToKeep(new std::set<QString>()), idsToKeep(new std::set<QString>()),
clearingWidgets(false) clearingWidgets(false),
currentId(""),
selection(0, 0)
{ {
bodyRenderer->setDocumentMargin(0);
QPushButton btn(QCoreApplication::translate("MessageLine", "Download")); QPushButton btn(QCoreApplication::translate("MessageLine", "Download"));
buttonHeight = btn.sizeHint().height(); buttonHeight = btn.sizeHint().height();
buttonWidth = btn.sizeHint().width(); buttonWidth = btn.sizeHint().width();
@ -82,10 +88,6 @@ MessageDelegate::~MessageDelegate()
delete pair.second; delete pair.second;
} }
for (const std::pair<const QString, QLabel*>& pair: *bodies){
delete pair.second;
}
for (const std::pair<const QString, Preview*>& pair: *previews){ for (const std::pair<const QString, Preview*>& pair: *previews){
delete pair.second; delete pair.second;
} }
@ -95,8 +97,8 @@ MessageDelegate::~MessageDelegate()
delete idsToKeep; delete idsToKeep;
delete buttons; delete buttons;
delete bars; delete bars;
delete bodies;
delete previews; delete previews;
delete bodyRenderer;
} }
void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
@ -123,11 +125,6 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio
opt.displayAlignment = Qt::AlignRight | Qt::AlignTop; opt.displayAlignment = Qt::AlignRight | Qt::AlignTop;
} }
QSize bodySize(0, 0);
if (data.text.size() > 0) {
bodySize = bodyMetrics.boundingRect(opt.rect, Qt::TextWordWrap, data.text).size();
}
QRect rect; QRect rect;
if (ntds) { if (ntds) {
painter->setFont(nickFont); painter->setFont(nickFont);
@ -170,15 +167,7 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio
painter->restore(); painter->restore();
QWidget* vp = static_cast<QWidget*>(painter->device()); QWidget* vp = static_cast<QWidget*>(painter->device());
if (data.text.size() > 0) { paintBody(data, painter, opt);
QLabel* body = getBody(data);
body->setParent(vp);
body->setMinimumSize(bodySize);
body->setMaximumSize(bodySize);
body->move(opt.rect.left(), opt.rect.y());
body->show();
opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0);
}
painter->setFont(dateFont); painter->setFont(dateFont);
QColor q = painter->pen().color(); QColor q = painter->pen().color();
QString dateString = data.date.toLocalTime().toString("hh:mm"); QString dateString = data.date.toLocalTime().toString("hh:mm");
@ -306,7 +295,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi); Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
QSize messageSize(0, 0); QSize messageSize(0, 0);
if (data.text.size() > 0) { if (data.text.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size(); bodyRenderer->setPlainText(data.text);
bodyRenderer->setTextWidth(messageRect.size().width());
QSizeF size = bodyRenderer->size();
size.setWidth(bodyRenderer->idealWidth());
messageSize = QSize(std::ceil(size.width()), std::ceil(size.height()));
messageSize.rheight() += textMargin; messageSize.rheight() += textMargin;
} }
@ -367,6 +361,183 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel
return messageSize; return messageSize;
} }
QRect MessageDelegate::getHoveredMessageBodyRect(const QModelIndex& index, const Models::FeedItem& data, const QRect& sizeHint) const
{
QRect localHint = sizeHint.adjusted(bubbleMargin, bubbleMargin + margin, -bubbleMargin, -bubbleMargin / 2);
if (needToDrawSender(index, data)) {
localHint.adjust(0, nickMetrics.lineSpacing() + textMargin, 0, 0);
}
int attachHeight = 0;
switch (data.attach.state) {
case Models::none:
break;
case Models::uploading:
attachHeight += Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint).height() + textMargin;
[[fallthrough]];
case Models::downloading:
attachHeight += barHeight + textMargin;
break;
case Models::remote:
attachHeight += buttonHeight + textMargin;
break;
case Models::ready:
case Models::local: {
QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint);
attachHeight += aSize.height() + textMargin;
}
break;
case Models::errorDownload: {
QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size();
attachHeight += commentSize.height() + buttonHeight + textMargin * 2;
}
break;
case Models::errorUpload: {
QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint);
QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size();
attachHeight += aSize.height() + commentSize.height() + textMargin * 2;
}
break;
}
int bottomSize = std::max(dateMetrics.lineSpacing(), statusIconSize);
localHint.adjust(0, attachHeight, 0, -(bottomSize + textMargin));
return localHint;
}
QString MessageDelegate::getAnchor(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
if (data.text.size() > 0) {
QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint);
if (localHint.contains(point)) {
QPoint translated = point - localHint.topLeft();
bodyRenderer->setHtml(Shared::processMessageBody(data.text));
bodyRenderer->setTextWidth(localHint.size().width());
return bodyRenderer->documentLayout()->anchorAt(translated);
}
}
return QString();
}
void MessageDelegate::leftClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const
{
QString anchor = getAnchor(point, index, sizeHint);
if (anchor.size() > 0) {
emit openLink(anchor);
}
}
QString MessageDelegate::leftDoubleClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint)
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
if (data.text.size() > 0) {
QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint);
if (localHint.contains(point)) {
QPoint translated = point - localHint.topLeft();
bodyRenderer->setHtml(Shared::processMessageBody(data.text));
bodyRenderer->setTextWidth(localHint.size().width());
QAbstractTextDocumentLayout* lay = bodyRenderer->documentLayout();
int position = lay->hitTest(translated, Qt::HitTestAccuracy::FuzzyHit);
QTextCursor cursor(bodyRenderer);
cursor.setPosition(position, QTextCursor::MoveAnchor);
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
selection.first = cursor.anchor();
selection.second = cursor.position();
currentId = data.id;
if (selection.first != selection.second) {
return cursor.selectedText();
}
}
}
return "";
}
Shared::Hover MessageDelegate::hoverType(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
if (data.text.size() > 0) {
QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint);
if (localHint.contains(point)) {
QPoint translated = point - localHint.topLeft();
bodyRenderer->setHtml(Shared::processMessageBody(data.text));
bodyRenderer->setTextWidth(localHint.size().width());
QAbstractTextDocumentLayout* lay = bodyRenderer->documentLayout();
QString anchor = lay->anchorAt(translated);
if (anchor.size() > 0) {
return Shared::Hover::anchor;
} else {
int position = lay->hitTest(translated, Qt::HitTestAccuracy::ExactHit);
if (position != -1) {
return Shared::Hover::text;
}
}
}
}
return Shared::Hover::nothing;
}
QString MessageDelegate::mouseDrag(const QPoint& start, const QPoint& end, const QModelIndex& index, const QRect& sizeHint)
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
if (data.text.size() > 0) {
QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint);
if (localHint.contains(start)) {
QPoint tl = localHint.topLeft();
QPoint first = start - tl;
QPoint last = end - tl;
last.setX(std::max(last.x(), 0));
last.setX(std::min(last.x(), localHint.width() - 1));
last.setY(std::max(last.y(), 0));
last.setY(std::min(last.y(), localHint.height()));
bodyRenderer->setHtml(Shared::processMessageBody(data.text));
bodyRenderer->setTextWidth(localHint.size().width());
selection.first = bodyRenderer->documentLayout()->hitTest(first, Qt::HitTestAccuracy::FuzzyHit);
selection.second = bodyRenderer->documentLayout()->hitTest(last, Qt::HitTestAccuracy::FuzzyHit);
currentId = data.id;
if (selection.first != selection.second) {
QTextCursor cursor(bodyRenderer);
cursor.setPosition(selection.first, QTextCursor::MoveAnchor);
cursor.setPosition(selection.second, QTextCursor::KeepAnchor);
return cursor.selectedText();
}
}
}
return "";
}
QString MessageDelegate::clearSelection()
{
QString lastSelectedId = currentId;
currentId = "";
selection = std::pair(0, 0);
return lastSelectedId;
}
void MessageDelegate::initializeFonts(const QFont& font) void MessageDelegate::initializeFonts(const QFont& font)
{ {
bodyFont = font; bodyFont = font;
@ -390,9 +561,11 @@ void MessageDelegate::initializeFonts(const QFont& font)
dateFont.setPointSize(dateFont.pointSize() * dateFontMultiplier); dateFont.setPointSize(dateFont.pointSize() * dateFontMultiplier);
} }
bodyMetrics = QFontMetrics(bodyFont); bodyFont.setKerning(false);
nickMetrics = QFontMetrics(nickFont); nickMetrics = QFontMetrics(nickFont);
dateMetrics = QFontMetrics(dateFont); dateMetrics = QFontMetrics(dateFont);
bodyRenderer->setDefaultFont(bodyFont);
Preview::initializeFont(bodyFont); Preview::initializeFont(bodyFont);
} }
@ -572,34 +745,6 @@ QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const
return result; return result;
} }
QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const
{
std::map<QString, QLabel*>::const_iterator itr = bodies->find(data.id);
QLabel* result = 0;
if (itr != bodies->end()) {
result = itr->second;
} else {
result = new QLabel();
result->setFont(bodyFont);
result->setContextMenuPolicy(Qt::NoContextMenu);
result->setWordWrap(true);
result->setOpenExternalLinks(true);
result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
bodies->insert(std::make_pair(data.id, result));
}
result->setText(Shared::processMessageBody(data.text));
return result;
}
void MessageDelegate::beginClearWidgets()
{
idsToKeep->clear();
clearingWidgets = true;
}
template <typename T> template <typename T>
void removeElements(std::map<QString, T*>* elements, std::set<QString>* idsToKeep) { void removeElements(std::map<QString, T*>* elements, std::set<QString>* idsToKeep) {
std::set<QString> toRemove; std::set<QString> toRemove;
@ -614,6 +759,41 @@ void removeElements(std::map<QString, T*>* elements, std::set<QString>* idsToKee
} }
} }
int MessageDelegate::paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const
{
if (data.text.size() > 0) {
bodyRenderer->setHtml(Shared::processMessageBody(data.text));
bodyRenderer->setTextWidth(option.rect.size().width());
painter->save();
painter->translate(option.rect.topLeft());
if (data.id == currentId) {
QTextCursor cursor(bodyRenderer);
cursor.setPosition(selection.first, QTextCursor::MoveAnchor);
cursor.setPosition(selection.second, QTextCursor::KeepAnchor);
QTextCharFormat format = cursor.charFormat();
format.setBackground(option.palette.color(QPalette::Active, QPalette::Highlight));
format.setForeground(option.palette.color(QPalette::Active, QPalette::HighlightedText));
cursor.setCharFormat(format);
}
bodyRenderer->drawContents(painter);
painter->restore();
QSize bodySize(std::ceil(bodyRenderer->idealWidth()), std::ceil(bodyRenderer->size().height()));
option.rect.adjust(0, bodySize.height() + textMargin, 0, 0);
return bodySize.width();
}
return 0;
}
void MessageDelegate::beginClearWidgets()
{
idsToKeep->clear();
clearingWidgets = true;
}
void MessageDelegate::endClearWidgets() void MessageDelegate::endClearWidgets()
{ {
if (clearingWidgets) { if (clearingWidgets) {
@ -621,7 +801,6 @@ void MessageDelegate::endClearWidgets()
removeElements(bars, idsToKeep); removeElements(bars, idsToKeep);
removeElements(statusIcons, idsToKeep); removeElements(statusIcons, idsToKeep);
removeElements(pencilIcons, idsToKeep); removeElements(pencilIcons, idsToKeep);
removeElements(bodies, idsToKeep);
removeElements(previews, idsToKeep); removeElements(previews, idsToKeep);
idsToKeep->clear(); idsToKeep->clear();
@ -649,8 +828,3 @@ void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const
} }
} }
} }
// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
// {
//
// }

View File

@ -29,6 +29,7 @@
#include <QPushButton> #include <QPushButton>
#include <QProgressBar> #include <QProgressBar>
#include <QLabel> #include <QLabel>
#include <QTextDocument>
#include "shared/icons.h" #include "shared/icons.h"
#include "shared/global.h" #include "shared/global.h"
@ -56,6 +57,11 @@ public:
bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override; bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override;
void endClearWidgets(); void endClearWidgets();
void beginClearWidgets(); void beginClearWidgets();
void leftClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const;
QString leftDoubleClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint);
Shared::Hover hoverType(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const;
QString mouseDrag(const QPoint& start, const QPoint& end, const QModelIndex& index, const QRect& sizeHint);
QString clearSelection();
static int avatarHeight; static int avatarHeight;
static int margin; static int margin;
@ -63,23 +69,28 @@ public:
signals: signals:
void buttonPushed(const QString& messageId) const; void buttonPushed(const QString& messageId) const;
void invalidPath(const QString& messageId) const; void invalidPath(const QString& messageId) const;
void openLink(const QString& href) const;
protected: protected:
int paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; int paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
int paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; int paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
int paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; int paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
int paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; int paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
int paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
void paintAvatar(const Models::FeedItem& data, const QModelIndex& index, const QStyleOptionViewItem& option, QPainter* painter) const; void paintAvatar(const Models::FeedItem& data, const QModelIndex& index, const QStyleOptionViewItem& option, QPainter* painter) const;
void paintBubble(const Models::FeedItem& data, QPainter* painter, const QStyleOptionViewItem& option) const; void paintBubble(const Models::FeedItem& data, QPainter* painter, const QStyleOptionViewItem& option) const;
QPushButton* getButton(const Models::FeedItem& data) const; QPushButton* getButton(const Models::FeedItem& data) const;
QProgressBar* getBar(const Models::FeedItem& data) const; QProgressBar* getBar(const Models::FeedItem& data) const;
QLabel* getStatusIcon(const Models::FeedItem& data) const; QLabel* getStatusIcon(const Models::FeedItem& data) const;
QLabel* getPencilIcon(const Models::FeedItem& data) const; QLabel* getPencilIcon(const Models::FeedItem& data) const;
QLabel* getBody(const Models::FeedItem& data) const;
void clearHelperWidget(const Models::FeedItem& data) const; void clearHelperWidget(const Models::FeedItem& data) const;
bool needToDrawAvatar(const QModelIndex& index, const Models::FeedItem& data, const QStyleOptionViewItem& option) const; bool needToDrawAvatar(const QModelIndex& index, const Models::FeedItem& data, const QStyleOptionViewItem& option) const;
bool needToDrawSender(const QModelIndex& index, const Models::FeedItem& data) const; bool needToDrawSender(const QModelIndex& index, const Models::FeedItem& data) const;
QRect getHoveredMessageBodyRect(const QModelIndex& index, const Models::FeedItem& data, const QRect& sizeHint) const;
QString getAnchor(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const;
protected slots: protected slots:
void onButtonPushed() const; void onButtonPushed() const;
@ -93,7 +104,7 @@ private:
QFont bodyFont; QFont bodyFont;
QFont nickFont; QFont nickFont;
QFont dateFont; QFont dateFont;
QFontMetrics bodyMetrics; QTextDocument* bodyRenderer;
QFontMetrics nickMetrics; QFontMetrics nickMetrics;
QFontMetrics dateMetrics; QFontMetrics dateMetrics;
@ -105,11 +116,11 @@ private:
std::map<QString, QProgressBar*>* bars; std::map<QString, QProgressBar*>* bars;
std::map<QString, QLabel*>* statusIcons; std::map<QString, QLabel*>* statusIcons;
std::map<QString, QLabel*>* pencilIcons; std::map<QString, QLabel*>* pencilIcons;
std::map<QString, QLabel*>* bodies;
std::map<QString, Preview*>* previews; std::map<QString, Preview*>* previews;
std::set<QString>* idsToKeep; std::set<QString>* idsToKeep;
bool clearingWidgets; bool clearingWidgets;
QString currentId;
std::pair<int, int> selection;
}; };
#endif // MESSAGEDELEGATE_H #endif // MESSAGEDELEGATE_H

View File

@ -163,6 +163,12 @@ void Models::MessageFeed::changeMessage(const QString& id, const QMap<QString, Q
} }
emit dataChanged(index, index, cr); emit dataChanged(index, index, cr);
if (observersAmount == 0 && !msg->getForwarded() && changeRoles.count(MessageRoles::Text) > 0) {
unreadMessages->insert(id);
emit unreadMessagesCountChanged();
emit unnoticedMessage(*msg);
}
} }
} }
@ -312,12 +318,7 @@ QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
case Bulk: { case Bulk: {
FeedItem item; FeedItem item;
item.id = msg->getId(); item.id = msg->getId();
markMessageAsRead(item.id);
std::set<QString>::const_iterator umi = unreadMessages->find(item.id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCountChanged();
}
item.sentByMe = sentByMe(*msg); item.sentByMe = sentByMe(*msg);
item.date = msg->getTime(); item.date = msg->getTime();
@ -367,6 +368,17 @@ int Models::MessageFeed::rowCount(const QModelIndex& parent) const
return storage.size(); return storage.size();
} }
bool Models::MessageFeed::markMessageAsRead(const QString& id) const
{
std::set<QString>::const_iterator umi = unreadMessages->find(id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCountChanged();
return true;
}
return false;
}
unsigned int Models::MessageFeed::unreadMessagesCount() const unsigned int Models::MessageFeed::unreadMessagesCount() const
{ {
return unreadMessages->size(); return unreadMessages->size();

View File

@ -57,6 +57,8 @@ public:
void changeMessage(const QString& id, const QMap<QString, QVariant>& data); void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void removeMessage(const QString& id); void removeMessage(const QString& id);
Shared::Message getMessage(const QString& id); Shared::Message getMessage(const QString& id);
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@ -72,6 +74,7 @@ public:
void reportLocalPathInvalid(const QString& messageId); void reportLocalPathInvalid(const QString& messageId);
unsigned int unreadMessagesCount() const; unsigned int unreadMessagesCount() const;
bool markMessageAsRead(const QString& id) const;
void fileProgress(const QString& messageId, qreal value, bool up); void fileProgress(const QString& messageId, qreal value, bool up);
void fileError(const QString& messageId, const QString& error, bool up); void fileError(const QString& messageId, const QString& error, bool up);
void fileComplete(const QString& messageId, bool up); void fileComplete(const QString& messageId, bool up);
@ -125,8 +128,6 @@ protected:
bool sentByMe(const Shared::Message& msg) const; bool sentByMe(const Shared::Message& msg) const;
Attachment fillAttach(const Shared::Message& msg) const; Attachment fillAttach(const Shared::Message& msg) const;
Edition fillCorrection(const Shared::Message& msg) const; Edition fillCorrection(const Shared::Message& msg) const;
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const; std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const;
private: private:

View File

@ -18,6 +18,7 @@
#include "pagegeneral.h" #include "pagegeneral.h"
#include "ui_pagegeneral.h" #include "ui_pagegeneral.h"
#include "ui/squawk.h"
PageGeneral::PageGeneral(QWidget* parent): PageGeneral::PageGeneral(QWidget* parent):
QWidget(parent), QWidget(parent),
@ -28,7 +29,10 @@ PageGeneral::PageGeneral(QWidget* parent):
QSettings settings; QSettings settings;
m_ui->downloadsPathInput->setText(settings.value("downloadsPath").toString()); m_ui->downloadsPathInput->setText(settings.value("downloadsPath").toString());
m_ui->trayIconCheckbox->setChecked(settings.value("trayIconCheckbox").toBool());
connect(m_ui->downloadsPathButton, &QPushButton::clicked, this, &PageGeneral::onBrowseButtonClicked); connect(m_ui->downloadsPathButton, &QPushButton::clicked, this, &PageGeneral::onBrowseButtonClicked);
connect(m_ui->trayIconCheckbox, &QCheckBox::stateChanged, this, &PageGeneral::onTrayIconCheckboxChecked);
} }
PageGeneral::~PageGeneral() PageGeneral::~PageGeneral()
@ -76,3 +80,9 @@ void PageGeneral::onDialogDestroyed()
{ {
dialog = nullptr; dialog = nullptr;
} }
void PageGeneral::onTrayIconCheckboxChecked(){
QSettings settings;
settings.setValue("trayIconCheckbox", m_ui->trayIconCheckbox->isChecked());
Squawk::trayIcon->setVisible(m_ui->trayIconCheckbox->isChecked());
}

View File

@ -47,6 +47,7 @@ private slots:
void onBrowseButtonClicked(); void onBrowseButtonClicked();
void onDialogAccepted(); void onDialogAccepted();
void onDialogDestroyed(); void onDialogDestroyed();
void onTrayIconCheckboxChecked();
private: private:
QScopedPointer<Ui::PageGeneral> m_ui; QScopedPointer<Ui::PageGeneral> m_ui;

View File

@ -39,6 +39,19 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="trayIconCheckbox">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Hide Squawk to tray</string>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>