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

View File

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

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.>
Copyright (C) <year> <name of author>
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/>.

View File

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

View File

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

View File

@ -22,7 +22,7 @@
using namespace Core;
Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, NetworkAccess* p_net, QObject* parent):
Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, bool p_active, NetworkAccess* p_net, QObject* parent):
QObject(parent),
name(p_name),
archiveQueries(),
@ -41,13 +41,15 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
rcpm(new QXmppMessageReceiptManager()),
reconnectScheduled(false),
reconnectTimer(new QTimer),
avatarHash(),
avatarType(),
ownVCardRequestInProgress(false),
network(p_net),
passwordType(Shared::AccountPassword::plain),
lastError(Error::none),
pepSupport(false),
active(p_active),
notReadyPassword(false),
mh(new MessageHandler(this)),
rh(new RosterHandler(this))
rh(new RosterHandler(this)),
vh(new VCardHandler(this))
{
config.setUser(p_login);
config.setDomain(p_server);
@ -73,10 +75,6 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
client.addExtension(mm);
client.addExtension(bm);
QObject::connect(vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived);
//QObject::connect(&vm, &QXmppVCardManager::clientVCardReceived, this, &Account::onOwnVCardReceived); //for some reason it doesn't work, launching from common handler
client.addExtension(um);
QObject::connect(um, &QXmppUploadRequestManager::slotReceived, mh, &MessageHandler::onUploadSlotReceived);
QObject::connect(um, &QXmppUploadRequestManager::requestFailed, mh, &MessageHandler::onUploadSlotRequestFailed);
@ -91,62 +89,18 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
client.addExtension(rcpm);
QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived);
QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
path += "/" + name;
QDir dir(path);
if (!dir.exists()) {
bool res = dir.mkpath(path);
if (!res) {
qDebug() << "Couldn't create a cache directory for account" << name;
throw 22;
}
}
QFile* avatar = new QFile(path + "/avatar.png");
QString type = "png";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpg");
type = "jpg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.jpeg");
type = "jpeg";
if (!avatar->exists()) {
delete avatar;
avatar = new QFile(path + "/avatar.gif");
type = "gif";
}
}
}
if (avatar->exists()) {
if (avatar->open(QFile::ReadOnly)) {
QCryptographicHash sha1(QCryptographicHash::Sha1);
sha1.addData(avatar);
avatarHash = sha1.result();
avatarType = type;
}
}
if (avatarType.size() != 0) {
presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
presence.setPhotoHash(avatarHash.toUtf8());
} else {
presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady);
}
reconnectTimer->setSingleShot(true);
QObject::connect(reconnectTimer, &QTimer::timeout, this, &Account::onReconnectTimer);
// QXmppLogger* logger = new QXmppLogger(this);
// logger->setLoggingType(QXmppLogger::SignalLogging);
// client.setLogger(logger);
//
// QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){
// qDebug() << text;
// });
if (name == "Test") {
QXmppLogger* logger = new QXmppLogger(this);
logger->setLoggingType(QXmppLogger::SignalLogging);
client.setLogger(logger);
QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){
qDebug() << text;
});
}
}
Account::~Account()
@ -160,6 +114,7 @@ Account::~Account()
QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete);
QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError);
delete vh;
delete mh;
delete rh;
@ -185,7 +140,12 @@ void Core::Account::connect()
reconnectTimer->stop();
}
if (state == Shared::ConnectionState::disconnected) {
client.connectToServer(config, presence);
if (notReadyPassword) {
emit needPassword();
} else {
client.connectToServer(config, presence);
}
} else {
qDebug("An attempt to connect an account which is already connected, skipping");
}
@ -224,6 +184,7 @@ void Core::Account::onClientStateChange(QXmppClient::State st)
dm->requestItems(getServer());
dm->requestInfo(getServer());
}
lastError = Error::none;
emit connectionStateChanged(state);
}
} else {
@ -255,45 +216,17 @@ void Core::Account::onClientStateChange(QXmppClient::State st)
void Core::Account::reconnect()
{
if (state == Shared::ConnectionState::connected && !reconnectScheduled) {
reconnectScheduled = true;
reconnectTimer->start(500);
client.disconnectFromServer();
} else {
qDebug() << "An attempt to reconnect account" << getName() << "which was not connected";
if (!reconnectScheduled) { //TODO define behavior if It was connection or disconnecting
if (state == Shared::ConnectionState::connected) {
reconnectScheduled = true;
reconnectTimer->start(500);
client.disconnectFromServer();
} else {
qDebug() << "An attempt to reconnect account" << getName() << "which was not connected";
}
}
}
QString Core::Account::getName() const {
return name;}
QString Core::Account::getLogin() const {
return config.user();}
QString Core::Account::getPassword() const {
return config.password();}
QString Core::Account::getServer() const {
return config.domain();}
Shared::AccountPassword Core::Account::getPasswordType() const {
return passwordType;}
void Core::Account::setPasswordType(Shared::AccountPassword pt) {
passwordType = pt; }
void Core::Account::setLogin(const QString& p_login) {
config.setUser(p_login);}
void Core::Account::setName(const QString& p_name) {
name = p_name;}
void Core::Account::setPassword(const QString& p_password) {
config.setPassword(p_password);}
void Core::Account::setServer(const QString& p_server) {
config.setDomain(p_server);}
Shared::Availability Core::Account::getAvailability() const
{
if (state == Shared::ConnectionState::connected) {
@ -325,32 +258,11 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence)
QString jid = comps.front().toLower();
QString resource = comps.back();
QString myJid = getLogin() + "@" + getServer();
if (jid == myJid) {
if (jid == getBareJid()) {
if (resource == getResource()) {
emit availabilityChanged(static_cast<Shared::Availability>(p_presence.availableStatusType()));
} else {
if (!ownVCardRequestInProgress) {
switch (p_presence.vCardUpdateType()) {
case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo
break;
case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here
break;
case QXmppPresence::VCardUpdateNoPhoto: //there is no photo, need to drop if any
if (avatarType.size() > 0) {
vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
break;
case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load
if (avatarHash != p_presence.photoHash()) {
vm->requestClientVCard();
ownVCardRequestInProgress = true;
}
break;
}
}
vh->handleOtherPresenceOfMyAccountChange(p_presence);
}
} else {
RosterItem* item = rh->getRosterItem(jid);
@ -392,18 +304,6 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence)
}
}
QString Core::Account::getResource() const {
return config.resource();}
void Core::Account::setResource(const QString& p_resource) {
config.setResource(p_resource);}
QString Core::Account::getFullJid() const {
return getLogin() + "@" + getServer() + "/" + getResource();}
void Core::Account::sendMessage(const Shared::Message& data) {
mh->sendMessage(data);}
void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg)
{
if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) {
@ -517,6 +417,7 @@ void Core::Account::onClientError(QXmppClient::Error err)
qDebug() << "Error";
QString errorText;
QString errorType;
lastError = Error::other;
switch (err) {
case QXmppClient::SocketError:
errorText = client.socketErrorString();
@ -558,6 +459,7 @@ void Core::Account::onClientError(QXmppClient::Error err)
break;
case QXmppStanza::Error::NotAuthorized:
errorText = "Authentication error";
lastError = Error::authentication;
break;
#if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0)
case QXmppStanza::Error::PaymentRequired:
@ -667,6 +569,166 @@ void Core::Account::setRoomJoined(const QString& jid, bool joined)
conf->setJoined(joined);
}
void Core::Account::onDiscoveryItemsReceived(const QXmppDiscoveryIq& items)
{
if (items.from() == getServer()) {
std::set<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){
rh->removeRoomRequest(jid);}
@ -689,253 +751,9 @@ void Core::Account::renameContactRequest(const QString& jid, const QString& newN
}
}
void Core::Account::onVCardReceived(const QXmppVCardIq& card)
{
QString id = card.from();
QStringList comps = id.split("/");
QString jid = comps.front().toLower();
QString resource("");
if (comps.size() > 1) {
resource = comps.back();
}
pendingVCardRequests.erase(id);
RosterItem* item = rh->getRosterItem(jid);
if (item == 0) {
if (jid == getLogin() + "@" + getServer()) {
onOwnVCardReceived(card);
} else {
qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping";
}
return;
}
Shared::VCard vCard = item->handleResponseVCard(card, resource);
emit receivedVCard(jid, vCard);
}
void Core::Account::invalidatePassword() {
notReadyPassword = true;}
void Core::Account::onOwnVCardReceived(const QXmppVCardIq& card)
{
QByteArray ava = card.photo();
bool avaChanged = false;
QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/";
if (ava.size() > 0) {
QCryptographicHash sha1(QCryptographicHash::Sha1);
sha1.addData(ava);
QString newHash(sha1.result());
QMimeDatabase db;
QMimeType newType = db.mimeTypeForData(ava);
if (avatarType.size() > 0) {
if (avatarHash != newHash) {
QString oldPath = path + "avatar." + avatarType;
QFile oldAvatar(oldPath);
bool oldToRemove = false;
if (oldAvatar.exists()) {
if (oldAvatar.rename(oldPath + ".bak")) {
oldToRemove = true;
} else {
qDebug() << "Received new avatar for account" << name << "but can't get rid of the old one, doing nothing";
}
}
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
if (newAvatar.open(QFile::WriteOnly)) {
newAvatar.write(ava);
newAvatar.close();
avatarHash = newHash;
avatarType = newType.preferredSuffix();
avaChanged = true;
} else {
qDebug() << "Received new avatar for account" << name << "but can't save it";
if (oldToRemove) {
qDebug() << "rolling back to the old avatar";
if (!oldAvatar.rename(oldPath)) {
qDebug() << "Couldn't roll back to the old avatar in account" << name;
}
}
}
}
} else {
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
if (newAvatar.open(QFile::WriteOnly)) {
newAvatar.write(ava);
newAvatar.close();
avatarHash = newHash;
avatarType = newType.preferredSuffix();
avaChanged = true;
} else {
qDebug() << "Received new avatar for account" << name << "but can't save it";
}
}
} else {
if (avatarType.size() > 0) {
QFile oldAvatar(path + "avatar." + avatarType);
if (!oldAvatar.remove()) {
qDebug() << "Received vCard for account" << name << "without avatar, but can't get rid of the file, doing nothing";
} else {
avatarType = "";
avatarHash = "";
avaChanged = true;
}
}
}
if (avaChanged) {
QMap<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);}
Core::Account::Error Core::Account::getLastError() const {
return lastError;}

View File

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

View File

@ -1,5 +1,5 @@
/*
* Squawk messenger.
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* This program is free software: you can redistribute it and/or modify
@ -15,10 +15,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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)
{
@ -271,5 +269,3 @@ void Core::initializeQXmppVCard(QXmppVCardIq& iq, const Shared::VCard& card) {
iq.setEmails(emails);
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
rosterhandler.cpp
rosterhandler.h
vcardhandler.cpp
vcardhandler.h
)

View File

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

View File

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

View File

@ -64,6 +64,7 @@ public:
void storeConferences();
void clearConferences();
void setPepSupport(bool support);
private slots:
void onRosterReceived();
@ -107,6 +108,7 @@ private:
std::map<QString, std::set<QString>> groups;
std::map<QString, QString> queuedContacts;
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 "urlstorage.h"
#include "storage/urlstorage.h"
#include "shared/pathcheck.h"
namespace Core {

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -40,5 +40,5 @@ QString Shared::processMessageBody(const QString& msg)
{
QString processed = msg.toHtmlEscaped();
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(54, 54, 94)
};
enum class Hover {
nothing,
text,
anchor
};
}
#endif // SHARED_UTILS_H

View File

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

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"?>
<!DOCTYPE TS>
<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>
<name>Account</name>
<message>
@ -10,10 +112,12 @@
</message>
<message>
<source>Your account login</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Suas informações de login</translation>
</message>
<message>
<source>john_smith1987</source>
<translatorcomment>Login placeholder</translatorcomment>
<translation>josé_silva1987</translation>
</message>
<message>
@ -22,10 +126,12 @@
</message>
<message>
<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>
</message>
<message>
<source>macaw.me</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>macaw.me</translation>
</message>
<message>
@ -38,6 +144,7 @@
</message>
<message>
<source>Password of your account</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Senha da sua conta</translation>
</message>
<message>
@ -46,10 +153,11 @@
</message>
<message>
<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>
<source>John</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>José</translation>
</message>
<message>
@ -58,6 +166,7 @@
</message>
<message>
<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>
</message>
<message>
@ -69,6 +178,14 @@
<source>Password storage</source>
<translation>Armazenamento de senha</translation>
</message>
<message>
<source>Active</source>
<translation>Ativo</translation>
</message>
<message>
<source>enable</source>
<translation>habilitar</translation>
</message>
</context>
<context>
<name>Accounts</name>
@ -98,30 +215,135 @@
</message>
<message>
<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>
</context>
<context>
<name>Conversation</name>
<message>
<source>Type your message here...</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Digite sua mensagem aqui...</translation>
</message>
<message>
<source>Chose a file to send</source>
<translation>Escolha um arquivo para enviar</translation>
</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>
<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;
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>
<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>
<source>Drop files here to attach them to your message</source>
<translation>Arraste seus arquivos aqui para anexá-los a sua mensagem</translation>
<source>Paste Image</source>
<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>
</context>
<context>
@ -370,14 +592,14 @@ p, li { white-space: pre-wrap; }
<name>Message</name>
<message>
<source>Open</source>
<translation>Abrir</translation>
<translation type="vanished">Abrir</translation>
</message>
</context>
<context>
<name>MessageLine</name>
<message>
<source>Downloading...</source>
<translation>Baixando...</translation>
<translation type="vanished">Baixando...</translation>
</message>
<message>
<source>Download</source>
@ -386,28 +608,28 @@ p, li { white-space: pre-wrap; }
<message>
<source>Error uploading file: %1
You can try again</source>
<translation>Error fazendo upload do arquivo:
<translation type="vanished">Error fazendo upload do arquivo:
%1
Você pode tentar novamente</translation>
</message>
<message>
<source>Upload</source>
<translation>Upload</translation>
<translation type="vanished">Upload</translation>
</message>
<message>
<source>Error downloading file: %1
You can try again</source>
<translation>Erro baixando arquivo:
<translation type="vanished">Erro baixando arquivo:
%1
Você pode tentar novamente</translation>
</message>
<message>
<source>Uploading...</source>
<translation>Fazendo upload...</translation>
<translation type="vanished">Fazendo upload...</translation>
</message>
<message>
<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>
</context>
<context>
@ -481,7 +703,7 @@ Você pode tentar novamente</translation>
<name>NewContact</name>
<message>
<source>Add new contact</source>
<translatorcomment>Заголовок окна</translatorcomment>
<translatorcomment>Window title</translatorcomment>
<translation>Adicionar novo contato</translation>
</message>
<message>
@ -502,7 +724,7 @@ Você pode tentar novamente</translation>
</message>
<message>
<source>name@server.dmn</source>
<translatorcomment>Placeholder поля ввода JID</translatorcomment>
<translatorcomment>Placeholder</translatorcomment>
<translation>nome@servidor.com.br</translation>
</message>
<message>
@ -518,6 +740,66 @@ Você pode tentar novamente</translation>
<translation>José Silva</translation>
</message>
</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>
<name>Squawk</name>
<message>
@ -530,6 +812,7 @@ Você pode tentar novamente</translation>
</message>
<message>
<source>Squawk</source>
<translatorcomment>Menu bar entry</translatorcomment>
<translation>Squawk</translation>
</message>
<message>
@ -550,11 +833,11 @@ Você pode tentar novamente</translation>
</message>
<message>
<source>Disconnect</source>
<translation>Desconectar</translation>
<translation type="vanished">Desconectar</translation>
</message>
<message>
<source>Connect</source>
<translation>Conectar</translation>
<translation type="vanished">Conectar</translation>
</message>
<message>
<source>VCard</source>
@ -627,26 +910,46 @@ ser exibido com
</message>
<message>
<source>Attached file</source>
<translation>Arquivo anexado</translation>
<translation type="vanished">Arquivo anexado</translation>
</message>
<message>
<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>
<source>Password for account %1</source>
<translation>Senha para a conta %1</translation>
<translation type="vanished">Senha para a conta %1</translation>
</message>
<message>
<source>Please select a contact to start chatting</source>
<translation>Por favor selecione um contato para começar a conversar</translation>
</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>
<name>VCard</name>
<message>
<source>Received 12.07.2007 at 17.35</source>
<translation>Recebido 12/07/2007 às 17:35</translation>
<translation>Nunca atualizado</translation>
</message>
<message>
<source>General</source>
@ -682,7 +985,7 @@ ser exibido com
</message>
<message>
<source>Unit / Department</source>
<translation>Unidade/Departamento</translation>
<translation>Unidade / Departamento</translation>
</message>
<message>
<source>Role / Profession</source>

View File

@ -1,6 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<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>
<name>Account</name>
<message>
@ -10,10 +112,12 @@
</message>
<message>
<source>Your account login</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Имя пользователя Вашей учетной записи</translation>
</message>
<message>
<source>john_smith1987</source>
<translatorcomment>Login placeholder</translatorcomment>
<translation>ivan_ivanov1987</translation>
</message>
<message>
@ -22,10 +126,12 @@
</message>
<message>
<source>A server address of your account. Like 404.city or macaw.me</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Адресс сервера вашей учетной записи (выглядит как 404.city или macaw.me)</translation>
</message>
<message>
<source>macaw.me</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>macaw.me</translation>
</message>
<message>
@ -38,6 +144,7 @@
</message>
<message>
<source>Password of your account</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Пароль вашей учетной записи</translation>
</message>
<message>
@ -46,10 +153,11 @@
</message>
<message>
<source>Just a name how would you call this account, doesn&apos;t affect anything</source>
<translation>Просто имя, то как Вы называете свою учетную запись, может быть любым</translation>
<translation>Просто имя, то как Вы называете свою учетную запись, может быть любым (нельзя поменять)</translation>
</message>
<message>
<source>John</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Иван</translation>
</message>
<message>
@ -58,6 +166,7 @@
</message>
<message>
<source>A resource name like &quot;Home&quot; or &quot;Work&quot;</source>
<translatorcomment>Tooltip</translatorcomment>
<translation>Имя этой программы для ваших контактов, может быть &quot;Home&quot; или &quot;Phone&quot;</translation>
</message>
<message>
@ -69,6 +178,14 @@
<source>Password storage</source>
<translation>Хранение пароля</translation>
</message>
<message>
<source>Active</source>
<translation>Активен</translation>
</message>
<message>
<source>enable</source>
<translation>включен</translation>
</message>
</context>
<context>
<name>Accounts</name>
@ -98,30 +215,139 @@
</message>
<message>
<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>
</context>
<context>
<name>Conversation</name>
<message>
<source>Type your message here...</source>
<translatorcomment>Placeholder</translatorcomment>
<translation>Введите сообщение...</translation>
</message>
<message>
<source>Chose a file to send</source>
<translation>Выберите файл для отправки</translation>
</message>
<message>
<source>Drop files here to attach them to your message</source>
<translation>Бросьте файлы сюда для того что бы прикрепить их к сообщению</translation>
</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;
&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;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>
<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>
<source>Drop files here to attach them to your message</source>
<translation>Бросьте файлы сюда для того что бы прикрепить их к сообщению</translation>
<source>Paste Image</source>
<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>
</context>
<context>
@ -370,14 +596,14 @@ p, li { white-space: pre-wrap; }
<name>Message</name>
<message>
<source>Open</source>
<translation>Открыть</translation>
<translation type="vanished">Открыть</translation>
</message>
</context>
<context>
<name>MessageLine</name>
<message>
<source>Downloading...</source>
<translation>Скачивается...</translation>
<translation type="vanished">Скачивается...</translation>
</message>
<message>
<source>Download</source>
@ -386,28 +612,28 @@ p, li { white-space: pre-wrap; }
<message>
<source>Error uploading file: %1
You can try again</source>
<translation>Ошибка загрузки файла на сервер:
<translation type="vanished">Ошибка загрузки файла на сервер:
%1
Для того, что бы попробовать снова нажмите на кнопку</translation>
</message>
<message>
<source>Upload</source>
<translation>Загрузить</translation>
<translation type="vanished">Загрузить</translation>
</message>
<message>
<source>Error downloading file: %1
You can try again</source>
<translation>Ошибка скачивания файла:
<translation type="vanished">Ошибка скачивания файла:
%1
Вы можете попробовать снова</translation>
</message>
<message>
<source>Uploading...</source>
<translation>Загружается...</translation>
<translation type="vanished">Загружается...</translation>
</message>
<message>
<source>Push the button to download the file</source>
<translation>Нажмите на кнопку что бы загрузить файл</translation>
<translation type="vanished">Нажмите на кнопку что бы загрузить файл</translation>
</message>
</context>
<context>
@ -481,7 +707,7 @@ You can try again</source>
<name>NewContact</name>
<message>
<source>Add new contact</source>
<translatorcomment>Заголовок окна</translatorcomment>
<translatorcomment>Window title</translatorcomment>
<translation>Добавление нового контакта</translation>
</message>
<message>
@ -502,7 +728,7 @@ You can try again</source>
</message>
<message>
<source>name@server.dmn</source>
<translatorcomment>Placeholder поля ввода JID</translatorcomment>
<translatorcomment>Placeholder</translatorcomment>
<translation>name@server.dmn</translation>
</message>
<message>
@ -518,6 +744,64 @@ You can try again</source>
<translation>Иван Иванов</translation>
</message>
</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>
<name>Squawk</name>
<message>
@ -530,6 +814,7 @@ You can try again</source>
</message>
<message>
<source>Squawk</source>
<translatorcomment>Menu bar entry</translatorcomment>
<translation>Squawk</translation>
</message>
<message>
@ -550,11 +835,11 @@ You can try again</source>
</message>
<message>
<source>Disconnect</source>
<translation>Отключить</translation>
<translation type="vanished">Отключить</translation>
</message>
<message>
<source>Connect</source>
<translation>Подключить</translation>
<translation type="vanished">Подключить</translation>
</message>
<message>
<source>VCard</source>
@ -627,20 +912,40 @@ to be displayed as %1</source>
</message>
<message>
<source>Attached file</source>
<translation>Прикрепленный файл</translation>
<translation type="vanished">Прикрепленный файл</translation>
</message>
<message>
<source>Input the password for account %1</source>
<translation>Введите пароль для учетной записи %1</translation>
<translation type="vanished">Введите пароль для учетной записи %1</translation>
</message>
<message>
<source>Password for account %1</source>
<translation>Пароль для учетной записи %1</translation>
<translation type="vanished">Пароль для учетной записи %1</translation>
</message>
<message>
<source>Please select a contact to start chatting</source>
<translation>Выберите контакт или группу что бы начать переписку</translation>
</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>
<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(utils)

View File

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

View File

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

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()
{
QDateTime lastActivity;

View File

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

View File

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

View File

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

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)
{
Participant* part = itr->second;

View File

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

View File

@ -276,6 +276,18 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
break;
}
break;
case Qt::ForegroundRole:
switch (item->type) {
case Item::account: {
Account* acc = static_cast<Account*>(item);
if (!acc->getActive()) {
result = qApp->palette().brush(QPalette::Disabled, QPalette::Text);
}
}
break;
default:
break;
}
default:
break;
}
@ -451,6 +463,7 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons
connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid);
connect(contact, &Contact::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages);
contacts.insert(std::make_pair(id, contact));
} else {
contact = itr->second;
@ -536,8 +549,8 @@ void Models::Roster::removeGroup(const QString& account, const QString& name)
void Models::Roster::changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data)
{
Element* el = getElement({account, jid});
if (el != NULL) {
Element* el = getElement(ElId(account, jid));
if (el != nullptr) {
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
el->update(itr.key(), itr.value());
}
@ -546,8 +559,8 @@ void Models::Roster::changeContact(const QString& account, const QString& jid, c
void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data)
{
Element* el = getElement({account, jid});
if (el != NULL) {
Element* el = getElement(ElId(account, jid));
if (el != nullptr) {
el->changeMessage(id, data);
} else {
qDebug() << "A request to change a message of the contact " << jid << " in the account " << account << " but it wasn't found";
@ -694,8 +707,8 @@ void Models::Roster::removePresence(const QString& account, const QString& jid,
void Models::Roster::addMessage(const QString& account, const Shared::Message& data)
{
Element* el = getElement({account, data.getPenPalJid()});
if (el != NULL) {
Element* el = getElement(ElId(account, data.getPenPalJid()));
if (el != nullptr) {
el->addMessage(data);
}
}
@ -751,7 +764,7 @@ void Models::Roster::removeAccount(const QString& account)
acc->deleteLater();
}
QString Models::Roster::getContactName(const QString& account, const QString& jid)
QString Models::Roster::getContactName(const QString& account, const QString& jid) const
{
ElId id(account, jid);
std::map<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);
connect(room, &Contact::requestArchive, this, &Roster::onElementRequestArchive);
connect(room, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(room, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(room, &Contact::localPathInvalid, this, &Roster::localPathInvalid);
connect(room, &Room::requestArchive, this, &Roster::onElementRequestArchive);
connect(room, &Room::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(room, &Room::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(room, &Room::localPathInvalid, this, &Roster::localPathInvalid);
connect(room, &Room::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages);
rooms.insert(std::make_pair(id, room));
acc->appendChild(room);
}
@ -895,7 +909,7 @@ bool Models::Roster::groupHasContact(const QString& account, const QString& grou
}
}
QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource)
QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource) const
{
ElId id(account, jid);
std::map<ElId, Contact*>::const_iterator cItr = contacts.find(id);
@ -915,9 +929,36 @@ QString Models::Roster::getContactIconPath(const QString& account, const QString
return path;
}
Models::Account * Models::Roster::getAccount(const QString& name)
Models::Account * Models::Roster::getAccount(const QString& name) {
return const_cast<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)
@ -936,7 +977,7 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString&
if (itr == accounts.end()) {
return QModelIndex();
} 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()) {
return QModelIndex();
} else {
@ -946,6 +987,48 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString&
}
}
QModelIndex Models::Roster::getContactIndex(const QString& account, const QString& jid, const QString& resource)
{
std::map<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)
{
Element* el = static_cast<Element*>(sender());
@ -956,7 +1039,7 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid,
{
ElId id(account, jid);
Element* el = getElement(id);
if (el != NULL) {
if (el != nullptr) {
el->responseArchive(list, last);
}
}
@ -964,8 +1047,8 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid,
void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
Element* el = getElement(ElId(info.account, info.jid));
if (el != nullptr) {
el->fileProgress(info.messageId, value, up);
}
}
@ -974,8 +1057,8 @@ void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qr
void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
Element* el = getElement(ElId(info.account, info.jid));
if (el != nullptr) {
el->fileComplete(info.messageId, up);
}
}
@ -984,8 +1067,8 @@ void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bo
void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const QString& err, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
Element* el = getElement(ElId(info.account, info.jid));
if (el != nullptr) {
el->fileError(info.messageId, err, up);
}
}
@ -993,20 +1076,20 @@ void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const
Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id)
{
std::map<ElId, Contact*>::iterator cItr = contacts.find(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;
return const_cast<Models::Element*>(getElementConst(id));
}
Models::Item::Type Models::Roster::getContactType(const Models::Roster::ElId& id) const
{
const Models::Element* el = getElementConst(id);
if (el == nullptr) {
return Item::root;
}
return el->type;
}
void Models::Roster::onAccountReconnected()
{
Account* acc = static_cast<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();
public slots:
void addAccount(const QMap<QString, QVariant> &data);
void updateAccount(const QString& account, const QString& field, const QVariant& value);
void removeAccount(const QString& account);
@ -65,7 +66,12 @@ public:
void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<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);
QString getContactName(const QString& account, const QString& jid);
public:
QString getContactName(const QString& account, const QString& jid) const;
Item::Type getContactType(const Models::Roster::ElId& id) const;
const Element* getElementConst(const ElId& id) const;
Element* getElement(const ElId& id);
QVariant data ( const QModelIndex& index, int role ) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
@ -77,10 +83,13 @@ public:
std::deque<QString> groupList(const QString& account) const;
bool groupHasContact(const QString& account, const QString& group, const QString& contactJID) const;
QString getContactIconPath(const QString& account, const QString& jid, const QString& resource);
QString getContactIconPath(const QString& account, const QString& jid, const QString& resource) const;
Account* getAccount(const QString& name);
const Account* getAccountConst(const QString& name) const;
QModelIndex getAccountIndex(const QString& name);
QModelIndex getGroupIndex(const QString& account, const QString& name);
QModelIndex getContactIndex(const QString& account, const QString& jid, const QString& resource = "");
bool markMessageAsRead(const ElId& elementId, const QString& messageId);
void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last);
void fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up);
@ -92,12 +101,10 @@ public:
signals:
void requestArchive(const QString& account, const QString& jid, const QString& before);
void fileDownloadRequest(const QString& url);
void unreadMessagesCountChanged(int count);
void unnoticedMessage(const QString& account, const Shared::Message& msg);
void localPathInvalid(const QString& path);
private:
Element* getElement(const ElId& id);
private slots:
void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector<int>& roles);
void onAccountReconnected();
@ -109,7 +116,8 @@ private slots:
void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex);
void onChildMoved();
void onElementRequestArchive(const QString& before);
void recalculateUnreadMessages();
private:
Item* root;
std::map<QString, Account*> accounts;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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["resource"] = m_ui->resource->text();
map["passwordType"] = m_ui->passwordType->currentIndex();
map["active"] = m_ui->active->isChecked();
return map;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,9 @@
#include <QApplication>
#include <QMouseEvent>
#include <QAbstractItemView>
#include <QAbstractTextDocumentLayout>
#include <QTextBlock>
#include <cmath>
#include "messagedelegate.h"
#include "messagefeed.h"
@ -41,7 +44,7 @@ MessageDelegate::MessageDelegate(QObject* parent):
bodyFont(),
nickFont(),
dateFont(),
bodyMetrics(bodyFont),
bodyRenderer(new QTextDocument()),
nickMetrics(nickFont),
dateMetrics(dateFont),
buttonHeight(0),
@ -51,11 +54,14 @@ MessageDelegate::MessageDelegate(QObject* parent):
bars(new std::map<QString, QProgressBar*>()),
statusIcons(new std::map<QString, QLabel*>()),
pencilIcons(new std::map<QString, QLabel*>()),
bodies(new std::map<QString, QLabel*>()),
previews(new std::map<QString, Preview*>()),
idsToKeep(new std::set<QString>()),
clearingWidgets(false)
clearingWidgets(false),
currentId(""),
selection(0, 0)
{
bodyRenderer->setDocumentMargin(0);
QPushButton btn(QCoreApplication::translate("MessageLine", "Download"));
buttonHeight = btn.sizeHint().height();
buttonWidth = btn.sizeHint().width();
@ -82,10 +88,6 @@ MessageDelegate::~MessageDelegate()
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *bodies){
delete pair.second;
}
for (const std::pair<const QString, Preview*>& pair: *previews){
delete pair.second;
}
@ -95,8 +97,8 @@ MessageDelegate::~MessageDelegate()
delete idsToKeep;
delete buttons;
delete bars;
delete bodies;
delete previews;
delete bodyRenderer;
}
void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
@ -123,11 +125,6 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio
opt.displayAlignment = Qt::AlignRight | Qt::AlignTop;
}
QSize bodySize(0, 0);
if (data.text.size() > 0) {
bodySize = bodyMetrics.boundingRect(opt.rect, Qt::TextWordWrap, data.text).size();
}
QRect rect;
if (ntds) {
painter->setFont(nickFont);
@ -170,15 +167,7 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio
painter->restore();
QWidget* vp = static_cast<QWidget*>(painter->device());
if (data.text.size() > 0) {
QLabel* body = getBody(data);
body->setParent(vp);
body->setMinimumSize(bodySize);
body->setMaximumSize(bodySize);
body->move(opt.rect.left(), opt.rect.y());
body->show();
opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0);
}
paintBody(data, painter, opt);
painter->setFont(dateFont);
QColor q = painter->pen().color();
QString dateString = data.date.toLocalTime().toString("hh:mm");
@ -306,7 +295,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
QSize messageSize(0, 0);
if (data.text.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size();
bodyRenderer->setPlainText(data.text);
bodyRenderer->setTextWidth(messageRect.size().width());
QSizeF size = bodyRenderer->size();
size.setWidth(bodyRenderer->idealWidth());
messageSize = QSize(std::ceil(size.width()), std::ceil(size.height()));
messageSize.rheight() += textMargin;
}
@ -367,6 +361,183 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel
return messageSize;
}
QRect MessageDelegate::getHoveredMessageBodyRect(const QModelIndex& index, const Models::FeedItem& data, const QRect& sizeHint) const
{
QRect localHint = sizeHint.adjusted(bubbleMargin, bubbleMargin + margin, -bubbleMargin, -bubbleMargin / 2);
if (needToDrawSender(index, data)) {
localHint.adjust(0, nickMetrics.lineSpacing() + textMargin, 0, 0);
}
int attachHeight = 0;
switch (data.attach.state) {
case Models::none:
break;
case Models::uploading:
attachHeight += Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint).height() + textMargin;
[[fallthrough]];
case Models::downloading:
attachHeight += barHeight + textMargin;
break;
case Models::remote:
attachHeight += buttonHeight + textMargin;
break;
case Models::ready:
case Models::local: {
QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint);
attachHeight += aSize.height() + textMargin;
}
break;
case Models::errorDownload: {
QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size();
attachHeight += commentSize.height() + buttonHeight + textMargin * 2;
}
break;
case Models::errorUpload: {
QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint);
QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size();
attachHeight += aSize.height() + commentSize.height() + textMargin * 2;
}
break;
}
int bottomSize = std::max(dateMetrics.lineSpacing(), statusIconSize);
localHint.adjust(0, attachHeight, 0, -(bottomSize + textMargin));
return localHint;
}
QString MessageDelegate::getAnchor(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
Models::FeedItem data = qvariant_cast<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)
{
bodyFont = font;
@ -390,9 +561,11 @@ void MessageDelegate::initializeFonts(const QFont& font)
dateFont.setPointSize(dateFont.pointSize() * dateFontMultiplier);
}
bodyMetrics = QFontMetrics(bodyFont);
bodyFont.setKerning(false);
nickMetrics = QFontMetrics(nickFont);
dateMetrics = QFontMetrics(dateFont);
bodyRenderer->setDefaultFont(bodyFont);
Preview::initializeFont(bodyFont);
}
@ -572,34 +745,6 @@ QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const
return result;
}
QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const
{
std::map<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>
void removeElements(std::map<QString, T*>* elements, std::set<QString>* idsToKeep) {
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()
{
if (clearingWidgets) {
@ -621,7 +801,6 @@ void MessageDelegate::endClearWidgets()
removeElements(bars, idsToKeep);
removeElements(statusIcons, idsToKeep);
removeElements(pencilIcons, idsToKeep);
removeElements(bodies, idsToKeep);
removeElements(previews, idsToKeep);
idsToKeep->clear();
@ -649,8 +828,3 @@ void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const
}
}
}
// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
// {
//
// }

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,19 @@
</item>
</layout>
</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>
</widget>
<resources/>