Compare commits

...

48 Commits

Author SHA1 Message Date
antonpavanvo 1af20e27f2 fix: Pop up windows: About, Accounts, Account, Settings - opening on
centr of parent
2022-05-27 10:02:36 +04:00
antonpavanvo ea7dcc5f18 fix: About window now is a dialog 2022-05-26 19:00:18 +04:00
Blue 645b92ba51
release 0.2.2 preparation 2022-05-05 20:46:49 +03:00
Blue 80c5e2f2b4
added en lolcalization file, actualized localizations 2022-05-04 19:20:30 +03:00
Blue 1f065f23e6
double click word selection handle, sigint sermentation fault fix 2022-05-03 12:17:08 +03:00
Blue 3c48577eee
selection message body now actually working 2022-05-02 22:25:50 +03:00
Blue 0340db7f2f
first successfull attempt to visualize selection on message body 2022-05-01 23:19:52 +03:00
Blue c3a45ec58c
merge conflicts, text copying from context menu in message line 2022-04-30 21:41:25 +03:00
Blue 7ba94e9deb
link clicking and hovering in message body now works! 2022-04-29 00:29:44 +03:00
Blue eac87e713f
seem to have found a text block, to activate with the click later 2022-04-28 00:08:59 +03:00
Blue d86e2c28a0
an attempt to display text in a better way with QTextDocument + QTextBrowser 2022-04-27 01:17:53 +03:00
Blue 2fcc432aef
some polish 2022-04-26 23:08:25 +03:00
Blue e58213b294
Now notifications have actions! Some more usefull functions to roster model 2022-04-24 18:52:29 +03:00
Blue 3916aec358
unread messages count now is displayed on the launcher icon 2022-04-23 16:58:08 +03:00
Blue 721d3a1a89
refactoring: UI squawk now belongs to a new class, it enables me doing trayed mode, when main window is destroyed 2022-04-22 18:26:18 +03:00
Blue 83cb220175
better notification sending, edited message now modifies notification (or sends), little structure change 2022-04-19 20:24:41 +03:00
Blue 18859cb960
first ideas for notifications 2022-04-18 19:54:42 +03:00
Blue 4c20a314f0
a crash fix on one of archive corner cases 2022-04-17 16:25:15 +03:00
Blue 51ac1ac709
first attempt 2022-04-17 14:58:46 +03:00
Blue 8f949277f6
actual pasword reasking on failed authentication 2022-04-14 11:13:27 +03:00
Blue ce686e121b
account removal bugfix, some testing 2022-04-13 22:02:48 +03:00
Blue f64e5c2df0
account connect/disconnect now activate/deactivate, it's a bit less contraversial; async account password asking new concept 2022-04-12 23:33:10 +03:00
Blue 2c26c7e264
ui squawk refactoring 2022-04-11 18:45:12 +03:00
Blue 69e0c88d8d
account refactoring, pep support discovery started 2022-04-08 19:18:15 +03:00
Blue 82d54ba4df
Report bugs tab and thanks to tab in about widget 2022-04-07 18:26:43 +03:00
Blue 1b66fda318
License is now can be viewed locally, some organization name packaging issies 2022-04-05 22:00:56 +03:00
Blue 9f746d203b
new tab in About: components 2022-04-04 23:49:01 +03:00
Blue 27377e0ec5
first attempt to make About window 2022-04-03 23:53:46 +03:00
Blue 4baa3bccbf
new screenshot 2022-04-02 16:09:11 +03:00
Blue 4786388822
0.2.1 2022-04-02 15:53:23 +03:00
Blue 62f02c18d7
now you can't edit messages with attachments: no other client actually allowes that, and if I edit they don't handle it properly anyway 2022-04-02 15:34:36 +03:00
Blue 1fcd403dba
testing, solved unhandled exception, conditions to restrict old message to be edited, license un some files that used to miss them 2022-04-01 00:32:22 +03:00
Blue 5f6691067a
minor bugfixes about message body, automatic focus and that quirk with font becomming bigger 2022-03-29 19:05:24 +03:00
Blue 788c6ca556
now it's possible to fix your messages 2022-03-28 23:25:33 +03:00
Blue bf4a27f35d
Bug with the edited message fixed, some further work on message correction 2022-03-27 22:05:31 +03:00
Blue 0823b35148
removed unused old message line files, first thoughts on message edition 2022-02-20 22:10:09 +03:00
Blue 73b1b58a96
Downloads folder now is movable 2022-02-19 21:31:49 +03:00
Blue d8b5ccb2da
downloaded files now stored with squawk:// prefix, that way I can move downloads folder without messing up the database 2022-02-19 00:27:09 +03:00
Blue 243edff8bd
first thoughts about downloads path changing 2022-02-17 20:26:15 +03:00
Blue da19eb86bb
color theme setting is now working 2022-01-27 20:44:32 +03:00
Blue 0ff9f12157
new optional KDE Frameworks plugin to support system color schemes 2022-01-26 23:53:44 +03:00
Blue 802e2f11a1
may be a bit better quit handling 2022-01-25 23:35:55 +03:00
Blue c708c33a92
basic theme changing 2022-01-21 22:02:50 +03:00
Blue a8a7ce2538
some more thoughts about settings widgets 2022-01-19 23:46:42 +03:00
Blue 841e526e59
just some toying with designer 2022-01-17 23:52:07 +03:00
Blue 6bee149e6b
started to work on settings 2022-01-16 22:54:57 +03:00
Blue 62a59eb7a1
Added logs for Shura to help me to debug a download attachment issue 2022-01-15 15:36:49 +03:00
Blue 296328f12d
a bit of polish 2022-01-11 23:50:42 +03:00
114 changed files with 7881 additions and 2758 deletions

View File

@ -1,5 +1,47 @@
# Changelog # Changelog
## 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
- a bug where the correction message was received, the indication was on but the text didn't actually change
- message body now doesn't intecept context menu from the whole message
- message input now doesn't increase font when you remove everything from it
### 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 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) ## Squawk 0.2.0 (Jan 10, 2022)
### Bug fixes ### Bug fixes
- carbon copies switches on again after reconnection - carbon copies switches on again after reconnection

View File

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.4) cmake_minimum_required(VERSION 3.4)
project(squawk VERSION 0.1.6 LANGUAGES CXX) project(squawk VERSION 0.2.2 LANGUAGES CXX)
cmake_policy(SET CMP0076 NEW) cmake_policy(SET CMP0076 NEW)
cmake_policy(SET CMP0079 NEW) cmake_policy(SET CMP0079 NEW)
@ -29,9 +29,11 @@ target_include_directories(squawk PRIVATE ${CMAKE_SOURCE_DIR})
option(SYSTEM_QXMPP "Use system qxmpp lib" ON) option(SYSTEM_QXMPP "Use system qxmpp lib" ON)
option(WITH_KWALLET "Build KWallet support module" ON) option(WITH_KWALLET "Build KWallet support module" ON)
option(WITH_KIO "Build KIO support module" ON) option(WITH_KIO "Build KIO support module" ON)
option(WITH_KCONFIG "Build KConfig support module" ON)
# Dependencies # Dependencies
## Qt ## Qt
set(QT_VERSION_MAJOR 5)
find_package(Qt5 COMPONENTS Widgets DBus Gui Xml Network Core REQUIRED) find_package(Qt5 COMPONENTS Widgets DBus Gui Xml Network Core REQUIRED)
find_package(Boost COMPONENTS) find_package(Boost COMPONENTS)
@ -89,6 +91,24 @@ if (WITH_KWALLET)
endif () endif ()
endif () endif ()
if (WITH_KCONFIG)
find_package(KF5Config CONFIG)
if (NOT KF5Config_FOUND)
set(WITH_KCONFIG OFF)
message("KConfig package wasn't found, KConfig support modules wouldn't be built")
else()
find_package(KF5ConfigWidgets CONFIG)
if (NOT KF5ConfigWidgets_FOUND)
set(WITH_KCONFIG OFF)
message("KConfigWidgets package wasn't found, KConfigWidgets support modules wouldn't be built")
else()
target_compile_definitions(squawk PRIVATE WITH_KCONFIG)
message("Building with support of KConfig")
message("Building with support of KConfigWidgets")
endif()
endif()
endif()
## Signal (TODO) ## Signal (TODO)
# find_package(Signal REQUIRED) # find_package(Signal REQUIRED)
@ -114,14 +134,21 @@ endif ()
message("Build type: ${CMAKE_BUILD_TYPE}") message("Build type: ${CMAKE_BUILD_TYPE}")
if(CMAKE_COMPILER_IS_GNUCXX) if(CMAKE_COMPILER_IS_GNUCXX)
target_compile_options(squawk PRIVATE set (COMPILE_OPTIONS -fno-sized-deallocation) # for eliminating _ZdlPvm
"-Wall;-Wextra" if (CMAKE_BUILD_TYPE STREQUAL "Release")
"$<$<CONFIG:DEBUG>:-g>" list(APPEND COMPILE_OPTIONS -O3)
"$<$<CONFIG:RELEASE>:-O3>" endif()
"-fno-sized-deallocation" # for eliminating _ZdlPvm if (CMAKE_BUILD_TYPE STREQUAL Debug)
) list(APPEND COMPILE_OPTIONS -g)
list(APPEND COMPILE_OPTIONS -Wall)
list(APPEND COMPILE_OPTIONS -Wextra)
endif()
message("Compilation options: " ${COMPILE_OPTIONS})
target_compile_options(squawk PRIVATE ${COMPILE_OPTIONS})
endif(CMAKE_COMPILER_IS_GNUCXX) endif(CMAKE_COMPILER_IS_GNUCXX)
add_subdirectory(main)
add_subdirectory(core) add_subdirectory(core)
add_subdirectory(external/simpleCrypt) add_subdirectory(external/simpleCrypt)
add_subdirectory(packaging) add_subdirectory(packaging)
@ -133,6 +160,8 @@ add_subdirectory(ui)
# Install the executable # Install the executable
install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES README.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk)
install(FILES LICENSE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk)
if (CMAKE_BUILD_TYPE STREQUAL "Release") if (CMAKE_BUILD_TYPE STREQUAL "Release")
if (APPLE) if (APPLE)

View File

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

View File

@ -4,17 +4,20 @@
[![AUR version](https://img.shields.io/aur/version/squawk?style=flat-square)](https://aur.archlinux.org/packages/squawk/) [![AUR version](https://img.shields.io/aur/version/squawk?style=flat-square)](https://aur.archlinux.org/packages/squawk/)
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/macaw.me?logo=liberapay&style=flat-square)](https://liberapay.com/macaw.me) [![Liberapay patrons](https://img.shields.io/liberapay/patrons/macaw.me?logo=liberapay&style=flat-square)](https://liberapay.com/macaw.me)
![Squawk screenshot](https://macaw.me/images/squawk/0.2.0.png) ![Squawk screenshot](https://macaw.me/images/squawk/0.2.2.png)
### Prerequisites ### Prerequisites
- QT 5.12 *(lower versions might work but it wasn't tested)* - QT 5.12 *(lower versions might work but it wasn't tested)*
- lmdb - lmdb
- CMake 3.3 or higher - CMake 3.4 or higher
- qxmpp 1.1.0 or higher - qxmpp 1.1.0 or higher
- KDE Frameworks: kwallet (optional) - KDE Frameworks: kwallet (optional)
- KDE Frameworks: KIO (optional) - KDE Frameworks: KIO (optional)
- Boost - KDE Frameworks: KConfig (optional)
- KDE Frameworks: KConfigWidgets (optional)
- Boost (just one little hpp from there)
- Imagemagick (for compilation, to rasterize an SVG logo)
### Getting ### Getting
@ -91,6 +94,7 @@ Here is the list of keys you can pass to configuration phase of `cmake ..`.
- `SYSTEM_QXMPP` - `True` tries to link against `qxmpp` installed in the system, `False` builds bundled `qxmpp` library (default is `True`) - `SYSTEM_QXMPP` - `True` tries to link against `qxmpp` installed in the system, `False` builds bundled `qxmpp` library (default is `True`)
- `WITH_KWALLET` - `True` builds the `KWallet` capability module if `KWallet` is installed and if not goes to `False`. `False` disables `KWallet` support (default is `True`) - `WITH_KWALLET` - `True` builds the `KWallet` capability module if `KWallet` is installed and if not goes to `False`. `False` disables `KWallet` support (default is `True`)
- `WITH_KIO` - `True` builds the `KIO` capability module if `KIO` is installed and if not goes to `False`. `False` disables `KIO` support (default is `True`) - `WITH_KIO` - `True` builds the `KIO` capability module if `KIO` is installed and if not goes to `False`. `False` disables `KIO` support (default is `True`)
- `WITH_KCONFIG` - `True` builds the `KConfig` and `KConfigWidgets` capability module if such packages are installed and if not goes to `False`. `False` disables `KConfig` and `KConfigWidgets` support (default is `True`)
## License ## License

View File

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

View File

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

View File

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

View File

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

32
core/adapterfunctions.h Normal file
View File

@ -0,0 +1,32 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CORE_ADAPTER_FUNCTIONS_H
#define CORE_ADAPTER_FUNCTIONS_H
#include <QXmppVCardIq.h>
#include <shared/vcard.h>
namespace Core {
void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card);
void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard);
}
#endif // CORE_ADAPTER_FUNCTIONS_H

View File

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

View File

@ -41,10 +41,10 @@ void Core::MessageHandler::onMessageReceived(const QXmppMessage& msg)
handled = handleGroupMessage(msg); handled = handleGroupMessage(msg);
break; break;
case QXmppMessage::Error: { case QXmppMessage::Error: {
QString id = msg.id(); std::tuple<bool, QString, QString> ids = getOriginalPendingMessageId(msg.id());
std::map<QString, QString>::const_iterator itr = pendingStateMessages.find(id); if (std::get<0>(ids)) {
if (itr != pendingStateMessages.end()) { QString id = std::get<1>(ids);
QString jid = itr->second; QString jid = std::get<2>(ids);
RosterItem* cnt = acc->rh->getRosterItem(jid); RosterItem* cnt = acc->rh->getRosterItem(jid);
QMap<QString, QVariant> cData = { QMap<QString, QVariant> cData = {
{"state", static_cast<uint>(Shared::Message::State::error)}, {"state", static_cast<uint>(Shared::Message::State::error)},
@ -53,9 +53,7 @@ void Core::MessageHandler::onMessageReceived(const QXmppMessage& msg)
if (cnt != 0) { if (cnt != 0) {
cnt->changeMessage(id, cData); cnt->changeMessage(id, cData);
} }
;
emit acc->changeMessage(jid, id, cData); emit acc->changeMessage(jid, id, cData);
pendingStateMessages.erase(itr);
handled = true; handled = true;
} else { } else {
qDebug() << "received a message with type \"Error\", not sure what to do with it now, skipping"; qDebug() << "received a message with type \"Error\", not sure what to do with it now, skipping";
@ -80,6 +78,7 @@ bool Core::MessageHandler::handleChatMessage(const QXmppMessage& msg, bool outgo
Contact* cnt = acc->rh->getContact(jid); Contact* cnt = acc->rh->getContact(jid);
if (cnt == 0) { if (cnt == 0) {
cnt = acc->rh->addOutOfRosterContact(jid); cnt = acc->rh->addOutOfRosterContact(jid);
qDebug() << "appending message" << sMsg.getId() << "to an out of roster contact";
} }
if (outgoing) { if (outgoing) {
if (forwarded) { if (forwarded) {
@ -110,7 +109,6 @@ bool Core::MessageHandler::handleGroupMessage(const QXmppMessage& msg, bool outg
{ {
const QString& body(msg.body()); const QString& body(msg.body());
if (body.size() != 0) { if (body.size() != 0) {
QString id = msg.id();
Shared::Message sMsg(Shared::Message::groupChat); Shared::Message sMsg(Shared::Message::groupChat);
initializeMessage(sMsg, msg, outgoing, forwarded, guessing); initializeMessage(sMsg, msg, outgoing, forwarded, guessing);
@ -120,12 +118,11 @@ bool Core::MessageHandler::handleGroupMessage(const QXmppMessage& msg, bool outg
return false; return false;
} }
std::map<QString, QString>::const_iterator pItr = pendingStateMessages.find(id); std::tuple<bool, QString, QString> ids = getOriginalPendingMessageId(msg.id());
if (pItr != pendingStateMessages.end()) { if (std::get<0>(ids)) {
QMap<QString, QVariant> cData = {{"state", static_cast<uint>(Shared::Message::State::delivered)}}; QMap<QString, QVariant> cData = {{"state", static_cast<uint>(Shared::Message::State::delivered)}};
cnt->changeMessage(id, cData); cnt->changeMessage(std::get<1>(ids), cData);
pendingStateMessages.erase(pItr); emit acc->changeMessage(std::get<2>(ids), std::get<1>(ids), cData);
emit acc->changeMessage(jid, id, cData);
} else { } else {
QString oId = msg.replaceId(); QString oId = msg.replaceId();
if (oId.size() > 0) { if (oId.size() > 0) {
@ -162,6 +159,7 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
id = source.id(); id = source.id();
} }
target.setStanzaId(source.stanzaId()); target.setStanzaId(source.stanzaId());
qDebug() << "initializing message with originId:" << source.originId() << ", id:" << source.id() << ", stansaId:" << source.stanzaId();
#else #else
id = source.id(); id = source.id();
#endif #endif
@ -170,6 +168,7 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
if (messageId.size() == 0) { if (messageId.size() == 0) {
target.generateRandomId(); //TODO out of desperation, I need at least a random ID target.generateRandomId(); //TODO out of desperation, I need at least a random ID
messageId = target.getId(); messageId = target.getId();
qDebug() << "Had do initialize a message with no id, assigning autogenerated" << messageId;
} }
target.setFrom(source.from()); target.setFrom(source.from());
target.setTo(source.to()); target.setTo(source.to());
@ -177,7 +176,7 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
target.setForwarded(forwarded); target.setForwarded(forwarded);
if (guessing) { if (guessing) {
if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) { if (target.getFromJid() == acc->getBareJid()) {
outgoing = true; outgoing = true;
} else { } else {
outgoing = false; outgoing = false;
@ -224,53 +223,74 @@ void Core::MessageHandler::onCarbonMessageSent(const QXmppMessage& msg)
handleChatMessage(msg, true, true); handleChatMessage(msg, true, true);
} }
void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString& id) std::tuple<bool, QString, QString> Core::MessageHandler::getOriginalPendingMessageId(const QString& id)
{ {
std::tuple<bool, QString, QString> result({false, "", ""});
std::map<QString, QString>::const_iterator itr = pendingStateMessages.find(id); std::map<QString, QString>::const_iterator itr = pendingStateMessages.find(id);
if (itr != pendingStateMessages.end()) { if (itr != pendingStateMessages.end()) {
QMap<QString, QVariant> cData = {{"state", static_cast<uint>(Shared::Message::State::delivered)}}; std::get<0>(result) = true;
RosterItem* ri = acc->rh->getRosterItem(itr->second); std::get<2>(result) = itr->second;
if (ri != 0) {
ri->changeMessage(id, cData); std::map<QString, QString>::const_iterator itrC = pendingCorrectionMessages.find(id);
if (itrC != pendingCorrectionMessages.end()) {
if (itrC->second.size() > 0) {
std::get<1>(result) = itrC->second;
} else {
std::get<1>(result) = itr->first;
}
pendingCorrectionMessages.erase(itrC);
} else {
std::get<1>(result) = itr->first;
} }
emit acc->changeMessage(itr->second, id, cData);
pendingStateMessages.erase(itr); pendingStateMessages.erase(itr);
} }
return result;
} }
void Core::MessageHandler::sendMessage(const Shared::Message& data, bool newMessage) void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString& id)
{ {
if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) { std::tuple<bool, QString, QString> ids = getOriginalPendingMessageId(id);
prepareUpload(data, newMessage); if (std::get<0>(ids)) {
} else { QMap<QString, QVariant> cData = {{"state", static_cast<uint>(Shared::Message::State::delivered)}};
performSending(data, newMessage); RosterItem* ri = acc->rh->getRosterItem(std::get<2>(ids));
if (ri != 0) {
ri->changeMessage(std::get<1>(ids), cData);
}
emit acc->changeMessage(std::get<2>(ids), std::get<1>(ids), cData);
} }
} }
void Core::MessageHandler::performSending(Shared::Message data, bool newMessage) void Core::MessageHandler::sendMessage(const Shared::Message& data, bool newMessage, QString originalId)
{
if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) {
pendingCorrectionMessages.insert(std::make_pair(data.getId(), originalId));
prepareUpload(data, newMessage);
} else {
performSending(data, originalId, newMessage);
}
}
void Core::MessageHandler::performSending(Shared::Message data, const QString& originalId, bool newMessage)
{ {
QString jid = data.getPenPalJid(); QString jid = data.getPenPalJid();
QString id = data.getId(); QString id = data.getId();
QString oob = data.getOutOfBandUrl(); qDebug() << "Sending message with id:" << id;
if (originalId.size() > 0) {
qDebug() << "To replace one with id:" << originalId;
}
RosterItem* ri = acc->rh->getRosterItem(jid); RosterItem* ri = acc->rh->getRosterItem(jid);
bool sent = false; bool sent = false;
QMap<QString, QVariant> changes; if (newMessage && originalId.size() > 0) {
newMessage = false;
}
QDateTime sendTime = QDateTime::currentDateTimeUtc(); QDateTime sendTime = QDateTime::currentDateTimeUtc();
if (acc->state == Shared::ConnectionState::connected) { if (acc->state == Shared::ConnectionState::connected) {
QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread()); QXmppMessage msg(createPacket(data, sendTime, originalId));
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 3, 0)
msg.setOriginId(id);
#endif
msg.setId(id);
msg.setType(static_cast<QXmppMessage::Type>(data.getType())); //it is safe here, my type is compatible
msg.setOutOfBandUrl(oob);
msg.setReceiptRequested(true);
msg.setStamp(sendTime);
sent = acc->client.sendPacket(msg); sent = acc->client.sendPacket(msg);
//sent = false;
if (sent) { if (sent) {
data.setState(Shared::Message::State::sent); data.setState(Shared::Message::State::sent);
} else { } else {
@ -283,6 +303,39 @@ void Core::MessageHandler::performSending(Shared::Message data, bool newMessage)
data.setErrorText("You are is offline or reconnecting"); data.setErrorText("You are is offline or reconnecting");
} }
QMap<QString, QVariant> changes(getChanges(data, sendTime, newMessage, originalId));
QString realId;
if (originalId.size() > 0) {
realId = originalId;
} else {
realId = id;
}
if (ri != 0) {
if (newMessage) {
ri->appendMessageToArchive(data);
} else {
ri->changeMessage(realId, changes);
}
if (sent) {
pendingStateMessages.insert(std::make_pair(id, jid));
if (originalId.size() > 0) {
pendingCorrectionMessages.insert(std::make_pair(id, originalId));
}
} else {
pendingStateMessages.erase(id);
pendingCorrectionMessages.erase(id);
}
}
emit acc->changeMessage(jid, realId, changes);
}
QMap<QString, QVariant> Core::MessageHandler::getChanges(Shared::Message& data, const QDateTime& time, bool newMessage, const QString& originalId) const
{
QMap<QString, QVariant> changes;
QString oob = data.getOutOfBandUrl();
Shared::Message::State mstate = data.getState(); Shared::Message::State mstate = data.getState();
changes.insert("state", static_cast<uint>(mstate)); changes.insert("state", static_cast<uint>(mstate));
if (mstate == Shared::Message::State::error) { if (mstate == Shared::Message::State::error) {
@ -292,24 +345,47 @@ void Core::MessageHandler::performSending(Shared::Message data, bool newMessage)
changes.insert("outOfBandUrl", oob); changes.insert("outOfBandUrl", oob);
} }
if (newMessage) { if (newMessage) {
data.setTime(sendTime); data.setTime(time);
} }
changes.insert("stamp", sendTime); if (originalId.size() > 0) {
changes.insert("body", data.getBody());
if (ri != 0) { }
if (newMessage) { changes.insert("stamp", time);
ri->appendMessageToArchive(data);
} else { //sometimes (when the image is pasted with ctrl+v)
ri->changeMessage(id, changes); //I start sending message with one path, then copy it to downloads directory
} //so, the final path changes. Let's assume it changes always since it costs me close to nothing
if (sent) { QString attachPath = data.getAttachPath();
pendingStateMessages.insert(std::make_pair(id, jid)); if (attachPath.size() > 0) {
} else { QString squawkified = Shared::squawkifyPath(attachPath);
pendingStateMessages.erase(id); changes.insert("attachPath", squawkified);
if (attachPath != squawkified) {
data.setAttachPath(squawkified);
} }
} }
emit acc->changeMessage(jid, id, changes); return changes;
}
QXmppMessage Core::MessageHandler::createPacket(const Shared::Message& data, const QDateTime& time, const QString& originalId) const
{
QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread());
QString id(data.getId());
if (originalId.size() > 0) {
msg.setReplaceId(originalId);
}
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 3, 0)
msg.setOriginId(id);
#endif
msg.setId(id);
msg.setType(static_cast<QXmppMessage::Type>(data.getType())); //it is safe here, my type is compatible
msg.setOutOfBandUrl(data.getOutOfBandUrl());
msg.setReceiptRequested(true);
msg.setStamp(time);
return msg;
} }
void Core::MessageHandler::prepareUpload(const Shared::Message& data, bool newMessage) void Core::MessageHandler::prepareUpload(const Shared::Message& data, bool newMessage)
@ -429,21 +505,23 @@ void Core::MessageHandler::onLoadFileError(const std::list<Shared::MessageInfo>&
void Core::MessageHandler::handleUploadError(const QString& jid, const QString& messageId, const QString& errorText) void Core::MessageHandler::handleUploadError(const QString& jid, const QString& messageId, const QString& errorText)
{ {
emit acc->uploadFileError(jid, messageId, "Error requesting slot to upload file: " + errorText); emit acc->uploadFileError(jid, messageId, "Error requesting slot to upload file: " + errorText);
pendingStateMessages.erase(jid); pendingStateMessages.erase(messageId);
pendingCorrectionMessages.erase(messageId);
requestChangeMessage(jid, messageId, { requestChangeMessage(jid, messageId, {
{"state", static_cast<uint>(Shared::Message::State::error)}, {"state", static_cast<uint>(Shared::Message::State::error)},
{"errorText", errorText} {"errorText", errorText}
}); });
} }
void Core::MessageHandler::onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path) void Core::MessageHandler::onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& url, const QString& path)
{ {
for (const Shared::MessageInfo& info : msgs) { for (const Shared::MessageInfo& info : msgs) {
if (info.account == acc->getName()) { if (info.account == acc->getName()) {
RosterItem* ri = acc->rh->getRosterItem(info.jid); RosterItem* ri = acc->rh->getRosterItem(info.jid);
if (ri != 0) { if (ri != 0) {
Shared::Message msg = ri->getMessage(info.messageId); Shared::Message msg = ri->getMessage(info.messageId);
sendMessageWithLocalUploadedFile(msg, path, false); msg.setAttachPath(path);
sendMessageWithLocalUploadedFile(msg, url, false);
} else { } else {
qDebug() << "A signal received about complete upload to" << acc->name << "for pal" << info.jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send"; qDebug() << "A signal received about complete upload to" << acc->name << "for pal" << info.jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send";
} }
@ -457,11 +535,11 @@ void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg,
if (msg.getBody().size() == 0) { //not sure why, but most messages do that if (msg.getBody().size() == 0) { //not sure why, but most messages do that
msg.setBody(url); //they duplicate oob in body, some of them wouldn't even show an attachment if you don't do that msg.setBody(url); //they duplicate oob in body, some of them wouldn't even show an attachment if you don't do that
} }
performSending(msg, newMessage); performSending(msg, pendingCorrectionMessages.at(msg.getId()), newMessage);
//TODO removal/progress update //TODO removal/progress update
} }
static const std::set<QString> allowerToChangeKeys({ static const std::set<QString> allowedToChangeKeys({
"attachPath", "attachPath",
"outOfBandUrl", "outOfBandUrl",
"state", "state",
@ -474,12 +552,12 @@ void Core::MessageHandler::requestChangeMessage(const QString& jid, const QStrin
if (cnt != 0) { if (cnt != 0) {
bool allSupported = true; bool allSupported = true;
QString unsupportedString; QString unsupportedString;
for (QMap<QString, QVariant>::const_iterator itr = data.begin(); itr != data.end(); ++itr) { //I need all this madness for (QMap<QString, QVariant>::const_iterator itr = data.begin(); itr != data.end(); ++itr) { //I need all this madness
if (allowerToChangeKeys.count(itr.key()) != 1) { //to not allow this method if (allowedToChangeKeys.count(itr.key()) != 1) { //to not allow this method
allSupported = false; //to make a message to look like if it was edited allSupported = false; //to make a message to look like if it was edited
unsupportedString = itr.key(); //basically I needed to control who exaclty calls this method unsupportedString = itr.key(); //basically I needed to control who exaclty calls this method
break; //because the underlying tech assumes that the change is initiated by user break; //because the underlying tech assumes that
} //not by system } //the change is initiated by user, not by system
} }
if (allSupported) { if (allSupported) {
cnt->changeMessage(messageId, data); cnt->changeMessage(messageId, data);
@ -498,7 +576,13 @@ void Core::MessageHandler::resendMessage(const QString& jid, const QString& id)
try { try {
Shared::Message msg = cnt->getMessage(id); Shared::Message msg = cnt->getMessage(id);
if (msg.getState() == Shared::Message::State::error) { if (msg.getState() == Shared::Message::State::error) {
sendMessage(msg, false); if (msg.getEdited()){
QString originalId = msg.getId();
msg.generateRandomId();
sendMessage(msg, false, originalId);
} else {
sendMessage(msg, false);
}
} else { } else {
qDebug() << "An attempt to resend a message to" << jid << "by account" << acc->getName() << ", but this message seems to have been normally sent, this method was made to retry sending failed to be sent messages, skipping"; qDebug() << "An attempt to resend a message to" << jid << "by account" << acc->getName() << ", but this message seems to have been normally sent, this method was made to retry sending failed to be sent messages, skipping";
} }

View File

@ -29,6 +29,7 @@
#include <shared/message.h> #include <shared/message.h>
#include <shared/messageinfo.h> #include <shared/messageinfo.h>
#include <shared/pathcheck.h>
namespace Core { namespace Core {
@ -45,7 +46,7 @@ public:
MessageHandler(Account* account); MessageHandler(Account* account);
public: public:
void sendMessage(const Shared::Message& data, bool newMessage = true); void sendMessage(const Shared::Message& data, bool newMessage = true, QString originalId = "");
void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const; void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const;
void resendMessage(const QString& jid, const QString& id); void resendMessage(const QString& jid, const QString& id);
@ -57,7 +58,7 @@ public slots:
void onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot); void onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot);
void onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request); void onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request);
void onDownloadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path); void onDownloadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path);
void onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path); void onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& url, const QString& path);
void onLoadFileError(const std::list<Shared::MessageInfo>& msgs, const QString& path, bool up); void onLoadFileError(const std::list<Shared::MessageInfo>& msgs, const QString& path, bool up);
void requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data); void requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data);
@ -66,13 +67,17 @@ private:
bool handleGroupMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); bool handleGroupMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false);
void logMessage(const QXmppMessage& msg, const QString& reason = "Message wasn't handled: "); void logMessage(const QXmppMessage& msg, const QString& reason = "Message wasn't handled: ");
void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage = true); void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage = true);
void performSending(Shared::Message data, bool newMessage = true); void performSending(Shared::Message data, const QString& originalId, bool newMessage = true);
void prepareUpload(const Shared::Message& data, bool newMessage = true); void prepareUpload(const Shared::Message& data, bool newMessage = true);
void handleUploadError(const QString& jid, const QString& messageId, const QString& errorText); void handleUploadError(const QString& jid, const QString& messageId, const QString& errorText);
QXmppMessage createPacket(const Shared::Message& data, const QDateTime& time, const QString& originalId) const;
QMap<QString, QVariant> getChanges(Shared::Message& data, const QDateTime& time, bool newMessage, const QString& originalId) const;
std::tuple<bool, QString, QString> getOriginalPendingMessageId(const QString& id);
private: private:
Account* acc; Account* acc;
std::map<QString, QString> pendingStateMessages; //key is message id, value is JID std::map<QString, QString> pendingStateMessages; //key is message id, value is JID
std::map<QString, QString> pendingCorrectionMessages; //key is new mesage, value is originalOne
std::deque<std::pair<QString, QString>> uploadingSlotsQueue; std::deque<std::pair<QString, QString>> uploadingSlotsQueue;
}; };

View File

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

View File

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

View File

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

View File

@ -0,0 +1,65 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef CORE_VCARDHANDLER_H
#define CORE_VCARDHANDLER_H
#include <set>
#include <QXmppVCardIq.h>
#include <QXmppPresence.h>
#include <QObject>
#include <shared/vcard.h>
#include <core/adapterfunctions.h>
/**
* @todo write docs
*/
namespace Core {
class Account;
class VCardHandler : public QObject
{
Q_OBJECT
public:
VCardHandler(Account* account);
~VCardHandler();
void handleOffline();
void requestVCard(const QString& jid);
void handleOtherPresenceOfMyAccountChange(const QXmppPresence& p_presence);
void uploadVCard(const Shared::VCard& card);
QString getAvatarPath() const;
private slots:
void onVCardReceived(const QXmppVCardIq& card);
void onOwnVCardReceived(const QXmppVCardIq& card);
private:
Account* acc;
bool ownVCardRequestInProgress;
std::set<QString> pendingVCardRequests;
QString avatarHash;
QString avatarType;
};
}
#endif // CORE_VCARDHANDLER_H

View File

@ -1,172 +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 "../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>
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.0");
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
Squawk w;
w.show();
Core::Squawk* squawk = new Core::Squawk();
QThread* coreThread = new QThread();
squawk->moveToThread(coreThread);
QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start);
QObject::connect(&app, &QApplication::aboutToQuit, squawk, &Core::Squawk::stop);
QObject::connect(&app, &QApplication::aboutToQuit, &w, &QMainWindow::close);
QObject::connect(squawk, &Core::Squawk::quit, coreThread, &QThread::quit);
QObject::connect(coreThread, &QThread::finished, squawk, &Core::Squawk::deleteLater);
QObject::connect(&w, &Squawk::newAccountRequest, squawk, &Core::Squawk::newAccountRequest);
QObject::connect(&w, &Squawk::modifyAccountRequest, squawk, &Core::Squawk::modifyAccountRequest);
QObject::connect(&w, &Squawk::removeAccountRequest, squawk, &Core::Squawk::removeAccountRequest);
QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount);
QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount);
QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState);
QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage);
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(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();
w.writeSettings();
coreThread->wait(500); //TODO hate doing that but settings for some reason don't get saved to the disk
return result;
}

View File

@ -28,8 +28,11 @@ Core::NetworkAccess::NetworkAccess(QObject* parent):
manager(0), manager(0),
storage("fileURLStorage"), storage("fileURLStorage"),
downloads(), downloads(),
uploads() uploads(),
currentPath()
{ {
QSettings settings;
currentPath = settings.value("downloadsPath").toString();
} }
Core::NetworkAccess::~NetworkAccess() Core::NetworkAccess::~NetworkAccess()
@ -302,7 +305,7 @@ void Core::NetworkAccess::onDownloadFinished()
if (path.size() > 0) { if (path.size() > 0) {
path = checkFileName(fileName, path); path = checkFileName(fileName, path);
QFile file(path); QFile file(Shared::resolvePath(path));
if (file.open(QIODevice::WriteOnly)) { if (file.open(QIODevice::WriteOnly)) {
file.write(dwn->reply->readAll()); file.write(dwn->reply->readAll());
file.close(); file.close();
@ -376,23 +379,20 @@ void Core::NetworkAccess::onUploadFinished()
Transfer* upl = itr->second; Transfer* upl = itr->second;
if (upl->success) { if (upl->success) {
qDebug() << "upload success for" << url; qDebug() << "upload success for" << url;
storage.addFile(upl->messages, upl->url, upl->path);
emit uploadFileComplete(upl->messages, upl->url, upl->path);
// Copy file to Download folder if it is a temp file. See Conversation::onImagePasted. // Copy file to Download folder if it is a temp file. See Conversation::onImagePasted.
if (upl->path.startsWith(QDir::tempPath() + QStringLiteral("/squawk_img_attach_")) && upl->path.endsWith(".png")) { if (upl->path.startsWith(QDir::tempPath() + QDir::separator() + QStringLiteral("squawk_img_attach_")) && upl->path.endsWith(".png")) {
QString err = ""; QString err = "";
QString downloadDirPath = prepareDirectory(upl->messages.front().jid); QString downloadDirPath = prepareDirectory(upl->messages.front().jid);
if (downloadDirPath.size() > 0) { if (downloadDirPath.size() > 0) {
QString newPath = downloadDirPath + "/" + upl->path.mid(QDir::tempPath().length() + 1); QString newPath = downloadDirPath + QDir::separator() + upl->path.mid(QDir::tempPath().length() + 1);
// Copy {TEMPDIR}/squawk_img_attach_XXXXXX.png to Download folder // Copy {TEMPDIR}/squawk_img_attach_XXXXXX.png to Download folder
bool copyResult = QFile::copy(upl->path, newPath); bool copyResult = QFile::copy(upl->path, Shared::resolvePath(newPath));
if (copyResult) { if (copyResult) {
// Change storage // Change storage
storage.setPath(upl->url, newPath); upl->path = newPath;
} else { } else {
err = "copying to " + newPath + " failed"; err = "copying to " + newPath + " failed";
} }
@ -404,6 +404,9 @@ void Core::NetworkAccess::onUploadFinished()
qDebug() << "failed to copy temporary upload file " << upl->path << " to download folder:" << err; qDebug() << "failed to copy temporary upload file " << upl->path << " to download folder:" << err;
} }
} }
storage.addFile(upl->messages, upl->url, upl->path);
emit uploadFileComplete(upl->messages, upl->url, upl->path);
} }
upl->reply->deleteLater(); upl->reply->deleteLater();
@ -435,12 +438,12 @@ void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTot
QString Core::NetworkAccess::getFileRemoteUrl(const QString& path) QString Core::NetworkAccess::getFileRemoteUrl(const QString& path)
{ {
QString p; QString p = Shared::squawkifyPath(path);
try { try {
p = storage.getUrl(path); p = storage.getUrl(p);
} catch (const Archive::NotFound& err) { } catch (const Archive::NotFound& err) {
p = "";
} catch (...) { } catch (...) {
throw; throw;
} }
@ -515,11 +518,13 @@ bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QStri
QString Core::NetworkAccess::prepareDirectory(const QString& jid) QString Core::NetworkAccess::prepareDirectory(const QString& jid)
{ {
QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); QString path = currentPath;
path += "/" + QApplication::applicationName(); QString addition;
if (jid.size() > 0) { if (jid.size() > 0) {
path += "/" + jid; addition = jid;
path += QDir::separator() + jid;
} }
QDir location(path); QDir location(path);
if (!location.exists()) { if (!location.exists()) {
@ -527,10 +532,10 @@ QString Core::NetworkAccess::prepareDirectory(const QString& jid)
if (!res) { if (!res) {
return ""; return "";
} else { } else {
return path; return "squawk://" + addition;
} }
} }
return path; return "squawk://" + addition;
} }
QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path) QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path)
@ -544,14 +549,17 @@ QString Core::NetworkAccess::checkFileName(const QString& name, const QString& p
suffix += "." + (*sItr); suffix += "." + (*sItr);
} }
QString postfix(""); QString postfix("");
QFileInfo proposedName(path + "/" + realName + suffix); QString resolvedPath = Shared::resolvePath(path);
QString count("");
QFileInfo proposedName(resolvedPath + QDir::separator() + realName + count + suffix);
int counter = 0; int counter = 0;
while (proposedName.exists()) { while (proposedName.exists()) {
QString count = QString("(") + std::to_string(++counter).c_str() + ")"; count = QString("(") + std::to_string(++counter).c_str() + ")";
proposedName = QFileInfo(path + "/" + realName + count + suffix); proposedName = QFileInfo(resolvedPath + QDir::separator() + realName + count + suffix);
} }
return proposedName.absoluteFilePath(); return path + QDir::separator() + realName + count + suffix;
} }
QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id)
@ -563,3 +571,19 @@ std::list<Shared::MessageInfo> Core::NetworkAccess::reportPathInvalid(const QStr
{ {
return storage.deletedFile(path); return storage.deletedFile(path);
} }
void Core::NetworkAccess::moveFilesDirectory(const QString& newPath)
{
QDir dir(currentPath);
bool success = true;
qDebug() << "moving" << currentPath << "to" << newPath;
for (QFileInfo fileInfo : dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) {
QString fileName = fileInfo.fileName();
success = dir.rename(fileName, newPath + QDir::separator() + fileName) && success;
}
if (!success) {
qDebug() << "couldn't move downloads directory, most probably downloads will be broken";
}
currentPath = newPath;
}

View File

@ -26,10 +26,12 @@
#include <QFileInfo> #include <QFileInfo>
#include <QFile> #include <QFile>
#include <QStandardPaths> #include <QStandardPaths>
#include <QSettings>
#include <set> #include <set>
#include "urlstorage.h" #include "storage/urlstorage.h"
#include "shared/pathcheck.h"
namespace Core { namespace Core {
@ -65,6 +67,7 @@ public slots:
void downladFile(const QString& url); void downladFile(const QString& url);
void registerFile(const QString& url, const QString& account, const QString& jid, const QString& id); void registerFile(const QString& url, const QString& account, const QString& jid, const QString& id);
void registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id); void registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id);
void moveFilesDirectory(const QString& newPath);
private: private:
void startDownload(const std::list<Shared::MessageInfo>& msgs, const QString& url); void startDownload(const std::list<Shared::MessageInfo>& msgs, const QString& url);
@ -87,6 +90,7 @@ private:
UrlStorage storage; UrlStorage storage;
std::map<QString, Transfer*> downloads; std::map<QString, Transfer*> downloads;
std::map<QString, Transfer*> uploads; std::map<QString, Transfer*> uploads;
QString currentPath;
struct Transfer { struct Transfer {
std::list<Shared::MessageInfo> messages; std::list<Shared::MessageInfo> messages;

View File

@ -1,2 +1,4 @@
add_library(kwalletWrapper SHARED kwallet.cpp) add_library(kwalletWrapper SHARED kwallet.cpp)
target_link_libraries(kwalletWrapper PRIVATE KF5::Wallet) target_link_libraries(kwalletWrapper PRIVATE KF5::Wallet)
install(TARGETS kwalletWrapper LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

View File

@ -1,3 +1,21 @@
/*
* 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 <KF5/KWallet/KWallet> #include <KF5/KWallet/KWallet>
extern "C" KWallet::Wallet* openWallet(const QString &name, WId w, KWallet::Wallet::OpenType ot = KWallet::Wallet::Synchronous) { extern "C" KWallet::Wallet* openWallet(const QString &name, WId w, KWallet::Wallet::OpenType ot = KWallet::Wallet::Synchronous) {

View File

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

View File

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

View File

@ -50,7 +50,7 @@ void SignalCatcher::handleSigInt()
char tmp; char tmp;
ssize_t s = ::read(sigintFd[1], &tmp, sizeof(tmp)); ssize_t s = ::read(sigintFd[1], &tmp, sizeof(tmp));
app->quit(); emit interrupt();
snInt->setEnabled(true); snInt->setEnabled(true);
} }

View File

@ -33,6 +33,9 @@ public:
static void intSignalHandler(int unused); static void intSignalHandler(int unused);
signals:
void interrupt();
public slots: public slots:
void handleSigInt(); void handleSigInt();

View File

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

View File

@ -86,7 +86,7 @@ signals:
void responseVCard(const QString& jid, const Shared::VCard& card); void responseVCard(const QString& jid, const Shared::VCard& card);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void requestPassword(const QString& account); void requestPassword(const QString& account, bool authernticationError);
public slots: public slots:
void start(); void start();
@ -101,6 +101,7 @@ public slots:
void changeState(Shared::Availability state); void changeState(Shared::Availability state);
void sendMessage(const QString& account, const Shared::Message& data); 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 resendMessage(const QString& account, const QString& jid, const QString& id);
void requestArchive(const QString& account, const QString& jid, int count, const QString& before); void requestArchive(const QString& account, const QString& jid, int count, const QString& before);
@ -123,6 +124,7 @@ public slots:
void uploadVCard(const QString& account, const Shared::VCard& card); void uploadVCard(const QString& account, const Shared::VCard& card);
void responsePassword(const QString& account, const QString& password); void responsePassword(const QString& account, const QString& password);
void onLocalPathInvalid(const QString& path); void onLocalPathInvalid(const QString& path);
void changeDownloadsPath(const QString& path);
private: private:
typedef std::deque<Account*> Accounts; typedef std::deque<Account*> Accounts;
@ -132,7 +134,6 @@ private:
AccountsMap amap; AccountsMap amap;
Shared::Availability state; Shared::Availability state;
NetworkAccess network; NetworkAccess network;
uint8_t waitingForAccounts;
bool isInitialized; bool isInitialized;
#ifdef WITH_KWALLET #ifdef WITH_KWALLET
@ -146,6 +147,7 @@ private slots:
const QString& password, const QString& password,
const QString& name, const QString& name,
const QString& resource, const QString& resource,
bool active,
Shared::AccountPassword passwordType Shared::AccountPassword passwordType
); );
@ -170,22 +172,22 @@ private slots:
void onAccountChangeRoomPresence(const QString& jid, const QString& nick, const QMap<QString, QVariant>& data); void onAccountChangeRoomPresence(const QString& jid, const QString& nick, const QMap<QString, QVariant>& data);
void onAccountRemoveRoomPresence(const QString& jid, const QString& nick); void onAccountRemoveRoomPresence(const QString& jid, const QString& nick);
void onAccountChangeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data); void onAccountChangeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void onAccountNeedPassword();
void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText); void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText);
void onWalletOpened(bool success); void onWalletOpened(bool success);
void onWalletResponsePassword(const QString& login, const QString& password);
void onWalletRejectPassword(const QString& login); void onWalletRejectPassword(const QString& login);
private: private:
void readSettings(); void readSettings();
void accountReady();
void parseAccount( void parseAccount(
const QString& login, const QString& login,
const QString& server, const QString& server,
const QString& password, const QString& password,
const QString& name, const QString& name,
const QString& resource, const QString& resource,
bool active,
Shared::AccountPassword passwordType Shared::AccountPassword passwordType
); );

View File

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

View File

@ -123,6 +123,7 @@ bool Core::Archive::addElement(const Shared::Message& message)
if (!opened) { if (!opened) {
throw Closed("addElement", jid.toStdString()); throw Closed("addElement", jid.toStdString());
} }
qDebug() << "Adding message with id " << message.getId();
QByteArray ba; QByteArray ba;
QDataStream ds(&ba, QIODevice::WriteOnly); QDataStream ds(&ba, QIODevice::WriteOnly);
message.serialize(ds); message.serialize(ds);

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,17 +1,20 @@
# Maintainer: Yury Gubich <blue@macaw.me> # Maintainer: Yury Gubich <blue@macaw.me>
pkgname=squawk pkgname=squawk
pkgver=0.2.0 pkgver=0.2.2
pkgrel=1 pkgrel=1
pkgdesc="An XMPP desktop messenger, written on pure c++ (qt)" pkgdesc="An XMPP desktop messenger, written on pure c++ (qt)"
arch=('i686' 'x86_64') arch=('i686' 'x86_64')
url="https://git.macaw.me/blue/squawk" url="https://git.macaw.me/blue/squawk"
license=('GPL3') license=('GPL3')
depends=('hicolor-icon-theme' 'desktop-file-utils' 'lmdb' 'qxmpp>=1.1.0') depends=('hicolor-icon-theme' 'desktop-file-utils' 'lmdb' 'qxmpp>=1.1.0')
makedepends=('cmake>=3.3' 'imagemagick' 'qt5-tools') makedepends=('cmake>=3.3' 'imagemagick' 'qt5-tools' 'boost')
optdepends=('kwallet: secure password storage (requires rebuild)') optdepends=('kwallet: secure password storage (requires rebuild)'
'kconfig: system themes support (requires rebuild)'
'kconfigwidgets: system themes support (requires rebuild)'
'kio: better show in folder action (requires rebuild)')
source=("$pkgname-$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz")
sha256sums=('8e93d3dbe1fc35cfecb7783af409c6a264244d11609b2241d4fe77d43d068419') sha256sums=('e4fa2174a3ba95159cc3b0bac3f00550c9e0ce971c55334e2662696a4543fc7e')
build() { build() {
cd "$srcdir/squawk" cd "$srcdir/squawk"
cmake . -D CMAKE_INSTALL_PREFIX=/usr -D CMAKE_BUILD_TYPE=Release cmake . -D CMAKE_INSTALL_PREFIX=/usr -D CMAKE_BUILD_TYPE=Release
@ -19,5 +22,5 @@ build() {
} }
package() { package() {
cd "$srcdir/squawk" cd "$srcdir/squawk"
DESTDIR="$pkgdir/" cmake --build . --target install DESTDIR="$pkgdir/" cmake --build . --target install
} }

View File

@ -1,4 +1,14 @@
if (WITH_KIO) if (WITH_KIO)
add_library(openFileManagerWindowJob SHARED openfilemanagerwindowjob.cpp) add_library(openFileManagerWindowJob SHARED openfilemanagerwindowjob.cpp)
target_link_libraries(openFileManagerWindowJob PRIVATE KF5::KIOWidgets) target_link_libraries(openFileManagerWindowJob PRIVATE KF5::KIOWidgets)
install(TARGETS openFileManagerWindowJob LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif () endif ()
if (WITH_KCONFIG)
add_library(colorSchemeTools SHARED colorschemetools.cpp)
target_link_libraries(colorSchemeTools PRIVATE KF5::ConfigCore)
target_link_libraries(colorSchemeTools PRIVATE KF5::ConfigWidgets)
install(TARGETS colorSchemeTools LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()

View File

@ -0,0 +1,70 @@
/*
* 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 <QIcon>
#include <QPainter>
#include <QFileInfo>
#include <KConfigCore/KSharedConfig>
#include <KConfigCore/KConfigGroup>
#include <KConfigWidgets/KColorScheme>
QPixmap createPixmap(int size, const QBrush& window, const QBrush& button, const QBrush& view, const QBrush& selection);
extern "C" QIcon* createPreview(const QString& path) {
KSharedConfigPtr schemeConfig = KSharedConfig::openConfig(path);
QIcon* result = new QIcon();
KColorScheme activeWindow(QPalette::Active, KColorScheme::Window, schemeConfig);
KColorScheme activeButton(QPalette::Active, KColorScheme::Button, schemeConfig);
KColorScheme activeView(QPalette::Active, KColorScheme::View, schemeConfig);
KColorScheme activeSelection(QPalette::Active, KColorScheme::Selection, schemeConfig);
result->addPixmap(createPixmap(16, activeWindow.background(), activeButton.background(), activeView.background(), activeSelection.background()));
result->addPixmap(createPixmap(24, activeWindow.background(), activeButton.background(), activeView.background(), activeSelection.background()));
return result;
}
extern "C" void deletePreview(QIcon* icon) {
delete icon;
}
extern "C" void colorSchemeName(const QString& path, QString& answer) {
KSharedConfigPtr config = KSharedConfig::openConfig(path);
KConfigGroup group(config, QStringLiteral("General"));
answer = group.readEntry("Name", QFileInfo(path).baseName());
}
extern "C" void createPalette(const QString& path, QPalette& answer) {
KSharedConfigPtr config = KSharedConfig::openConfig(path);
answer = KColorScheme::createApplicationPalette(config);
}
QPixmap createPixmap(int size, const QBrush& window, const QBrush& button, const QBrush& view, const QBrush& selection) {
QPixmap pix(size, size);
pix.fill(Qt::black);
QPainter p;
p.begin(&pix);
const int itemSize = size / 2 - 1;
p.fillRect(1, 1, itemSize, itemSize, window);
p.fillRect(1 + itemSize, 1, itemSize, itemSize, button);
p.fillRect(1, 1 + itemSize, itemSize, itemSize, view);
p.fillRect(1 + itemSize, 1 + itemSize, itemSize, itemSize, selection);
p.end();
return pix;
}

View File

@ -1,3 +1,21 @@
/*
* 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 <QUrl> #include <QUrl>
#include <QObject> #include <QObject>
#include <KIO/OpenFileManagerWindowJob> #include <KIO/OpenFileManagerWindowJob>

View File

@ -16,4 +16,6 @@ target_sources(squawk PRIVATE
utils.h utils.h
vcard.cpp vcard.cpp
vcard.h vcard.h
pathcheck.cpp
pathcheck.h
) )

View File

@ -28,5 +28,6 @@ Utils::Exception::~Exception()
const char* Utils::Exception::what() const noexcept( true ) const char* Utils::Exception::what() const noexcept( true )
{ {
return getMessage().c_str(); std::string* msg = new std::string(getMessage());
return msg->c_str();
} }

View File

@ -19,6 +19,7 @@
#include "global.h" #include "global.h"
#include "enums.h" #include "enums.h"
#include "ui/models/roster.h"
Shared::Global* Shared::Global::instance = 0; Shared::Global* Shared::Global::instance = 0;
const std::set<QString> Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; const std::set<QString> Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"};
@ -28,6 +29,14 @@ QLibrary Shared::Global::openFileManagerWindowJob("openFileManagerWindowJob");
Shared::Global::HighlightInFileManager Shared::Global::hfm = 0; Shared::Global::HighlightInFileManager Shared::Global::hfm = 0;
#endif #endif
#ifdef WITH_KCONFIG
QLibrary Shared::Global::colorSchemeTools("colorSchemeTools");
Shared::Global::CreatePreview Shared::Global::createPreview = 0;
Shared::Global::DeletePreview Shared::Global::deletePreview = 0;
Shared::Global::ColorSchemeName Shared::Global::colorSchemeName = 0;
Shared::Global::CreatePalette Shared::Global::createPalette = 0;
#endif
Shared::Global::Global(): Shared::Global::Global():
availability({ availability({
tr("Online", "Availability"), tr("Online", "Availability"),
@ -84,9 +93,12 @@ Shared::Global::Global():
tr("Squawk is going to query you for the password on every start of the program", "AccountPasswordDescription"), tr("Squawk is going to query you for the password on every start of the program", "AccountPasswordDescription"),
tr("Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions", "AccountPasswordDescription") tr("Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions", "AccountPasswordDescription")
}), }),
defaultSystemStyle(QApplication::style()->objectName()),
defaultSystemPalette(QApplication::palette()),
pluginSupport({ pluginSupport({
{"KWallet", false}, {"KWallet", false},
{"openFileManagerWindowJob", false} {"openFileManagerWindowJob", false},
{"colorSchemeTools", false}
}), }),
fileCache() fileCache()
{ {
@ -110,6 +122,24 @@ Shared::Global::Global():
qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't load the library" << openFileManagerWindowJob.errorString(); qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't load the library" << openFileManagerWindowJob.errorString();
} }
#endif #endif
#ifdef WITH_KCONFIG
colorSchemeTools.load();
if (colorSchemeTools.isLoaded()) {
createPreview = (CreatePreview) colorSchemeTools.resolve("createPreview");
deletePreview = (DeletePreview) colorSchemeTools.resolve("deletePreview");
colorSchemeName = (ColorSchemeName) colorSchemeTools.resolve("colorSchemeName");
createPalette = (CreatePalette) colorSchemeTools.resolve("createPalette");
if (createPreview && deletePreview && colorSchemeName && createPalette) {
setSupported("colorSchemeTools", true);
qDebug() << "Color Schemes support enabled";
} else {
qDebug() << "Color Schemes support disabled: couldn't resolve required methods in the library";
}
} else {
qDebug() << "Color Schemes support disabled: couldn't load the library" << colorSchemeTools.errorString();
}
#endif
} }
@ -275,6 +305,50 @@ void Shared::Global::highlightInFileManager(const QString& path)
} }
} }
QIcon Shared::Global::createThemePreview(const QString& path)
{
if (supported("colorSchemeTools")) {
QIcon* icon = createPreview(path);
QIcon localIcon = *icon;
deletePreview(icon);
return localIcon;
} else {
return QIcon();
}
}
QString Shared::Global::getColorSchemeName(const QString& path)
{
if (supported("colorSchemeTools")) {
QString res;
colorSchemeName(path, res);
return res;
} else {
return "";
}
}
void Shared::Global::setTheme(const QString& path)
{
if (supported("colorSchemeTools")) {
if (path.toLower() == "system") {
QApplication::setPalette(getInstance()->defaultSystemPalette);
} else {
QPalette pallete;
createPalette(path, pallete);
QApplication::setPalette(pallete);
}
}
}
void Shared::Global::setStyle(const QString& style)
{
if (style.toLower() == "system") {
QApplication::setStyle(getInstance()->defaultSystemStyle);
} else {
QApplication::setStyle(style);
}
}
#define FROM_INT_INPL(Enum) \ #define FROM_INT_INPL(Enum) \
template<> \ template<> \

View File

@ -27,7 +27,8 @@
#include <set> #include <set>
#include <deque> #include <deque>
#include <QCoreApplication> #include <QApplication>
#include <QStyle>
#include <QDebug> #include <QDebug>
#include <QMimeType> #include <QMimeType>
#include <QMimeDatabase> #include <QMimeDatabase>
@ -46,7 +47,6 @@ namespace Shared {
class Global { class Global {
Q_DECLARE_TR_FUNCTIONS(Global) Q_DECLARE_TR_FUNCTIONS(Global)
public: public:
struct FileInfo { struct FileInfo {
enum class Preview { enum class Preview {
@ -63,7 +63,7 @@ namespace Shared {
}; };
Global(); Global();
static Global* getInstance(); static Global* getInstance();
static QString getName(Availability av); static QString getName(Availability av);
static QString getName(ConnectionState cs); static QString getName(ConnectionState cs);
@ -84,6 +84,9 @@ namespace Shared {
const std::deque<QString> accountPassword; const std::deque<QString> accountPassword;
const std::deque<QString> accountPasswordDescription; const std::deque<QString> accountPasswordDescription;
const QString defaultSystemStyle;
const QPalette defaultSystemPalette;
static bool supported(const QString& pluginName); static bool supported(const QString& pluginName);
static void setSupported(const QString& pluginName, bool support); static void setSupported(const QString& pluginName, bool support);
@ -92,6 +95,10 @@ namespace Shared {
static FileInfo getFileInfo(const QString& path); static FileInfo getFileInfo(const QString& path);
static void highlightInFileManager(const QString& path); static void highlightInFileManager(const QString& path);
static QIcon createThemePreview(const QString& path);
static QString getColorSchemeName(const QString& path);
static void setTheme(const QString& path);
static void setStyle(const QString& style);
template<typename T> template<typename T>
static T fromInt(int src); static T fromInt(int src);
@ -125,6 +132,20 @@ namespace Shared {
static HighlightInFileManager hfm; static HighlightInFileManager hfm;
#endif #endif
#ifdef WITH_KCONFIG
static QLibrary colorSchemeTools;
typedef QIcon* (*CreatePreview)(const QString&);
typedef void (*DeletePreview)(QIcon*);
typedef void (*ColorSchemeName)(const QString&, QString&);
typedef void (*CreatePalette)(const QString&, QPalette&);
static CreatePreview createPreview;
static DeletePreview deletePreview;
static ColorSchemeName colorSchemeName;
static CreatePalette createPalette;
#endif
}; };
} }

View File

@ -404,9 +404,11 @@ bool Shared::Message::change(const QMap<QString, QVariant>& data)
correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied
} }
if (!edited || lastModified < correctionDate) { if (!edited || lastModified < correctionDate) {
originalMessage = body; if (!edited) {
originalMessage = body;
}
lastModified = correctionDate; lastModified = correctionDate;
setBody(body); setBody(b);
setEdited(true); setEdited(true);
} }
} }

96
shared/pathcheck.cpp Normal file
View File

@ -0,0 +1,96 @@
/*
* 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 "pathcheck.h"
QRegularExpression squawk("^squawk:\\/\\/");
QString Shared::downloadsPathCheck()
{
QSettings settings;
QVariant dpv = settings.value("downloadsPath");
QString path;
if (!dpv.isValid()) {
path = defaultDownloadsPath();
qDebug() << "no downloadsPath variable in config, using default" << path;
path = getAbsoluteWritablePath(path);
return path;
} else {
path = dpv.toString();
path = getAbsoluteWritablePath(path);
if (path.size() == 0) {
path = defaultDownloadsPath();
qDebug() << "falling back to the default downloads path" << path;
path = getAbsoluteWritablePath(path);
}
return path;
}
}
QString Shared::defaultDownloadsPath()
{
return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/" + QApplication::applicationName();
}
QString Shared::getAbsoluteWritablePath(const QString& path)
{
QDir location(path);
if (!location.exists()) {
bool res = location.mkpath(location.absolutePath());
if (!res) {
qDebug() << "couldn't create directory" << path;
return "";
}
}
QFileInfo info(location.absolutePath());
if (info.isWritable()) {
return location.absolutePath();
} else {
qDebug() << "directory" << path << "is not writable";
return "";
}
}
QString Shared::resolvePath(QString path)
{
QSettings settings;
QVariant dpv = settings.value("downloadsPath");
return path.replace(squawk, dpv.toString() + "/");
}
QString Shared::squawkifyPath(QString path)
{
QSettings settings;
QString current = settings.value("downloadsPath").toString();
if (path.startsWith(current)) {
path.replace(0, current.size() + 1, "squawk://");
}
return path;
}
bool Shared::isSubdirectoryOfSettings(const QString& path)
{
QSettings settings;
QDir oldPath(settings.value("downloadsPath").toString());
QDir newPath(path);
return newPath.canonicalPath().startsWith(oldPath.canonicalPath());
}

44
shared/pathcheck.h Normal file
View File

@ -0,0 +1,44 @@
/*
* 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 PATHCHECK_H
#define PATHCHECK_H
#include <QString>
#include <QStandardPaths>
#include <QSettings>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QDebug>
#include <QRegularExpression>
namespace Shared {
QString downloadsPathCheck();
QString downloadsPathCheck(QString path);
QString defaultDownloadsPath();
QString getAbsoluteWritablePath(const QString& path);
QString resolvePath(QString path);
QString squawkifyPath(QString path);
bool isSubdirectoryOfSettings(const QString& path);
}
#endif // PATHCHECK_H

View File

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

View File

@ -24,8 +24,6 @@
#include <QColor> #include <QColor>
#include <QRegularExpression> #include <QRegularExpression>
// #include "KIO/OpenFileManagerWindowJob"
#include <vector> #include <vector>
namespace Shared { namespace Shared {
@ -71,6 +69,12 @@ static const std::vector<QColor> colorPalette = {
QColor(17, 17, 80), QColor(17, 17, 80),
QColor(54, 54, 94) QColor(54, 54, 94)
}; };
enum class Hover {
nothing,
text,
anchor
};
} }
#endif // SHARED_UTILS_H #endif // SHARED_UTILS_H

View File

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

1420
translations/squawk.en.ts Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,18 +21,16 @@
#include <QDebug> #include <QDebug>
#include <QIcon> #include <QIcon>
Squawk::Squawk(QWidget *parent) : Squawk::Squawk(Models::Roster& p_rosterModel, QWidget *parent) :
QMainWindow(parent), QMainWindow(parent),
m_ui(new Ui::Squawk), m_ui(new Ui::Squawk),
accounts(0), accounts(nullptr),
rosterModel(), preferences(nullptr),
conversations(), about(nullptr),
rosterModel(p_rosterModel),
contextMenu(new QMenu()), contextMenu(new QMenu()),
dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()),
vCards(), vCards(),
requestedAccountsForPasswords(), currentConversation(nullptr),
prompt(0),
currentConversation(0),
restoreSelection(), restoreSelection(),
needToRestore(false) needToRestore(false)
{ {
@ -55,6 +53,7 @@ Squawk::Squawk(QWidget *parent) :
m_ui->comboBox->setCurrentIndex(static_cast<int>(Shared::Availability::offline)); m_ui->comboBox->setCurrentIndex(static_cast<int>(Shared::Availability::offline));
connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts); connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts);
connect(m_ui->actionPreferences, &QAction::triggered, this, &Squawk::onPreferences);
connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact); connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact);
connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference); connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference);
connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::close); connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::close);
@ -63,13 +62,10 @@ Squawk::Squawk(QWidget *parent) :
connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu); connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu);
connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed); connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed);
connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged); connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged);
connect(&rosterModel, &Models::Roster::unnoticedMessage, this, &Squawk::onUnnoticedMessage);
connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged); connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged);
connect(&rosterModel, &Models::Roster::requestArchive, this, &Squawk::onRequestArchive);
connect(&rosterModel, &Models::Roster::fileDownloadRequest, this, &Squawk::fileDownloadRequest);
connect(&rosterModel, &Models::Roster::localPathInvalid, this, &Squawk::localPathInvalid);
connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide); connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide);
connect(m_ui->actionAboutSquawk, &QAction::triggered, this, &Squawk::onAboutSquawkCalled);
//m_ui->mainToolBar->addWidget(m_ui->comboBox); //m_ui->mainToolBar->addWidget(m_ui->comboBox);
if (testAttribute(Qt::WA_TranslucentBackground)) { if (testAttribute(Qt::WA_TranslucentBackground)) {
@ -99,8 +95,8 @@ Squawk::~Squawk() {
void Squawk::onAccounts() void Squawk::onAccounts()
{ {
if (accounts == 0) { if (accounts == nullptr) {
accounts = new Accounts(rosterModel.accountsModel); accounts = new Accounts(rosterModel.accountsModel, this);
accounts->setAttribute(Qt::WA_DeleteOnClose); accounts->setAttribute(Qt::WA_DeleteOnClose);
connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed); connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed);
connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest); connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest);
@ -108,15 +104,26 @@ void Squawk::onAccounts()
connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount); connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount);
connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount); connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount);
connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest); connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest);
accounts->show();
} else {
accounts->show();
accounts->raise();
accounts->activateWindow();
} }
accounts->show();
accounts->raise();
accounts->activateWindow();
} }
void Squawk::onPreferences()
{
if (preferences == nullptr) {
preferences = new Settings(this);
preferences->setAttribute(Qt::WA_DeleteOnClose);
connect(preferences, &Settings::destroyed, this, &Squawk::onPreferencesClosed);
connect(preferences, &Settings::changeDownloadsPath, this, &Squawk::changeDownloadsPath);
}
preferences->show();
preferences->raise();
preferences->activateWindow();
}
void Squawk::onAccountsSizeChanged(unsigned int size) void Squawk::onAccountsSizeChanged(unsigned int size)
{ {
if (size > 0) { if (size > 0) {
@ -170,143 +177,47 @@ void Squawk::onJoinConferenceAccepted()
void Squawk::closeEvent(QCloseEvent* event) void Squawk::closeEvent(QCloseEvent* event)
{ {
if (accounts != 0) { if (accounts != nullptr) {
accounts->close(); accounts->close();
} }
if (preferences != nullptr) {
for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) { preferences->close();
disconnect(itr->second, &Conversation::destroyed, this, &Squawk::onConversationClosed); }
itr->second->close(); if (about != nullptr) {
about->close();
} }
conversations.clear();
for (std::map<QString, VCard*>::const_iterator itr = vCards.begin(), end = vCards.end(); itr != end; ++itr) { for (std::map<QString, VCard*>::const_iterator itr = vCards.begin(), end = vCards.end(); itr != end; ++itr) {
disconnect(itr->second, &VCard::destroyed, this, &Squawk::onVCardClosed); disconnect(itr->second, &VCard::destroyed, this, &Squawk::onVCardClosed);
itr->second->close(); itr->second->close();
} }
vCards.clear(); vCards.clear();
writeSettings();
emit closing();;
QMainWindow::closeEvent(event); QMainWindow::closeEvent(event);
} }
void Squawk::onAccountsClosed() {
accounts = nullptr;}
void Squawk::onAccountsClosed(QObject* parent) void Squawk::onPreferencesClosed() {
{ preferences = nullptr;}
accounts = 0;
}
void Squawk::newAccount(const QMap<QString, QVariant>& account) void Squawk::onAboutSquawkClosed() {
{ about = nullptr;}
rosterModel.addAccount(account);
}
void Squawk::onComboboxActivated(int index) void Squawk::onComboboxActivated(int index)
{ {
Shared::Availability av = Shared::Global::fromInt<Shared::Availability>(index); Shared::Availability av = Shared::Global::fromInt<Shared::Availability>(index);
if (av != Shared::Availability::offline) { emit changeState(av);
int size = rosterModel.accountsModel->rowCount(QModelIndex());
if (size > 0) {
emit changeState(av);
for (int i = 0; i < size; ++i) {
Models::Account* acc = rosterModel.accountsModel->getAccount(i);
if (acc->getState() == Shared::ConnectionState::disconnected) {
emit connectAccount(acc->getName());
}
}
} else {
m_ui->comboBox->setCurrentIndex(static_cast<int>(Shared::Availability::offline));
}
} else {
emit changeState(av);
int size = rosterModel.accountsModel->rowCount(QModelIndex());
for (int i = 0; i != size; ++i) {
Models::Account* acc = rosterModel.accountsModel->getAccount(i);
if (acc->getState() != Shared::ConnectionState::disconnected) {
emit disconnectAccount(acc->getName());
}
}
}
} }
void Squawk::changeAccount(const QString& account, const QMap<QString, QVariant>& data) void Squawk::expand(const QModelIndex& index) {
{ m_ui->roster->expand(index);}
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
QString attr = itr.key();
rosterModel.updateAccount(account, attr, *itr);
}
}
void Squawk::addContact(const QString& account, const QString& jid, const QString& group, const QMap<QString, QVariant>& data) void Squawk::stateChanged(Shared::Availability state) {
{ m_ui->comboBox->setCurrentIndex(static_cast<int>(state));}
rosterModel.addContact(account, jid, group, data);
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("roster");
settings.beginGroup(account);
if (settings.value("expanded", false).toBool()) {
QModelIndex ind = rosterModel.getAccountIndex(account);
m_ui->roster->expand(ind);
}
settings.endGroup();
settings.endGroup();
settings.endGroup();
}
void Squawk::addGroup(const QString& account, const QString& name)
{
rosterModel.addGroup(account, name);
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("roster");
settings.beginGroup(account);
if (settings.value("expanded", false).toBool()) {
QModelIndex ind = rosterModel.getAccountIndex(account);
m_ui->roster->expand(ind);
if (settings.value(name + "/expanded", false).toBool()) {
m_ui->roster->expand(rosterModel.getGroupIndex(account, name));
}
}
settings.endGroup();
settings.endGroup();
settings.endGroup();
}
void Squawk::removeGroup(const QString& account, const QString& name)
{
rosterModel.removeGroup(account, name);
}
void Squawk::changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data)
{
rosterModel.changeContact(account, jid, data);
}
void Squawk::removeContact(const QString& account, const QString& jid)
{
rosterModel.removeContact(account, jid);
}
void Squawk::removeContact(const QString& account, const QString& jid, const QString& group)
{
rosterModel.removeContact(account, jid, group);
}
void Squawk::addPresence(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data)
{
rosterModel.addPresence(account, jid, name, data);
}
void Squawk::removePresence(const QString& account, const QString& jid, const QString& name)
{
rosterModel.removePresence(account, jid, name);
}
void Squawk::stateChanged(Shared::Availability state)
{
m_ui->comboBox->setCurrentIndex(static_cast<int>(state));
}
void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
{ {
@ -315,203 +226,35 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
if (node->type == Models::Item::reference) { if (node->type == Models::Item::reference) {
node = static_cast<Models::Reference*>(node)->dereference(); node = static_cast<Models::Reference*>(node)->dereference();
} }
Models::Contact* contact = 0; Models::Contact* contact = nullptr;
Models::Room* room = 0; Models::Room* room = nullptr;
QString res;
Models::Roster::ElId* id = 0;
switch (node->type) { switch (node->type) {
case Models::Item::contact: case Models::Item::contact:
contact = static_cast<Models::Contact*>(node); contact = static_cast<Models::Contact*>(node);
id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()));
break; break;
case Models::Item::presence: case Models::Item::presence:
contact = static_cast<Models::Contact*>(node->parentItem()); contact = static_cast<Models::Contact*>(node->parentItem());
id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()), node->getName());
res = node->getName();
break; break;
case Models::Item::room: case Models::Item::room:
room = static_cast<Models::Room*>(node); room = static_cast<Models::Room*>(node);
id = new Models::Roster::ElId(room->getAccountName(), room->getJid()); emit openConversation(Models::Roster::ElId(room->getAccountName(), room->getJid()));
break; break;
default: default:
m_ui->roster->expand(item); m_ui->roster->expand(item);
break; break;
} }
if (id != 0) {
Conversations::const_iterator itr = conversations.find(*id);
Models::Account* acc = rosterModel.getAccount(id->account);
Conversation* conv = 0;
bool created = false;
if (itr != conversations.end()) {
conv = itr->second;
} else if (contact != 0) {
created = true;
conv = new Chat(acc, contact);
} else if (room != 0) {
created = true;
conv = new Room(acc, room);
if (!room->getJoined()) {
emit setRoomJoined(id->account, id->name, true);
}
}
if (conv != 0) {
if (created) {
conv->setAttribute(Qt::WA_DeleteOnClose);
subscribeConversation(conv);
conversations.insert(std::make_pair(*id, conv));
}
conv->show();
conv->raise();
conv->activateWindow();
if (res.size() > 0) {
conv->setPalResource(res);
}
}
delete id;
}
} }
} }
void Squawk::onConversationClosed(QObject* parent) void Squawk::closeCurrentConversation()
{ {
Conversation* conv = static_cast<Conversation*>(sender()); if (currentConversation != nullptr) {
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 Squawk::fileProgress(const std::list<Shared::MessageInfo> msgs, qreal value, bool up)
{
rosterModel.fileProgress(msgs, value, up);
}
void Squawk::fileDownloadComplete(const std::list<Shared::MessageInfo> msgs, const QString& path)
{
rosterModel.fileComplete(msgs, false);
}
void Squawk::fileError(const std::list<Shared::MessageInfo> msgs, const QString& error, bool up)
{
rosterModel.fileError(msgs, error, up);
}
void Squawk::fileUploadComplete(const std::list<Shared::MessageInfo> msgs, const QString& url, const QString& path)
{
rosterModel.fileComplete(msgs, true);
}
void Squawk::accountMessage(const QString& account, const Shared::Message& data)
{
rosterModel.addMessage(account, data);
}
void Squawk::onUnnoticedMessage(const QString& account, const Shared::Message& msg)
{
notify(account, msg); //Telegram does this way - notifies even if the app is visible
QApplication::alert(this);
}
void Squawk::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data)
{
rosterModel.changeMessage(account, jid, id, data);
}
void Squawk::notify(const QString& account, const Shared::Message& msg)
{
QString name = QString(rosterModel.getContactName(account, msg.getPenPalJid()));
QString path = QString(rosterModel.getContactIconPath(account, msg.getPenPalJid(), msg.getPenPalResource()));
QVariantList args;
args << QString(QCoreApplication::applicationName());
args << QVariant(QVariant::UInt); //TODO some normal id
if (path.size() > 0) {
args << path;
} else {
args << QString("mail-message"); //TODO should here better be unknown user icon?
}
if (msg.getType() == Shared::Message::groupChat) {
args << msg.getFromResource() + " from " + name;
} else {
args << name;
}
QString body(msg.getBody());
QString oob(msg.getOutOfBandUrl());
if (body == oob) {
body = tr("Attached file");
}
args << body;
args << QStringList();
args << QVariantMap();
args << 3000;
dbus.callWithArgumentList(QDBus::AutoDetect, "Notify", args);
}
void Squawk::onConversationMessage(const Shared::Message& msg)
{
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
rosterModel.addMessage(acc, msg);
emit sendMessage(acc, msg);
}
void Squawk::onConversationResend(const QString& id)
{
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
QString jid = conv->getJid();
emit resendMessage(acc, jid, id);
}
void Squawk::onRequestArchive(const QString& account, const QString& jid, const QString& before)
{
emit requestArchive(account, jid, 20, before); //TODO amount as a settings value
}
void Squawk::responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last)
{
rosterModel.responseArchive(account, jid, list, last);
}
void Squawk::removeAccount(const QString& account)
{
Conversations::const_iterator itr = conversations.begin();
while (itr != conversations.end()) {
if (itr->first.account == account) {
Conversations::const_iterator lItr = itr;
++itr;
Conversation* conv = lItr->second;
disconnect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed);
conv->close();
conversations.erase(lItr);
} else {
++itr;
}
}
if (currentConversation != 0 && currentConversation->getAccount() == account) {
currentConversation->deleteLater(); currentConversation->deleteLater();
currentConversation = 0; currentConversation = nullptr;
m_ui->filler->show(); m_ui->filler->show();
} }
rosterModel.removeAccount(account);
} }
void Squawk::onRosterContextMenu(const QPoint& point) void Squawk::onRosterContextMenu(const QPoint& point)
@ -531,17 +274,12 @@ void Squawk::onRosterContextMenu(const QPoint& point)
hasMenu = true; hasMenu = true;
QString name = acc->getName(); QString name = acc->getName();
if (acc->getState() != Shared::ConnectionState::disconnected) { if (acc->getActive()) {
QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Disconnect")); QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Deactivate"));
con->setEnabled(active); connect(con, &QAction::triggered, std::bind(&Squawk::disconnectAccount, this, name));
connect(con, &QAction::triggered, [this, name]() {
emit disconnectAccount(name);
});
} else { } else {
QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Connect")); QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Activate"));
connect(con, &QAction::triggered, [this, name]() { connect(con, &QAction::triggered, std::bind(&Squawk::connectAccount, this, name));
emit connectAccount(name);
});
} }
QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard"));
@ -549,22 +287,18 @@ void Squawk::onRosterContextMenu(const QPoint& point)
connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, name, acc->getBareJid(), true)); connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, name, acc->getBareJid(), true));
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
remove->setEnabled(active); connect(remove, &QAction::triggered, std::bind(&Squawk::removeAccountRequest, this, name));
connect(remove, &QAction::triggered, [this, name]() {
emit removeAccount(name);
});
} }
break; break;
case Models::Item::contact: { case Models::Item::contact: {
Models::Contact* cnt = static_cast<Models::Contact*>(item); Models::Contact* cnt = static_cast<Models::Contact*>(item);
Models::Roster::ElId id(cnt->getAccountName(), cnt->getJid());
QString cntName = cnt->getName();
hasMenu = true; hasMenu = true;
QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog")); QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog"));
dialog->setEnabled(active); dialog->setEnabled(active);
connect(dialog, &QAction::triggered, [this, index]() { connect(dialog, &QAction::triggered, std::bind(&Squawk::onRosterItemDoubleClicked, this, index));
onRosterItemDoubleClicked(index);
});
Shared::SubscriptionState state = cnt->getState(); Shared::SubscriptionState state = cnt->getState();
switch (state) { switch (state) {
@ -572,9 +306,7 @@ void Squawk::onRosterContextMenu(const QPoint& point)
case Shared::SubscriptionState::to: { case Shared::SubscriptionState::to: {
QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
unsub->setEnabled(active); unsub->setEnabled(active);
connect(unsub, &QAction::triggered, [this, cnt]() { connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
emit unsubscribeContact(cnt->getAccountName(), cnt->getJid(), "");
});
} }
break; break;
case Shared::SubscriptionState::from: case Shared::SubscriptionState::from:
@ -582,75 +314,68 @@ void Squawk::onRosterContextMenu(const QPoint& point)
case Shared::SubscriptionState::none: { case Shared::SubscriptionState::none: {
QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
sub->setEnabled(active); sub->setEnabled(active);
connect(sub, &QAction::triggered, [this, cnt]() { connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
emit subscribeContact(cnt->getAccountName(), cnt->getJid(), "");
});
} }
} }
QString accName = cnt->getAccountName();
QString cntJID = cnt->getJid();
QString cntName = cnt->getName();
QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename")); QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename"));
rename->setEnabled(active); rename->setEnabled(active);
connect(rename, &QAction::triggered, [this, cntName, accName, cntJID]() { connect(rename, &QAction::triggered, [this, cntName, id]() {
QInputDialog* dialog = new QInputDialog(this); QInputDialog* dialog = new QInputDialog(this);
connect(dialog, &QDialog::accepted, [this, dialog, cntName, accName, cntJID]() { connect(dialog, &QDialog::accepted, [this, dialog, cntName, id]() {
QString newName = dialog->textValue(); QString newName = dialog->textValue();
if (newName != cntName) { if (newName != cntName) {
emit renameContactRequest(accName, cntJID, newName); emit renameContactRequest(id.account, id.name, newName);
} }
dialog->deleteLater(); dialog->deleteLater();
}); });
connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
dialog->setInputMode(QInputDialog::TextInput); dialog->setInputMode(QInputDialog::TextInput);
dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(cntJID)); dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(id.name));
dialog->setWindowTitle(tr("Renaming %1").arg(cntJID)); dialog->setWindowTitle(tr("Renaming %1").arg(id.name));
dialog->setTextValue(cntName); dialog->setTextValue(cntName);
dialog->exec(); dialog->exec();
}); });
QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups")); QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups"));
std::deque<QString> groupList = rosterModel.groupList(accName); std::deque<QString> groupList = rosterModel.groupList(id.account);
for (QString groupName : groupList) { for (QString groupName : groupList) {
QAction* gr = groupsMenu->addAction(groupName); QAction* gr = groupsMenu->addAction(groupName);
gr->setCheckable(true); gr->setCheckable(true);
gr->setChecked(rosterModel.groupHasContact(accName, groupName, cntJID)); gr->setChecked(rosterModel.groupHasContact(id.account, groupName, id.name));
gr->setEnabled(active); gr->setEnabled(active);
connect(gr, &QAction::toggled, [this, accName, groupName, cntJID](bool checked) { connect(gr, &QAction::toggled, [this, groupName, id](bool checked) {
if (checked) { if (checked) {
emit addContactToGroupRequest(accName, cntJID, groupName); emit addContactToGroupRequest(id.account, id.name, groupName);
} else { } else {
emit removeContactFromGroupRequest(accName, cntJID, groupName); emit removeContactFromGroupRequest(id.account, id.name, groupName);
} }
}); });
} }
QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group")); QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group"));
newGroup->setEnabled(active); newGroup->setEnabled(active);
connect(newGroup, &QAction::triggered, [this, accName, cntJID]() { connect(newGroup, &QAction::triggered, [this, id]() {
QInputDialog* dialog = new QInputDialog(this); QInputDialog* dialog = new QInputDialog(this);
connect(dialog, &QDialog::accepted, [this, dialog, accName, cntJID]() { connect(dialog, &QDialog::accepted, [this, dialog, id]() {
emit addContactToGroupRequest(accName, cntJID, dialog->textValue()); emit addContactToGroupRequest(id.account, id.name, dialog->textValue());
dialog->deleteLater(); dialog->deleteLater();
}); });
connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
dialog->setInputMode(QInputDialog::TextInput); dialog->setInputMode(QInputDialog::TextInput);
dialog->setLabelText(tr("New group name")); dialog->setLabelText(tr("New group name"));
dialog->setWindowTitle(tr("Add %1 to a new group").arg(cntJID)); dialog->setWindowTitle(tr("Add %1 to a new group").arg(id.name));
dialog->exec(); dialog->exec();
}); });
QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard"));
card->setEnabled(active); card->setEnabled(active);
connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, accName, cnt->getJid(), false)); connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, id.account, id.name, false));
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
remove->setEnabled(active); remove->setEnabled(active);
connect(remove, &QAction::triggered, [this, cnt]() { connect(remove, &QAction::triggered, std::bind(&Squawk::removeContactRequest, this, id.account, id.name));
emit removeContactRequest(cnt->getAccountName(), cnt->getJid());
});
} }
break; break;
@ -669,32 +394,16 @@ void Squawk::onRosterContextMenu(const QPoint& point)
if (room->getAutoJoin()) { if (room->getAutoJoin()) {
QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
unsub->setEnabled(active); unsub->setEnabled(active);
connect(unsub, &QAction::triggered, [this, id]() { connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
emit setRoomAutoJoin(id.account, id.name, false);
if (conversations.find(id) == conversations.end()
&& (currentConversation == 0 || currentConversation->getId() != id)
) { //to leave the room if it's not opened in a conversation window
emit setRoomJoined(id.account, id.name, false);
}
});
} else { } else {
QAction* unsub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
unsub->setEnabled(active); sub->setEnabled(active);
connect(unsub, &QAction::triggered, [this, id]() { connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
emit setRoomAutoJoin(id.account, id.name, true);
if (conversations.find(id) == conversations.end()
&& (currentConversation == 0 || currentConversation->getId() != id)
) { //to join the room if it's not already joined
emit setRoomJoined(id.account, id.name, true);
}
});
} }
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
remove->setEnabled(active); remove->setEnabled(active);
connect(remove, &QAction::triggered, [this, id]() { connect(remove, &QAction::triggered, std::bind(&Squawk::removeRoomRequest, this, id.account, id.name));
emit removeRoomRequest(id.account, id.name);
});
} }
break; break;
default: default:
@ -706,36 +415,6 @@ void Squawk::onRosterContextMenu(const QPoint& point)
} }
} }
void Squawk::addRoom(const QString& account, const QString jid, const QMap<QString, QVariant>& data)
{
rosterModel.addRoom(account, jid, data);
}
void Squawk::changeRoom(const QString& account, const QString jid, const QMap<QString, QVariant>& data)
{
rosterModel.changeRoom(account, jid, data);
}
void Squawk::removeRoom(const QString& account, const QString jid)
{
rosterModel.removeRoom(account, jid);
}
void Squawk::addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data)
{
rosterModel.addRoomParticipant(account, jid, name, data);
}
void Squawk::changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data)
{
rosterModel.changeRoomParticipant(account, jid, name, data);
}
void Squawk::removeRoomParticipant(const QString& account, const QString& jid, const QString& name)
{
rosterModel.removeRoomParticipant(account, jid, name);
}
void Squawk::responseVCard(const QString& jid, const Shared::VCard& card) void Squawk::responseVCard(const QString& jid, const Shared::VCard& card)
{ {
std::map<QString, VCard*>::const_iterator itr = vCards.find(jid); std::map<QString, VCard*>::const_iterator itr = vCards.find(jid);
@ -793,70 +472,40 @@ void Squawk::onVCardSave(const Shared::VCard& card, const QString& account)
widget->deleteLater(); widget->deleteLater();
} }
void Squawk::readSettings()
{
QSettings settings;
settings.beginGroup("ui");
if (settings.contains("availability")) {
int avail = settings.value("availability").toInt();
m_ui->comboBox->setCurrentIndex(avail);
emit stateChanged(Shared::Global::fromInt<Shared::Availability>(avail));
int size = settings.beginReadArray("connectedAccounts");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
emit connectAccount(settings.value("name").toString()); //TODO this is actually not needed, stateChanged event already connects everything you have
} // need to fix that
settings.endArray();
}
settings.endGroup();
}
void Squawk::writeSettings() void Squawk::writeSettings()
{ {
QSettings settings; QSettings settings;
settings.beginGroup("ui"); settings.beginGroup("ui");
settings.beginGroup("window"); settings.beginGroup("window");
settings.setValue("geometry", saveGeometry()); settings.setValue("geometry", saveGeometry());
settings.setValue("state", saveState()); settings.setValue("state", saveState());
settings.endGroup(); settings.endGroup();
settings.setValue("splitter", m_ui->splitter->saveState()); settings.setValue("splitter", m_ui->splitter->saveState());
settings.remove("roster");
settings.setValue("availability", m_ui->comboBox->currentIndex()); settings.beginGroup("roster");
settings.beginWriteArray("connectedAccounts"); int size = rosterModel.accountsModel->rowCount(QModelIndex());
int size = rosterModel.accountsModel->rowCount(QModelIndex()); for (int i = 0; i < size; ++i) {
for (int i = 0; i < size; ++i) { QModelIndex acc = rosterModel.index(i, 0, QModelIndex());
Models::Account* acc = rosterModel.accountsModel->getAccount(i); Models::Account* account = rosterModel.accountsModel->getAccount(i);
if (acc->getState() != Shared::ConnectionState::disconnected) { QString accName = account->getName();
settings.setArrayIndex(i); settings.beginGroup(accName);
settings.setValue("name", acc->getName());
} settings.setValue("expanded", m_ui->roster->isExpanded(acc));
} std::deque<QString> groups = rosterModel.groupList(accName);
settings.endArray(); for (const QString& groupName : groups) {
settings.beginGroup(groupName);
settings.remove("roster"); QModelIndex gIndex = rosterModel.getGroupIndex(accName, groupName);
settings.beginGroup("roster"); settings.setValue("expanded", m_ui->roster->isExpanded(gIndex));
for (int i = 0; i < size; ++i) { settings.endGroup();
QModelIndex acc = rosterModel.index(i, 0, QModelIndex()); }
Models::Account* account = rosterModel.accountsModel->getAccount(i);
QString accName = account->getName(); settings.endGroup();
settings.beginGroup(accName); }
settings.setValue("expanded", m_ui->roster->isExpanded(acc));
std::deque<QString> groups = rosterModel.groupList(accName);
for (const QString& groupName : groups) {
settings.beginGroup(groupName);
QModelIndex gIndex = rosterModel.getGroupIndex(accName, groupName);
settings.setValue("expanded", m_ui->roster->isExpanded(gIndex));
settings.endGroup();
}
settings.endGroup(); settings.endGroup();
}
settings.endGroup();
settings.endGroup(); settings.endGroup();
settings.sync();
} }
void Squawk::onItemCollepsed(const QModelIndex& index) void Squawk::onItemCollepsed(const QModelIndex& index)
@ -878,59 +527,6 @@ void Squawk::onItemCollepsed(const QModelIndex& index)
} }
} }
void Squawk::requestPassword(const QString& account)
{
requestedAccountsForPasswords.push_back(account);
checkNextAccountForPassword();
}
void Squawk::checkNextAccountForPassword()
{
if (prompt == 0 && requestedAccountsForPasswords.size() > 0) {
prompt = new QInputDialog(this);
QString accName = requestedAccountsForPasswords.front();
connect(prompt, &QDialog::accepted, this, &Squawk::onPasswordPromptAccepted);
connect(prompt, &QDialog::rejected, this, &Squawk::onPasswordPromptRejected);
prompt->setInputMode(QInputDialog::TextInput);
prompt->setTextEchoMode(QLineEdit::Password);
prompt->setLabelText(tr("Input the password for account %1").arg(accName));
prompt->setWindowTitle(tr("Password for account %1").arg(accName));
prompt->setTextValue("");
prompt->exec();
}
}
void Squawk::onPasswordPromptAccepted()
{
emit responsePassword(requestedAccountsForPasswords.front(), prompt->textValue());
onPasswordPromptDone();
}
void Squawk::onPasswordPromptDone()
{
prompt->deleteLater();
prompt = 0;
requestedAccountsForPasswords.pop_front();
checkNextAccountForPassword();
}
void Squawk::onPasswordPromptRejected()
{
//for now it's the same on reject and on accept, but one day I'm gonna make
//"Asking for the password again on the authentication failure" feature
//and here I'll be able to break the circle of password requests
emit responsePassword(requestedAccountsForPasswords.front(), prompt->textValue());
onPasswordPromptDone();
}
void Squawk::subscribeConversation(Conversation* conv)
{
connect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed);
connect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage);
connect(conv, &Conversation::resendMessage, this, &Squawk::onConversationResend);
connect(conv, &Conversation::notifyableMessage, this, &Squawk::notify);
}
void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous) void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{ {
if (restoreSelection.isValid() && restoreSelection == current) { if (restoreSelection.isValid() && restoreSelection == current) {
@ -943,10 +539,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
if (node->type == Models::Item::reference) { if (node->type == Models::Item::reference) {
node = static_cast<Models::Reference*>(node)->dereference(); node = static_cast<Models::Reference*>(node)->dereference();
} }
Models::Contact* contact = 0; Models::Contact* contact = nullptr;
Models::Room* room = 0; Models::Room* room = nullptr;
QString res; QString res;
Models::Roster::ElId* id = 0; Models::Roster::ElId* id = nullptr;
bool hasContext = true; bool hasContext = true;
switch (node->type) { switch (node->type) {
case Models::Item::contact: case Models::Item::contact:
@ -975,7 +571,7 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
} }
if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) { if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) {
if (id != 0) { if (id != nullptr) {
delete id; delete id;
} }
needToRestore = true; needToRestore = true;
@ -983,10 +579,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
return; return;
} }
if (id != 0) { if (id != nullptr) {
if (currentConversation != 0) { if (currentConversation != nullptr) {
if (currentConversation->getId() == *id) { if (currentConversation->getId() == *id) {
if (contact != 0) { if (contact != nullptr) {
currentConversation->setPalResource(res); currentConversation->setPalResource(res);
} }
return; return;
@ -998,20 +594,16 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
} }
Models::Account* acc = rosterModel.getAccount(id->account); Models::Account* acc = rosterModel.getAccount(id->account);
if (contact != 0) { if (contact != nullptr) {
currentConversation = new Chat(acc, contact); currentConversation = new Chat(acc, contact);
} else if (room != 0) { } else if (room != nullptr) {
currentConversation = new Room(acc, room); currentConversation = new Room(acc, room);
if (!room->getJoined()) {
emit setRoomJoined(id->account, id->name, true);
}
} }
if (!testAttribute(Qt::WA_TranslucentBackground)) { if (!testAttribute(Qt::WA_TranslucentBackground)) {
currentConversation->setFeedFrames(true, false, true, true); currentConversation->setFeedFrames(true, false, true, true);
} }
subscribeConversation(currentConversation); emit openedConversation();
if (res.size() > 0) { if (res.size() > 0) {
currentConversation->setPalResource(res); currentConversation->setPalResource(res);
@ -1021,18 +613,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
delete id; delete id;
} else { } else {
if (currentConversation != 0) { closeCurrentConversation();
currentConversation->deleteLater();
currentConversation = 0;
m_ui->filler->show();
}
} }
} else { } else {
if (currentConversation != 0) { closeCurrentConversation();
currentConversation->deleteLater();
currentConversation = 0;
m_ui->filler->show();
}
} }
} }
@ -1043,3 +627,30 @@ void Squawk::onContextAboutToHide()
m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect); m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect);
} }
} }
void Squawk::onAboutSquawkCalled()
{
if (about == nullptr) {
about = new About(this);
about->setAttribute(Qt::WA_DeleteOnClose);
connect(about, &Settings::destroyed, this, &Squawk::onAboutSquawkClosed);
}
about->show();
about->raise();
about->activateWindow();
}
Models::Roster::ElId Squawk::currentConversationId() const
{
if (currentConversation == nullptr) {
return Models::Roster::ElId();
} else {
return Models::Roster::ElId(currentConversation->getAccount(), currentConversation->getJid());
}
}
void Squawk::select(QModelIndex index)
{
m_ui->roster->scrollTo(index, QAbstractItemView::EnsureVisible);
m_ui->roster->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
}

View File

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

View File

@ -191,6 +191,7 @@
<string>Settings</string> <string>Settings</string>
</property> </property>
<addaction name="actionAccounts"/> <addaction name="actionAccounts"/>
<addaction name="actionPreferences"/>
</widget> </widget>
<widget class="QMenu" name="menuFile"> <widget class="QMenu" name="menuFile">
<property name="title"> <property name="title">
@ -200,8 +201,15 @@
<addaction name="actionAddConference"/> <addaction name="actionAddConference"/>
<addaction name="actionQuit"/> <addaction name="actionQuit"/>
</widget> </widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAboutSquawk"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuSettings"/> <addaction name="menuSettings"/>
<addaction name="menuHelp"/>
</widget> </widget>
<action name="actionAccounts"> <action name="actionAccounts">
<property name="icon"> <property name="icon">
@ -245,6 +253,20 @@
<string>Add conference</string> <string>Add conference</string>
</property> </property>
</action> </action>
<action name="actionPreferences">
<property name="icon">
<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> </widget>
<resources> <resources>
<include location="../resources/resources.qrc"/> <include location="../resources/resources.qrc"/>

View File

@ -26,32 +26,62 @@ Badge::Badge(const QString& p_id, const QString& p_text, const QIcon& icon, QWid
closeButton(new QPushButton()), closeButton(new QPushButton()),
layout(new QHBoxLayout(this)) layout(new QHBoxLayout(this))
{ {
setBackgroundRole(QPalette::Base); createMandatoryComponents();
//setAutoFillBackground(true);
setFrameStyle(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
image->setPixmap(icon.pixmap(25, 25)); image->setPixmap(icon.pixmap(25, 25));
QIcon tabCloseIcon = QIcon::fromTheme("tab-close");
if (tabCloseIcon.isNull()) {
tabCloseIcon.addFile(QString::fromUtf8(":/images/fallback/dark/big/edit-none.svg"), QSize(), QIcon::Normal, QIcon::Off);
}
closeButton->setIcon(tabCloseIcon);
closeButton->setMaximumHeight(25);
closeButton->setMaximumWidth(25);
layout->addWidget(image); layout->addWidget(image);
layout->addWidget(text); layout->addWidget(text);
layout->addWidget(closeButton); layout->addWidget(closeButton);
}
layout->setContentsMargins(2, 2, 2, 2);
Badge::Badge(QWidget* parent):
connect(closeButton, &QPushButton::clicked, this, &Badge::close); QFrame(parent),
id(Shared::generateUUID()),
image(nullptr),
text(nullptr),
closeButton(new QPushButton()),
layout(new QHBoxLayout(this))
{
createMandatoryComponents();
layout->addWidget(closeButton);
}
void Badge::setIcon(const QIcon& icon)
{
if (image == nullptr) {
image = new QLabel();
image->setPixmap(icon.pixmap(25, 25));
layout->insertWidget(0, image);
} else {
image->setPixmap(icon.pixmap(25, 25));
}
}
void Badge::setText(const QString& p_text)
{
if (text == nullptr) {
text = new QLabel(p_text);
int index = 0;
if (image != nullptr) {
index = 1;
}
layout->insertWidget(index, text);
} else {
text->setText(p_text);
}
} }
Badge::~Badge() Badge::~Badge()
{ {
if (image != nullptr) {
delete image;
}
if (text != nullptr) {
delete text;
}
delete closeButton;
} }
bool Badge::Comparator::operator()(const Badge* a, const Badge* b) const bool Badge::Comparator::operator()(const Badge* a, const Badge* b) const
@ -63,3 +93,22 @@ bool Badge::Comparator::operator()(const Badge& a, const Badge& b) const
{ {
return a.id < b.id; return a.id < b.id;
} }
void Badge::createMandatoryComponents()
{
setBackgroundRole(QPalette::Base);
//setAutoFillBackground(true);
setFrameStyle(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
QIcon tabCloseIcon = QIcon::fromTheme("tab-close");
if (tabCloseIcon.isNull()) {
tabCloseIcon.addFile(QString::fromUtf8(":/images/fallback/dark/big/edit-none.svg"), QSize(), QIcon::Normal, QIcon::Off);
}
closeButton->setIcon(tabCloseIcon);
closeButton->setMaximumHeight(25);
closeButton->setMaximumWidth(25);
layout->setContentsMargins(2, 2, 2, 2);
connect(closeButton, &QPushButton::clicked, this, &Badge::close);
}

View File

@ -25,6 +25,8 @@
#include <QIcon> #include <QIcon>
#include <QPushButton> #include <QPushButton>
#include "shared/utils.h"
/** /**
* @todo write docs * @todo write docs
*/ */
@ -33,9 +35,14 @@ class Badge : public QFrame
Q_OBJECT Q_OBJECT
public: public:
Badge(const QString& id, const QString& text, const QIcon& icon, QWidget* parent = nullptr); Badge(const QString& id, const QString& text, const QIcon& icon, QWidget* parent = nullptr);
Badge(QWidget* parent = nullptr);
~Badge(); ~Badge();
const QString id; const QString id;
public:
void setText(const QString& text);
void setIcon(const QIcon& icon);
signals: signals:
void close(); void close();
@ -45,6 +52,9 @@ private:
QLabel* text; QLabel* text;
QPushButton* closeButton; QPushButton* closeButton;
QHBoxLayout* layout; QHBoxLayout* layout;
private:
void createMandatoryComponents();
public: public:
struct Comparator { struct Comparator {

View File

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

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

@ -0,0 +1,111 @@
// 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):
QDialog(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::WindowStaysOnTopHint);
if (parent)
move(parent->window()->frameGeometry().topLeft() +
parent->window()->rect().center() - rect().center());
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 <QDialog>
#include <QScopedPointer>
#include <QApplication>
#include <QFile>
#include <QTextStream>
#include <QStandardPaths>
namespace Ui
{
class About;
}
/**
* @todo write docs
*/
class About : public QDialog
{
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

@ -19,8 +19,8 @@
#include "account.h" #include "account.h"
#include "ui_account.h" #include "ui_account.h"
Account::Account(): Account::Account(QWidget *parent):
QDialog(), QDialog(parent),
m_ui(new Ui::Account) m_ui(new Ui::Account)
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@ -38,6 +38,10 @@ Account::Account():
QStandardItem *item = model->item(static_cast<int>(Shared::AccountPassword::kwallet)); QStandardItem *item = model->item(static_cast<int>(Shared::AccountPassword::kwallet));
item->setFlags(item->flags() & ~Qt::ItemIsEnabled); item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
} }
if (parent)
move(parent->window()->frameGeometry().topLeft() +
parent->window()->rect().center() - rect().center());
} }
Account::~Account() Account::~Account()
@ -53,6 +57,7 @@ QMap<QString, QVariant> Account::value() const
map["name"] = m_ui->name->text(); map["name"] = m_ui->name->text();
map["resource"] = m_ui->resource->text(); map["resource"] = m_ui->resource->text();
map["passwordType"] = m_ui->passwordType->currentIndex(); map["passwordType"] = m_ui->passwordType->currentIndex();
map["active"] = m_ui->active->isChecked();
return map; return map;
} }

View File

@ -38,7 +38,7 @@ class Account : public QDialog
Q_OBJECT Q_OBJECT
public: public:
Account(); Account(QWidget *parent = nullptr);
~Account(); ~Account();
QMap<QString, QVariant> value() const; QMap<QString, QVariant> value() const;

View File

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

View File

@ -22,7 +22,7 @@
#include <QDebug> #include <QDebug>
Accounts::Accounts(Models::Accounts* p_model, QWidget *parent) : Accounts::Accounts(Models::Accounts* p_model, QWidget *parent) :
QWidget(parent), QDialog(parent),
m_ui(new Ui::Accounts), m_ui(new Ui::Accounts),
model(p_model), model(p_model),
editing(false), editing(false),
@ -38,13 +38,17 @@ Accounts::Accounts(Models::Accounts* p_model, QWidget *parent) :
connect(m_ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Accounts::onSelectionChanged); connect(m_ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Accounts::onSelectionChanged);
connect(p_model, &Models::Accounts::changed, this, &Accounts::updateConnectButton); connect(p_model, &Models::Accounts::changed, this, &Accounts::updateConnectButton);
connect(m_ui->tableView, &QTableView::doubleClicked, this, &Accounts::onEditButton); connect(m_ui->tableView, &QTableView::doubleClicked, this, &Accounts::onEditButton);
if (parent)
move(parent->window()->frameGeometry().topLeft() +
parent->window()->rect().center() - rect().center());
} }
Accounts::~Accounts() = default; Accounts::~Accounts() = default;
void Accounts::onAddButton() void Accounts::onAddButton()
{ {
Account* acc = new Account(); Account* acc = new Account(this);
connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted);
connect(acc, &Account::rejected, this, &Accounts::onAccountRejected); connect(acc, &Account::rejected, this, &Accounts::onAccountRejected);
acc->exec(); acc->exec();
@ -74,7 +78,7 @@ void Accounts::onAccountRejected()
void Accounts::onEditButton() void Accounts::onEditButton()
{ {
Account* acc = new Account(); Account* acc = new Account(this);
const Models::Account* mAcc = model->getAccount(m_ui->tableView->selectionModel()->selectedRows().at(0).row()); const Models::Account* mAcc = model->getAccount(m_ui->tableView->selectionModel()->selectedRows().at(0).row());
acc->setData({ acc->setData({
@ -83,7 +87,8 @@ void Accounts::onEditButton()
{"server", mAcc->getServer()}, {"server", mAcc->getServer()},
{"name", mAcc->getName()}, {"name", mAcc->getName()},
{"resource", mAcc->getResource()}, {"resource", mAcc->getResource()},
{"passwordType", QVariant::fromValue(mAcc->getPasswordType())} {"passwordType", QVariant::fromValue(mAcc->getPasswordType())},
{"active", mAcc->getActive()}
}); });
acc->lockId(); acc->lockId();
connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted);
@ -118,17 +123,17 @@ void Accounts::updateConnectButton()
bool allConnected = true; bool allConnected = true;
for (int i = 0; i < selectionSize && allConnected; ++i) { for (int i = 0; i < selectionSize && allConnected; ++i) {
const Models::Account* mAcc = model->getAccount(sm->selectedRows().at(i).row()); const Models::Account* mAcc = model->getAccount(sm->selectedRows().at(i).row());
allConnected = mAcc->getState() == Shared::ConnectionState::connected; allConnected = allConnected && mAcc->getActive();
} }
if (allConnected) { if (allConnected) {
toDisconnect = true; toDisconnect = true;
m_ui->connectButton->setText(tr("Disconnect")); m_ui->connectButton->setText(tr("Deactivate"));
} else { } else {
toDisconnect = false; toDisconnect = false;
m_ui->connectButton->setText(tr("Connect")); m_ui->connectButton->setText(tr("Activate"));
} }
} else { } else {
m_ui->connectButton->setText(tr("Connect")); m_ui->connectButton->setText(tr("Activate"));
toDisconnect = false; toDisconnect = false;
m_ui->connectButton->setEnabled(false); m_ui->connectButton->setEnabled(false);
} }

View File

@ -19,19 +19,19 @@
#ifndef ACCOUNTS_H #ifndef ACCOUNTS_H
#define ACCOUNTS_H #define ACCOUNTS_H
#include <QWidget> #include <QDialog>
#include <QScopedPointer> #include <QScopedPointer>
#include <QItemSelection> #include <QItemSelection>
#include "account.h" #include "account.h"
#include "../models/accounts.h" #include "ui/models/accounts.h"
namespace Ui namespace Ui
{ {
class Accounts; class Accounts;
} }
class Accounts : public QWidget class Accounts : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:

View File

@ -0,0 +1,60 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "credentialsprompt.h"
#include "ui_credentialsprompt.h"
CredentialsPrompt::CredentialsPrompt(QWidget* parent):
QDialog(parent),
m_ui(new Ui::CredentialsPrompt),
title(),
message()
{
m_ui->setupUi(this);
title = windowTitle();
message = m_ui->message->text();
}
CredentialsPrompt::~CredentialsPrompt()
{
}
void CredentialsPrompt::setAccount(const QString& account)
{
m_ui->message->setText(message.arg(account));
setWindowTitle(title.arg(account));
}
QString CredentialsPrompt::getLogin() const
{
return m_ui->login->text();
}
QString CredentialsPrompt::getPassword() const
{
return m_ui->password->text();
}
void CredentialsPrompt::setLogin(const QString& login)
{
m_ui->login->setText(login);
}
void CredentialsPrompt::setPassword(const QString& password)
{
m_ui->password->setText(password);
}

View File

@ -0,0 +1,52 @@
// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef CREDENTIALSPROMPT_H
#define CREDENTIALSPROMPT_H
#include <qdialog.h>
#include <QScopedPointer>
namespace Ui
{
class CredentialsPrompt;
}
/**
* @todo write docs
*/
class CredentialsPrompt : public QDialog
{
Q_OBJECT
public:
CredentialsPrompt(QWidget* parent = nullptr);
~CredentialsPrompt();
void setAccount(const QString& account);
void setLogin(const QString& login);
void setPassword(const QString& password);
QString getLogin() const;
QString getPassword() const;
private:
QScopedPointer<Ui::CredentialsPrompt> m_ui;
QString title;
QString message;
};
#endif // CREDENTIALSPROMPT_H

View File

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

View File

@ -26,7 +26,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QAbstractTextDocumentLayout> #include <QAbstractTextDocumentLayout>
#include <QCoreApplication> #include <QApplication>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QDir> #include <QDir>
#include <QMenu> #include <QMenu>
@ -57,7 +57,9 @@ Conversation::Conversation(bool muc, Models::Account* acc, Models::Element* el,
tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1), tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1),
pasteImageAction(new QAction(tr("Paste Image"), this)), pasteImageAction(new QAction(tr("Paste Image"), this)),
shadow(10, 1, Qt::black, this), shadow(10, 1, Qt::black, this),
contextMenu(new QMenu()) contextMenu(new QMenu()),
currentAction(CurrentAction::none),
currentMessageId()
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@ -83,20 +85,22 @@ Conversation::Conversation(bool muc, Models::Account* acc, Models::Element* el,
statusIcon = m_ui->statusIcon; statusIcon = m_ui->statusIcon;
statusLabel = m_ui->statusLabel; statusLabel = m_ui->statusLabel;
connect(&ker, &KeyEnterReceiver::enterPressed, this, &Conversation::onEnterPressed); connect(&ker, &KeyEnterReceiver::enterPressed, this, qOverload<>(&Conversation::initiateMessageSending));
connect(&ker, &KeyEnterReceiver::imagePasted, this, &Conversation::onImagePasted); connect(&ker, &KeyEnterReceiver::imagePasted, this, &Conversation::onImagePasted);
connect(m_ui->sendButton, &QPushButton::clicked, this, &Conversation::onEnterPressed); connect(m_ui->sendButton, &QPushButton::clicked, this, qOverload<>(&Conversation::initiateMessageSending));
connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach); connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach);
connect(m_ui->clearButton, &QPushButton::clicked, this, &Conversation::onClearButton); connect(m_ui->clearButton, &QPushButton::clicked, this, &Conversation::clear);
connect(m_ui->messageEditor->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, connect(m_ui->messageEditor->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged,
this, &Conversation::onTextEditDocSizeChanged); this, &Conversation::onTextEditDocSizeChanged);
m_ui->messageEditor->installEventFilter(&ker); m_ui->messageEditor->installEventFilter(&ker);
m_ui->messageEditor->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_ui->messageEditor, &QTextEdit::customContextMenuRequested, this, &Conversation::onMessageEditorContext); connect(m_ui->messageEditor, &QTextEdit::customContextMenuRequested, this, &Conversation::onMessageEditorContext);
connect(pasteImageAction, &QAction::triggered, this, &Conversation::onImagePasted); connect(pasteImageAction, &QAction::triggered, this, &Conversation::onImagePasted);
connect(m_ui->currentActionBadge, &Badge::close, this, &Conversation::clear);
m_ui->currentActionBadge->setVisible(false);
//line->setAutoFillBackground(false); //line->setAutoFillBackground(false);
//if (testAttribute(Qt::WA_TranslucentBackground)) { //if (testAttribute(Qt::WA_TranslucentBackground)) {
//m_ui->scrollArea->setAutoFillBackground(false); //m_ui->scrollArea->setAutoFillBackground(false);
@ -220,24 +224,33 @@ void Conversation::setPalResource(const QString& res)
activePalResource = res; activePalResource = res;
} }
void Conversation::onEnterPressed() void Conversation::initiateMessageSending()
{ {
QString body(m_ui->messageEditor->toPlainText()); QString body(m_ui->messageEditor->toPlainText());
if (body.size() > 0) { if (body.size() > 0) {
m_ui->messageEditor->clear();
Shared::Message msg = createMessage(); Shared::Message msg = createMessage();
msg.setBody(body); msg.setBody(body);
emit sendMessage(msg); initiateMessageSending(msg);
} }
if (filesToAttach.size() > 0) { if (filesToAttach.size() > 0) {
for (Badge* badge : filesToAttach) { for (Badge* badge : filesToAttach) {
Shared::Message msg = createMessage(); Shared::Message msg = createMessage();
msg.setAttachPath(badge->id); msg.setAttachPath(badge->id);
element->feed->registerUpload(msg.getId()); element->feed->registerUpload(msg.getId());
emit sendMessage(msg); initiateMessageSending(msg);
} }
clearAttachedFiles(); }
clear();
}
void Conversation::initiateMessageSending(const Shared::Message& msg)
{
if (currentAction == CurrentAction::edit) {
emit replaceMessage(currentMessageId, msg);
currentAction = CurrentAction::none;
} else {
emit sendMessage(msg);
} }
} }
@ -344,8 +357,11 @@ void Conversation::clearAttachedFiles()
filesLayout->setContentsMargins(0, 0, 0, 0); filesLayout->setContentsMargins(0, 0, 0, 0);
} }
void Conversation::onClearButton() void Conversation::clear()
{ {
currentMessageId.clear();
currentAction = CurrentAction::none;
m_ui->currentActionBadge->setVisible(false);
clearAttachedFiles(); clearAttachedFiles();
m_ui->messageEditor->clear(); m_ui->messageEditor->clear();
} }
@ -472,18 +488,38 @@ void Conversation::onFeedContext(const QPoint& pos)
Shared::Message* item = static_cast<Shared::Message*>(index.internalPointer()); Shared::Message* item = static_cast<Shared::Message*>(index.internalPointer());
contextMenu->clear(); contextMenu->clear();
QString id = item->getId();
bool showMenu = false; bool showMenu = false;
if (item->getState() == Shared::Message::State::error) { if (item->getState() == Shared::Message::State::error) {
showMenu = true; showMenu = true;
QString id = item->getId();
QAction* resend = contextMenu->addAction(Shared::icon("view-refresh"), tr("Try sending again")); QAction* resend = contextMenu->addAction(Shared::icon("view-refresh"), tr("Try sending again"));
connect(resend, &QAction::triggered, [this, id]() { connect(resend, &QAction::triggered, [this, id]() {
element->feed->registerUpload(id); element->feed->registerUpload(id);
emit resendMessage(id); emit resendMessage(id);
}); });
} }
QString selected = feed->getSelectedText();
if (selected.size() > 0) {
showMenu = true;
QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected"));
connect(copy, &QAction::triggered, [selected] () {
QClipboard* cb = QApplication::clipboard();
cb->setText(selected);
});
}
QString body = item->getBody();
if (body.size() > 0) {
showMenu = true;
QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy message"));
connect(copy, &QAction::triggered, [body] () {
QClipboard* cb = QApplication::clipboard();
cb->setText(body);
});
}
QString path = item->getAttachPath(); QString path = Shared::resolvePath(item->getAttachPath());
if (path.size() > 0) { if (path.size() > 0) {
showMenu = true; showMenu = true;
QAction* open = contextMenu->addAction(Shared::icon("document-preview"), tr("Open")); QAction* open = contextMenu->addAction(Shared::icon("document-preview"), tr("Open"));
@ -496,6 +532,14 @@ void Conversation::onFeedContext(const QPoint& pos)
Shared::Global::highlightInFileManager(path); Shared::Global::highlightInFileManager(path);
}); });
} }
bool hasAttach = item->getAttachPath() > 0 || item->getOutOfBandUrl() > 0;
//the only mandatory condition - is for the message to be outgoing, the rest is just a good intention on the server
if (item->getOutgoing() && !hasAttach && index.row() < 100 && item->getTime().daysTo(QDateTime::currentDateTimeUtc()) < 20) {
showMenu = true;
QAction* edit = contextMenu->addAction(Shared::icon("edit-rename"), tr("Edit"));
connect(edit, &QAction::triggered, this, std::bind(&Conversation::onMessageEditRequested, this, id));
}
if (showMenu) { if (showMenu) {
contextMenu->popup(feed->viewport()->mapToGlobal(pos)); contextMenu->popup(feed->viewport()->mapToGlobal(pos));
@ -513,3 +557,34 @@ void Conversation::onMessageEditorContext(const QPoint& pos)
editorMenu->exec(this->m_ui->messageEditor->mapToGlobal(pos)); editorMenu->exec(this->m_ui->messageEditor->mapToGlobal(pos));
} }
void Conversation::onMessageEditRequested(const QString& id)
{
clear();
try {
Shared::Message msg = element->feed->getMessage(id);
currentMessageId = id;
m_ui->currentActionBadge->setVisible(true);
m_ui->currentActionBadge->setText(tr("Editing message..."));
currentAction = CurrentAction::edit;
m_ui->messageEditor->setText(msg.getBody());
QString path = msg.getAttachPath();
if (path.size() > 0) {
addAttachedFile(path);
}
} catch (const Models::MessageFeed::NotFound& e) {
qDebug() << "The message requested to be edited was not found" << e.getMessage().c_str();
qDebug() << "Ignoring";
}
}
void Conversation::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
emit shown();
m_ui->messageEditor->setFocus();
}

View File

@ -33,6 +33,7 @@
#include "shared/order.h" #include "shared/order.h"
#include "shared/icons.h" #include "shared/icons.h"
#include "shared/utils.h" #include "shared/utils.h"
#include "shared/pathcheck.h"
#include "ui/models/account.h" #include "ui/models/account.h"
#include "ui/models/roster.h" #include "ui/models/roster.h"
@ -82,6 +83,7 @@ public:
signals: signals:
void sendMessage(const Shared::Message& message); void sendMessage(const Shared::Message& message);
void replaceMessage(const QString& originalId, const Shared::Message& message);
void resendMessage(const QString& id); void resendMessage(const QString& id);
void requestArchive(const QString& before); void requestArchive(const QString& before);
void shown(); void shown();
@ -101,25 +103,33 @@ protected:
void dropEvent(QDropEvent* event) override; void dropEvent(QDropEvent* event) override;
void initializeOverlay(); void initializeOverlay();
virtual void onMessage(const Shared::Message& msg); virtual void onMessage(const Shared::Message& msg);
virtual void showEvent(QShowEvent * event) override;
protected slots: protected slots:
void onEnterPressed(); void initiateMessageSending();
void initiateMessageSending(const Shared::Message& msg);
void onImagePasted(); void onImagePasted();
void onAttach(); void onAttach();
void onFileSelected(); void onFileSelected();
void onBadgeClose(); void onBadgeClose();
void onClearButton(); void clear();
void onTextEditDocSizeChanged(const QSizeF& size); void onTextEditDocSizeChanged(const QSizeF& size);
void onAccountChanged(Models::Item* item, int row, int col); void onAccountChanged(Models::Item* item, int row, int col);
void onFeedMessage(const Shared::Message& msg); void onFeedMessage(const Shared::Message& msg);
void positionShadow(); void positionShadow();
void onFeedContext(const QPoint &pos); void onFeedContext(const QPoint &pos);
void onMessageEditorContext(const QPoint &pos); void onMessageEditorContext(const QPoint &pos);
void onMessageEditRequested(const QString& id);
public: public:
const bool isMuc; const bool isMuc;
protected: protected:
enum class CurrentAction {
none,
edit
};
Models::Account* account; Models::Account* account;
Models::Element* element; Models::Element* element;
QString palJid; QString palJid;
@ -141,6 +151,8 @@ protected:
ShadowOverlay shadow; ShadowOverlay shadow;
QMenu* contextMenu; QMenu* contextMenu;
CurrentAction currentAction;
QString currentMessageId;
private: private:
static bool painterInitialized; static bool painterInitialized;

View File

@ -279,6 +279,29 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<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>
<widget class="Badge" name="currentActionBadge">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
@ -379,6 +402,9 @@
<height>30</height> <height>30</height>
</size> </size>
</property> </property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="autoFillBackground"> <property name="autoFillBackground">
<bool>false</bool> <bool>false</bool>
</property> </property>
@ -400,7 +426,7 @@ background-color: transparent
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Liberation Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; 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;</string> &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;</string>
</property> </property>
<property name="acceptRichText"> <property name="acceptRichText">
@ -419,6 +445,14 @@ p, li { white-space: pre-wrap; }
</item> </item>
</layout> </layout>
</widget> </widget>
<customwidgets>
<customwidget>
<class>Badge</class>
<extends>QFrame</extends>
<header location="global">ui/utils/badge.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources> <resources>
<include location="../../resources/resources.qrc"/> <include location="../../resources/resources.qrc"/>
</resources> </resources>

View File

@ -1,14 +1,10 @@
target_sources(squawk PRIVATE target_sources(squawk PRIVATE
messagedelegate.cpp messagedelegate.cpp
messagedelegate.h messagedelegate.h
#messageline.cpp
#messageline.h
preview.cpp preview.cpp
preview.h preview.h
messagefeed.cpp messagefeed.cpp
messagefeed.h messagefeed.h
feedview.cpp feedview.cpp
feedview.h feedview.h
#message.cpp
#message.h
) )

View File

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

View File

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

View File

@ -1,344 +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 "message.h"
#include <QDebug>
#include <QMimeDatabase>
#include <QPixmap>
#include <QFileInfo>
#include <QRegularExpression>
Message::Message(const Shared::Message& source, bool p_outgoing, const QString& p_sender, const QString& avatarPath, QWidget* parent):
QWidget(parent),
outgoing(p_outgoing),
msg(source),
body(new QWidget()),
statusBar(new QWidget()),
bodyLayout(new QVBoxLayout(body)),
layout(new QHBoxLayout(this)),
date(new QLabel(msg.getTime().toLocalTime().toString())),
sender(new QLabel(p_sender)),
text(new QLabel()),
shadow(new QGraphicsDropShadowEffect()),
button(0),
file(0),
progress(0),
fileComment(new QLabel()),
statusIcon(0),
editedLabel(0),
avatar(new Image(avatarPath.size() == 0 ? Shared::iconPath("user", true) : avatarPath, 60)),
hasButton(false),
hasProgress(false),
hasFile(false),
commentAdded(false),
hasStatusIcon(false),
hasEditedLabel(false)
{
setContentsMargins(0, 0, 0, 0);
layout->setContentsMargins(10, 5, 10, 5);
body->setBackgroundRole(QPalette::AlternateBase);
body->setAutoFillBackground(true);
QString bd = Shared::processMessageBody(msg.getBody());
text->setTextFormat(Qt::RichText);
text->setText(bd);;
text->setTextInteractionFlags(text->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
text->setWordWrap(true);
text->setOpenExternalLinks(true);
if (bd.size() == 0) {
text->hide();
}
QFont dFont = date->font();
dFont.setItalic(true);
dFont.setPointSize(dFont.pointSize() - 2);
date->setFont(dFont);
QFont f;
f.setBold(true);
sender->setFont(f);
bodyLayout->addWidget(sender);
bodyLayout->addWidget(text);
shadow->setBlurRadius(10);
shadow->setXOffset(1);
shadow->setYOffset(1);
shadow->setColor(Qt::black);
body->setGraphicsEffect(shadow);
avatar->setMaximumHeight(60);
avatar->setMaximumWidth(60);
statusBar->setContentsMargins(0, 0, 0, 0);
QHBoxLayout* statusLay = new QHBoxLayout();
statusLay->setContentsMargins(0, 0, 0, 0);
statusBar->setLayout(statusLay);
if (outgoing) {
sender->setAlignment(Qt::AlignRight);
date->setAlignment(Qt::AlignRight);
statusIcon = new QLabel();
setState();
statusLay->addWidget(statusIcon);
statusLay->addWidget(date);
layout->addStretch();
layout->addWidget(body);
layout->addWidget(avatar);
hasStatusIcon = true;
} else {
layout->addWidget(avatar);
layout->addWidget(body);
layout->addStretch();
statusLay->addWidget(date);
}
if (msg.getEdited()) {
setEdited();
}
bodyLayout->addWidget(statusBar);
layout->setAlignment(avatar, Qt::AlignTop);
}
Message::~Message()
{
if (!commentAdded) {
delete fileComment;
}
//delete body; //not sure if I should delete it here, it's probably already owned by the infrastructure and gonna die with the rest of the widget
//delete avatar;
}
QString Message::getId() const
{
return msg.getId();
}
QString Message::getSenderJid() const
{
return msg.getFromJid();
}
QString Message::getSenderResource() const
{
return msg.getFromResource();
}
QString Message::getFileUrl() const
{
return msg.getOutOfBandUrl();
}
void Message::setSender(const QString& p_sender)
{
sender->setText(p_sender);
}
void Message::addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip)
{
hideFile();
hideProgress();
if (!hasButton) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
button = new QPushButton(icon, buttonText);
button->setToolTip(tooltip);
connect(button, &QPushButton::clicked, this, &Message::buttonClicked);
bodyLayout->insertWidget(2, button);
hasButton = true;
}
}
void Message::setProgress(qreal value)
{
hideFile();
hideButton();
if (!hasProgress) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
progress = new QProgressBar();
progress->setRange(0, 100);
bodyLayout->insertWidget(2, progress);
hasProgress = true;
}
progress->setValue(value * 100);
}
void Message::showFile(const QString& path)
{
hideButton();
hideProgress();
if (!hasFile) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
QMimeDatabase db;
QMimeType type = db.mimeTypeForFile(path);
QStringList parts = type.name().split("/");
QString big = parts.front();
QFileInfo info(path);
if (big == "image") {
file = new Image(path);
} else {
file = new QLabel();
file->setPixmap(QIcon::fromTheme(type.iconName()).pixmap(50));
file->setAlignment(Qt::AlignCenter);
showComment(info.fileName(), true);
}
file->setContextMenuPolicy(Qt::ActionsContextMenu);
QAction* openAction = new QAction(QIcon::fromTheme("document-new-from-template"), tr("Open"), file);
connect(openAction, &QAction::triggered, [path]() { //TODO need to get rid of this shame
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
file->addAction(openAction);
bodyLayout->insertWidget(2, file);
hasFile = true;
}
}
void Message::hideComment()
{
if (commentAdded) {
bodyLayout->removeWidget(fileComment);
fileComment->hide();
fileComment->setWordWrap(false);
commentAdded = false;
}
}
void Message::hideButton()
{
if (hasButton) {
button->deleteLater();
button = 0;
hasButton = false;
}
}
void Message::hideFile()
{
if (hasFile) {
file->deleteLater();
file = 0;
hasFile = false;
}
}
void Message::hideProgress()
{
if (hasProgress) {
progress->deleteLater();
progress = 0;
hasProgress = false;;
}
}
void Message::showComment(const QString& comment, bool wordWrap)
{
if (!commentAdded) {
int index = 2;
if (hasFile) {
index++;
}
if (hasButton) {
index++;
}
if (hasProgress) {
index++;
}
bodyLayout->insertWidget(index, fileComment);
fileComment->show();
commentAdded = true;
}
fileComment->setWordWrap(wordWrap);
fileComment->setText(comment);
}
const Shared::Message & Message::getMessage() const
{
return msg;
}
void Message::setAvatarPath(const QString& p_path)
{
if (p_path.size() == 0) {
avatar->setPath(Shared::iconPath("user", true));
} else {
avatar->setPath(p_path);
}
}
bool Message::change(const QMap<QString, QVariant>& data)
{
bool idChanged = msg.change(data);
QString body = msg.getBody();
QString bd = Shared::processMessageBody(body);
if (body.size() > 0) {
text->setText(bd);
text->show();
} else {
text->setText(body);
text->hide();
}
if (msg.getEdited()) {
setEdited();
}
if (hasStatusIcon) {
setState();
}
return idChanged;
}
void Message::setEdited()
{
if (!hasEditedLabel) {
editedLabel = new QLabel();
hasEditedLabel = true;
QIcon q(Shared::icon("edit-rename"));
editedLabel->setPixmap(q.pixmap(12, 12));
QHBoxLayout* statusLay = static_cast<QHBoxLayout*>(statusBar->layout());
statusLay->insertWidget(1, editedLabel);
}
editedLabel->setToolTip("Last time edited: " + msg.getLastModified().toLocalTime().toString()
+ "\nOriginal message: " + msg.getOriginalBody());
}
void Message::setState()
{
Shared::Message::State state = msg.getState();
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(state)]));
QString tt = Shared::Global::getName(state);
if (state == Shared::Message::State::error) {
QString errText = msg.getErrorText();
if (errText.size() > 0) {
tt += ": " + errText;
}
}
statusIcon->setToolTip(tt);
statusIcon->setPixmap(q.pixmap(12, 12));
}

View File

@ -1,103 +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/>.
*/
#ifndef MESSAGE_H
#define MESSAGE_H
#include <QWidget>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QGraphicsDropShadowEffect>
#include <QPushButton>
#include <QProgressBar>
#include <QAction>
#include <QDesktopServices>
#include <QUrl>
#include <QMap>
#include "shared/message.h"
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
#include "resizer.h"
#include "image.h"
/**
* @todo write docs
*/
class Message : public QWidget
{
Q_OBJECT
public:
Message(const Shared::Message& source, bool outgoing, const QString& sender, const QString& avatarPath = "", QWidget* parent = nullptr);
~Message();
void setSender(const QString& sender);
QString getId() const;
QString getSenderJid() const;
QString getSenderResource() const;
QString getFileUrl() const;
const Shared::Message& getMessage() const;
void addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip = "");
void showComment(const QString& comment, bool wordWrap = false);
void hideComment();
void showFile(const QString& path);
void setProgress(qreal value);
void setAvatarPath(const QString& p_path);
bool change(const QMap<QString, QVariant>& data);
bool const outgoing;
signals:
void buttonClicked();
private:
Shared::Message msg;
QWidget* body;
QWidget* statusBar;
QVBoxLayout* bodyLayout;
QHBoxLayout* layout;
QLabel* date;
QLabel* sender;
QLabel* text;
QGraphicsDropShadowEffect* shadow;
QPushButton* button;
QLabel* file;
QProgressBar* progress;
QLabel* fileComment;
QLabel* statusIcon;
QLabel* editedLabel;
Image* avatar;
bool hasButton;
bool hasProgress;
bool hasFile;
bool commentAdded;
bool hasStatusIcon;
bool hasEditedLabel;
private:
void hideButton();
void hideProgress();
void hideFile();
void setState();
void setEdited();
};
#endif // MESSAGE_H

View File

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

View File

@ -86,7 +86,7 @@ void Models::MessageFeed::addMessage(const Shared::Message& msg)
emit newMessage(msg); emit newMessage(msg);
if (observersAmount == 0 && !msg.getForwarded()) { //not to notify when the message is delivered by the carbon copy if (observersAmount == 0 && !msg.getForwarded()) { //not to notify when the message is delivered by the carbon copy
unreadMessages->insert(msg.getId()); //cuz it could be my own one or the one I read on another device unreadMessages->insert(id); //cuz it could be my own one or the one I read on another device
emit unreadMessagesCountChanged(); emit unreadMessagesCountChanged();
emit unnoticedMessage(msg); emit unnoticedMessage(msg);
} }
@ -163,6 +163,12 @@ void Models::MessageFeed::changeMessage(const QString& id, const QMap<QString, Q
} }
emit dataChanged(index, index, cr); emit dataChanged(index, index, cr);
if (observersAmount == 0 && !msg->getForwarded() && changeRoles.count(MessageRoles::Text) > 0) {
unreadMessages->insert(id);
emit unreadMessagesCountChanged();
emit unnoticedMessage(*msg);
}
} }
} }
@ -224,8 +230,20 @@ std::set<Models::MessageFeed::MessageRoles> Models::MessageFeed::detectChanges(c
void Models::MessageFeed::removeMessage(const QString& id) void Models::MessageFeed::removeMessage(const QString& id)
{ {
//todo;
} }
Shared::Message Models::MessageFeed::getMessage(const QString& id)
{
StorageById::iterator itr = indexById.find(id);
if (itr == indexById.end()) {
throw NotFound(id.toStdString(), rosterItem->getJid().toStdString(), rosterItem->getAccountName().toStdString());
}
return **itr;
}
QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
{ {
int i = index.row(); int i = index.row();
@ -300,12 +318,7 @@ QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
case Bulk: { case Bulk: {
FeedItem item; FeedItem item;
item.id = msg->getId(); item.id = msg->getId();
markMessageAsRead(item.id);
std::set<QString>::const_iterator umi = unreadMessages->find(item.id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCountChanged();
}
item.sentByMe = sentByMe(*msg); item.sentByMe = sentByMe(*msg);
item.date = msg->getTime(); item.date = msg->getTime();
@ -355,6 +368,17 @@ int Models::MessageFeed::rowCount(const QModelIndex& parent) const
return storage.size(); return storage.size();
} }
bool Models::MessageFeed::markMessageAsRead(const QString& id) const
{
std::set<QString>::const_iterator umi = unreadMessages->find(id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCountChanged();
return true;
}
return false;
}
unsigned int Models::MessageFeed::unreadMessagesCount() const unsigned int Models::MessageFeed::unreadMessagesCount() const
{ {
return unreadMessages->size(); return unreadMessages->size();

View File

@ -32,6 +32,7 @@
#include <shared/message.h> #include <shared/message.h>
#include <shared/icons.h> #include <shared/icons.h>
#include <shared/exception.h>
namespace Models { namespace Models {
@ -55,6 +56,9 @@ public:
void addMessage(const Shared::Message& msg); void addMessage(const Shared::Message& msg);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data); void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void removeMessage(const QString& id); void removeMessage(const QString& id);
Shared::Message getMessage(const QString& id);
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@ -70,6 +74,7 @@ public:
void reportLocalPathInvalid(const QString& messageId); void reportLocalPathInvalid(const QString& messageId);
unsigned int unreadMessagesCount() const; unsigned int unreadMessagesCount() const;
bool markMessageAsRead(const QString& id) const;
void fileProgress(const QString& messageId, qreal value, bool up); void fileProgress(const QString& messageId, qreal value, bool up);
void fileError(const QString& messageId, const QString& error, bool up); void fileError(const QString& messageId, const QString& error, bool up);
void fileComplete(const QString& messageId, bool up); void fileComplete(const QString& messageId, bool up);
@ -103,13 +108,26 @@ public:
Error, Error,
Bulk Bulk
}; };
class NotFound:
public Utils::Exception
{
public:
NotFound(const std::string& k, const std::string& j, const std::string& acc):Exception(), key(k), jid(j), account(acc){}
std::string getMessage() const {
return "Message with id " + key + " wasn't found in messageFeed " + account + " of the chat with " + jid;
}
private:
std::string key;
std::string jid;
std::string account;
};
protected: protected:
bool sentByMe(const Shared::Message& msg) const; bool sentByMe(const Shared::Message& msg) const;
Attachment fillAttach(const Shared::Message& msg) const; Attachment fillAttach(const Shared::Message& msg) const;
Edition fillCorrection(const Shared::Message& msg) const; Edition fillCorrection(const Shared::Message& msg) const;
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const; std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const;
private: private:

Some files were not shown because too many files have changed in this diff Show More