Compare commits

...

30 Commits

Author SHA1 Message Date
f45319de25 now instead of storing uploading message in ram I store them in database to be able to recover unsent ones on the next statrt. Found and fixed bug with spam repaints in feedview because of icons 2021-05-07 21:26:02 +03:00
ebf0c64ffb highlight in directory now is optional runtime plugin to KIO, several more file managers to fallback, refactor, 2 new icons 2021-05-06 17:44:43 +03:00
d514db9c4a message context menu began, open and show in folder features 2021-05-04 17:09:41 +03:00
f34289399e text inside of message is selectable again, links are clickable, some refactor, some bugfixes 2021-05-03 14:23:41 +03:00
05d6761baa a bit of refactor, fix the time to request next portion of messages in ui, fancy shadows are back! 2021-05-03 03:35:43 +03:00
216dcd29e9 bug fixing, better progres indicator positioning 2021-05-02 02:03:08 +03:00
0973cb2991 first lousy attempt to make load indicator in feedView 2021-04-30 23:07:00 +03:00
50190f3eac handled a case when user removes downloaded file, minor optimizations on message changing 2021-04-28 23:26:19 +03:00
b44873d587 pal resourse sticking, notifications of unread messages, new message icons 2021-04-27 22:29:15 +03:00
4c5efad9dc CMake build error, status icon text tooltip 2021-04-26 19:37:36 +03:00
8310708c92 First attemtps to upload files, debug, reused of once uploaded or downloaded files 2021-04-23 14:53:48 +03:00
d936c0302d bug with downloads in group chats, status icons in messages, visuals, feedView optimisations 2021-04-23 01:41:32 +03:00
0e937199b0 debug and actual first way to display pictures in messageFeed 2021-04-21 00:56:47 +03:00
48e498be25 some debug, message changing in messageFeed 2021-04-20 00:49:24 +03:00
3a7735b192 First steps on the new idea of file up/downloading 2021-04-18 15:49:20 +03:00
8f914c02a7 temp url storage commit 2021-04-13 16:27:31 +03:00
50bb3f5fd7 started progress bars, changed gcc standard to 17 2021-03-22 21:04:26 +03:00
a0348b8fd2 file progress events delivery methonds 2021-02-27 15:21:27 +03:00
85555da81f just a temp one 2021-02-07 20:02:11 +03:00
ebe5addfb5 just proxying button event from feed view delegate to the feed model 2021-02-06 14:02:42 +03:00
b3c6860e25 seems like i have found solution how to properly render buttons 2021-02-02 01:55:15 +03:00
00ffbac6b0 initial attempt to paint buttons in the messagefeed 2021-01-14 14:22:02 +03:00
ff4124d1f0 Resolved the bug about crash with an empty history chat 2021-01-12 20:15:21 +03:00
15342f3c53 self nick in the chat fix, hovering message feature 2021-01-08 00:50:12 +03:00
270a32db9e achive from the beginning memorizing bugfix, limitation of the requests in the model 2020-08-21 23:57:48 +03:00
e0ef1ef797 Some basic message painting 2020-08-21 00:32:30 +03:00
e1eea2f3a2 made the first prototype, scrolling and word wrapping seems to be working 2020-08-17 13:27:14 +03:00
e54cff0f0c changed my mind, gonna implement the feed on qt instead of qml, first tries, nothing working yet 2020-08-16 00:48:28 +03:00
4e6bd04b02 experimenting with qml 2020-08-12 19:55:01 +03:00
38159eafeb first compiling prototype, doesnt work yet 2020-08-12 01:49:51 +03:00
78 changed files with 4392 additions and 1678 deletions

View File

@ -6,10 +6,14 @@
- requesting the history of the current chat after reconnection
- global availability (in drop down list) gets restored after reconnection
- status icon in active chat changes when presence of the pen pal changes
- infinite progress when open the dialogue with something that has no history to show
### Improvements
- slightly reduced the traffic on the startup by not requesting history of all MUCs
- completely rewritten message feed, now it works way faster
- OPTIONAL RUNTIME dependency: "KIO Widgets" that is supposed to allow you to open a file in your default file manager
- show in folder now is supposed to try it's best to show file in folder, even you don't have KIO installed
- once uploaded local files don't get second time uploaded - the remote URL is reused
## Squawk 0.1.5 (Jul 29, 2020)
### Bug fixes

View File

@ -1,8 +1,8 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.4)
project(squawk)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
@ -12,6 +12,7 @@ include(GNUInstallDirs)
include_directories(.)
find_package(Qt5Widgets CONFIG REQUIRED)
find_package(Qt5QuickCompiler CONFIG REQUIRED)
find_package(Qt5LinguistTools)
if(NOT CMAKE_BUILD_TYPE)
@ -32,6 +33,7 @@ set(squawk_SRC
shared/message.cpp
shared/vcard.cpp
shared/icons.cpp
shared/messageinfo.cpp
)
set(squawk_HEAD
@ -44,6 +46,7 @@ set(squawk_HEAD
shared/utils.h
shared/vcard.h
shared/icons.h
shared/messageinfo.h
)
configure_file(resources/images/logo.svg squawk.svg COPYONLY)
@ -64,6 +67,7 @@ qt5_add_resources(RCC resources/resources.qrc)
option(SYSTEM_QXMPP "Use system qxmpp lib" ON)
option(WITH_KWALLET "Build KWallet support module" ON)
option(WITH_KIO "Build KIO support module" ON)
if (SYSTEM_QXMPP)
find_package(QXmpp CONFIG)
@ -95,8 +99,21 @@ endif()
add_executable(squawk ${squawk_SRC} ${squawk_HEAD} ${RCC})
target_link_libraries(squawk Qt5::Widgets)
if (WITH_KIO)
find_package(KF5KIO CONFIG)
if (NOT KF5KIO_FOUND)
set(WITH_KIO OFF)
message("KIO package wasn't found, KIO support modules wouldn't be built")
else()
add_definitions(-DWITH_KIO)
message("Building with support of KIO")
endif()
endif()
add_subdirectory(ui)
add_subdirectory(core)
add_subdirectory(plugins)
add_subdirectory(external/simpleCrypt)
@ -104,6 +121,8 @@ target_link_libraries(squawk squawkUI)
target_link_libraries(squawk squawkCORE)
target_link_libraries(squawk uuid)
add_dependencies(${CMAKE_PROJECT_NAME} translations)
# Install the executable

View File

@ -67,6 +67,7 @@ Here is the list of keys you can pass to configuration phase of `cmake ..`.
- `CMAKE_BUILD_TYPE` - `Debug` just builds showing all warnings, `Release` builds with no warnings and applies optimizations (default is `Debug`)
- `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_KIO` - `True` builds the `KIO` capability module if `KIO` is installed and if not goes to `False`. `False` disables `KIO` support (default is `True`)
## License

View File

@ -15,7 +15,7 @@ set(squawkCORE_SRC
rosteritem.cpp
contact.cpp
conference.cpp
storage.cpp
urlstorage.cpp
networkaccess.cpp
adapterFuctions.cpp
handlers/messagehandler.cpp

View File

@ -84,8 +84,9 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
QObject::connect(dm, &QXmppDiscoveryManager::itemsReceived, this, &Account::onDiscoveryItemsReceived);
QObject::connect(dm, &QXmppDiscoveryManager::infoReceived, this, &Account::onDiscoveryInfoReceived);
QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded);
QObject::connect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError);
QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete);
QObject::connect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete);
QObject::connect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError);
client.addExtension(rcpm);
QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived);
@ -155,8 +156,9 @@ Account::~Account()
reconnectTimer->stop();
}
QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded);
QObject::disconnect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError);
QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete);
QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete);
QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError);
delete mh;
delete rh;
@ -402,9 +404,6 @@ QString Core::Account::getFullJid() const {
void Core::Account::sendMessage(const Shared::Message& data) {
mh->sendMessage(data);}
void Core::Account::sendMessage(const Shared::Message& data, const QString& path) {
mh->sendMessage(data, path);}
void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg)
{
if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) {
@ -434,13 +433,13 @@ void Core::Account::requestArchive(const QString& jid, int count, const QString&
if (contact == 0) {
qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the contact with such id wasn't found, skipping";
emit responseArchive(jid, std::list<Shared::Message>());
emit responseArchive(jid, std::list<Shared::Message>(), true);
return;
}
if (state != Shared::ConnectionState::connected) {
qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the account is not online, skipping";
emit responseArchive(contact->jid, std::list<Shared::Message>());
emit responseArchive(contact->jid, std::list<Shared::Message>(), false);
}
contact->requestHistory(count, before);
@ -552,9 +551,11 @@ void Core::Account::onClientError(QXmppClient::Error err)
case QXmppStanza::Error::NotAuthorized:
errorText = "Authentication error";
break;
#if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0)
case QXmppStanza::Error::PaymentRequired:
errorText = "Payment is required";
break;
#endif
case QXmppStanza::Error::RecipientUnavailable:
errorText = "Recipient is unavailable";
break;
@ -909,3 +910,16 @@ void Core::Account::handleDisconnection()
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);}

View File

@ -88,7 +88,6 @@ public:
void setPasswordType(Shared::AccountPassword pt);
QString getFullJid() const;
void sendMessage(const Shared::Message& data);
void sendMessage(const Shared::Message& data, const QString& path);
void requestArchive(const QString& jid, int count, const QString& before);
void subscribeToContact(const QString& jid, const QString& reason);
void unsubscribeFromContact(const QString& jid, const QString& reason);
@ -97,6 +96,7 @@ public:
void addContactToGroupRequest(const QString& jid, const QString& groupName);
void removeContactFromGroupRequest(const QString& jid, const QString& groupName);
void renameContactRequest(const QString& jid, const QString& newName);
void requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data);
void setRoomJoined(const QString& jid, bool joined);
void setRoomAutoJoin(const QString& jid, bool joined);
@ -127,14 +127,14 @@ signals:
void removePresence(const QString& jid, const QString& name);
void message(const Shared::Message& data);
void changeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void responseArchive(const QString& jid, const std::list<Shared::Message>& list);
void responseArchive(const QString& jid, const std::list<Shared::Message>& list, bool last);
void error(const QString& text);
void addRoomParticipant(const QString& jid, const QString& nickName, const QMap<QString, QVariant>& data);
void changeRoomParticipant(const QString& jid, const QString& nickName, const QMap<QString, QVariant>& data);
void removeRoomParticipant(const QString& jid, const QString& nickName);
void receivedVCard(const QString& jid, const Shared::VCard& card);
void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap<QString, QString> headers);
void uploadFileError(const QString& messageId, const QString& error);
void uploadFileError(const QString& jid, const QString& messageId, const QString& error);
private:
QString name;
@ -183,6 +183,7 @@ private slots:
void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items);
void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info);
void onContactHistoryResponse(const std::list<Shared::Message>& list, bool last);
private:
void handleDisconnection();

View File

@ -271,6 +271,8 @@ void Core::Archive::changeMessage(const QString& id, const QMap<QString, QVarian
bool hadStanzaId = msg.getStanzaId().size() > 0;
QDateTime oTime = msg.getTime();
bool idChange = msg.change(data);
QDateTime nTime = msg.getTime();
bool orderChange = oTime != nTime;
MDB_val lmdbKey, lmdbData;
QByteArray ba;
@ -280,15 +282,21 @@ void Core::Archive::changeMessage(const QString& id, const QMap<QString, QVarian
lmdbKey.mv_size = strId.size();
lmdbKey.mv_data = (char*)strId.c_str();
int rc;
if (idChange || orderChange) {
if (idChange) {
rc = mdb_del(txn, main, &lmdbKey, &lmdbData);
} else {
quint64 ostamp = oTime.toMSecsSinceEpoch();
lmdbData.mv_data = (quint8*)&ostamp;
lmdbData.mv_size = 8;
rc = mdb_del(txn, order, &lmdbData, &lmdbKey);
}
if (rc == 0) {
strId = msg.getId().toStdString();
lmdbKey.mv_size = strId.size();
lmdbKey.mv_data = (char*)strId.c_str();
quint64 stamp = oTime.toMSecsSinceEpoch();
quint64 stamp = nTime.toMSecsSinceEpoch();
lmdbData.mv_data = (quint8*)&stamp;
lmdbData.mv_size = 8;
rc = mdb_put(txn, order, &lmdbData, &lmdbKey, 0);
@ -502,8 +510,9 @@ long unsigned int Core::Archive::size() const
mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn);
MDB_stat stat;
mdb_stat(txn, order, &stat);
size_t amount = stat.ms_entries;
mdb_txn_abort(txn);
return stat.ms_entries;
return amount;
}
std::list<Shared::Message> Core::Archive::getBefore(int count, const QString& id)
@ -603,10 +612,10 @@ void Core::Archive::setFromTheBeginning(bool is)
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
bool success = setStatValue("beginning", is, txn);
if (success != 0) {
mdb_txn_abort(txn);
} else {
if (success) {
mdb_txn_commit(txn);
} else {
mdb_txn_abort(txn);
}
}
}

View File

@ -23,7 +23,6 @@ Core::MessageHandler::MessageHandler(Core::Account* account):
QObject(),
acc(account),
pendingStateMessages(),
pendingMessages(),
uploadingSlotsQueue()
{
}
@ -168,14 +167,16 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
id = source.id();
#endif
target.setId(id);
if (target.getId().size() == 0) {
QString messageId = target.getId();
if (messageId.size() == 0) {
target.generateRandomId(); //TODO out of desperation, I need at least a random ID
messageId = target.getId();
}
target.setFrom(source.from());
target.setTo(source.to());
target.setBody(source.body());
target.setForwarded(forwarded);
target.setOutOfBandUrl(source.outOfBandUrl());
if (guessing) {
if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) {
outgoing = true;
@ -189,6 +190,12 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp
} else {
target.setCurrentTime();
}
QString oob = source.outOfBandUrl();
if (oob.size() > 0) {
target.setAttachPath(acc->network->addMessageAndCheckForPath(oob, acc->getName(), target.getPenPalJid(), messageId));
}
target.setOutOfBandUrl(oob);
}
void Core::MessageHandler::logMessage(const QXmppMessage& msg, const QString& reason)
@ -232,11 +239,23 @@ void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString&
}
}
void Core::MessageHandler::sendMessage(Shared::Message data)
void Core::MessageHandler::sendMessage(const Shared::Message& data)
{
if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) {
prepareUpload(data);
} else {
performSending(data);
}
}
void Core::MessageHandler::performSending(Shared::Message data, bool newMessage)
{
QString jid = data.getPenPalJid();
QString id = data.getId();
QString oob = data.getOutOfBandUrl();
RosterItem* ri = acc->rh->getRosterItem(jid);
bool sent = false;
QMap<QString, QVariant> changes;
if (acc->state == Shared::ConnectionState::connected) {
QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread());
@ -245,23 +264,16 @@ void Core::MessageHandler::sendMessage(Shared::Message data)
#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.setOutOfBandUrl(oob);
msg.setReceiptRequested(true);
bool sent = acc->client.sendPacket(msg);
sent = acc->client.sendPacket(msg);
if (sent) {
data.setState(Shared::Message::State::sent);
} else {
data.setState(Shared::Message::State::error);
data.setErrorText("Couldn't send message via QXMPP library check out logs");
}
if (ri != 0) {
ri->appendMessageToArchive(data);
if (sent) {
pendingStateMessages.insert(std::make_pair(id, jid));
}
data.setErrorText("Couldn't send message: internal QXMPP library error, probably need to check out the logs");
}
} else {
@ -269,41 +281,74 @@ void Core::MessageHandler::sendMessage(Shared::Message data)
data.setErrorText("You are is offline or reconnecting");
}
emit acc->changeMessage(jid, id, {
{"state", static_cast<uint>(data.getState())},
{"errorText", data.getErrorText()}
});
Shared::Message::State mstate = data.getState();
changes.insert("state", static_cast<uint>(mstate));
if (mstate == Shared::Message::State::error) {
changes.insert("errorText", data.getErrorText());
}
if (oob.size() > 0) {
changes.insert("outOfBandUrl", oob);
}
if (!newMessage) {
changes.insert("stamp", data.getTime());
}
if (ri != 0) {
if (newMessage) {
ri->appendMessageToArchive(data);
} else {
ri->changeMessage(id, changes);
}
if (sent) {
pendingStateMessages.insert(std::make_pair(id, jid));
} else {
pendingStateMessages.erase(id);
}
}
emit acc->changeMessage(jid, id, changes);
}
void Core::MessageHandler::sendMessage(const Shared::Message& data, const QString& path)
void Core::MessageHandler::prepareUpload(const Shared::Message& data)
{
if (acc->state == Shared::ConnectionState::connected) {
QString jid = data.getPenPalJid();
QString id = data.getId();
RosterItem* ri = acc->rh->getRosterItem(jid);
if (!ri) {
qDebug() << "An attempt to initialize upload in" << acc->name << "for pal" << jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send";
return;
}
QString path = data.getAttachPath();
QString url = acc->network->getFileRemoteUrl(path);
if (url.size() != 0) {
sendMessageWithLocalUploadedFile(data, url);
} else {
if (acc->network->isUploading(path, data.getId())) {
pendingMessages.emplace(data.getId(), data);
if (acc->network->checkAndAddToUploading(acc->getName(), jid, id, path)) {
ri->appendMessageToArchive(data);
pendingStateMessages.insert(std::make_pair(id, jid));
} else {
if (acc->um->serviceFound()) {
QFileInfo file(path);
if (file.exists() && file.isReadable()) {
uploadingSlotsQueue.emplace_back(path, data);
ri->appendMessageToArchive(data);
pendingStateMessages.insert(std::make_pair(id, jid));
uploadingSlotsQueue.emplace_back(path, id);
if (uploadingSlotsQueue.size() == 1) {
acc->um->requestUploadSlot(file);
}
} else {
onFileUploadError(data.getId(), "Uploading file no longer exists or your system user has no permission to read it");
handleUploadError(jid, id, "Uploading file no longer exists or your system user has no permission to read it");
qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but the file doesn't exist or is not readable";
}
} else {
onFileUploadError(data.getId(), "Your server doesn't support file upload service, or it's prohibited for your account");
handleUploadError(jid, id, "Your server doesn't support file upload service, or it's prohibited for your account");
qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but upload manager didn't discover any upload services";
}
}
}
} else {
onFileUploadError(data.getId(), "Account is offline or reconnecting");
handleUploadError(data.getPenPalJid(), data.getId(), "Account is offline or reconnecting");
qDebug() << "An attempt to send message with not connected account " << acc->name << ", skipping";
}
}
@ -314,12 +359,12 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo
if (uploadingSlotsQueue.size() == 0) {
qDebug() << "HTTP Upload manager of account" << acc->name << "reports about success requesting upload slot, but none was requested";
} else {
const std::pair<QString, Shared::Message>& pair = uploadingSlotsQueue.front();
const QString& mId = pair.second.getId();
acc->network->uploadFile(mId, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders());
pendingMessages.emplace(mId, pair.second);
uploadingSlotsQueue.pop_front();
const std::pair<QString, QString>& pair = uploadingSlotsQueue.front();
const QString& mId = pair.second;
QString palJid = pendingStateMessages.at(mId);
acc->network->uploadFile({acc->name, palJid, mId}, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders());
uploadingSlotsQueue.pop_front();
if (uploadingSlotsQueue.size() > 0) {
acc->um->requestUploadSlot(uploadingSlotsQueue.front().first);
}
@ -328,44 +373,111 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo
void Core::MessageHandler::onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request)
{
QString err(request.error().text());
if (uploadingSlotsQueue.size() == 0) {
qDebug() << "HTTP Upload manager of account" << acc->name << "reports about an error requesting upload slot, but none was requested";
qDebug() << request.error().text();
qDebug() << err;
} else {
const std::pair<QString, Shared::Message>& pair = uploadingSlotsQueue.front();
qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << request.error().text();
emit acc->uploadFileError(pair.second.getId(), "Error requesting slot to upload file: " + request.error().text());
const std::pair<QString, QString>& pair = uploadingSlotsQueue.front();
qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << err;
handleUploadError(pendingStateMessages.at(pair.second), pair.second, err);
uploadingSlotsQueue.pop_front();
if (uploadingSlotsQueue.size() > 0) {
acc->um->requestUploadSlot(uploadingSlotsQueue.front().first);
}
uploadingSlotsQueue.pop_front();
}
}
void Core::MessageHandler::onFileUploaded(const QString& messageId, const QString& url)
void Core::MessageHandler::onDownloadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path)
{
std::map<QString, Shared::Message>::const_iterator itr = pendingMessages.find(messageId);
if (itr != pendingMessages.end()) {
sendMessageWithLocalUploadedFile(itr->second, url);
pendingMessages.erase(itr);
QMap<QString, QVariant> cData = {
{"attachPath", path}
};
for (const Shared::MessageInfo& info : msgs) {
if (info.account == acc->getName()) {
RosterItem* cnt = acc->rh->getRosterItem(info.jid);
if (cnt != 0) {
if (cnt->changeMessage(info.messageId, cData)) {
emit acc->changeMessage(info.jid, info.messageId, cData);
}
}
}
}
}
void Core::MessageHandler::onFileUploadError(const QString& messageId, const QString& errMsg)
void Core::MessageHandler::onLoadFileError(const std::list<Shared::MessageInfo>& msgs, const QString& text, bool up)
{
std::map<QString, Shared::Message>::const_iterator itr = pendingMessages.find(messageId);
if (itr != pendingMessages.end()) {
pendingMessages.erase(itr);
if (up) {
for (const Shared::MessageInfo& info : msgs) {
if (info.account == acc->getName()) {
handleUploadError(info.jid, info.messageId, text);
}
}
}
}
void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url)
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);
pendingStateMessages.erase(jid);
requestChangeMessage(jid, messageId, {
{"state", static_cast<uint>(Shared::Message::State::error)},
{"errorText", errorText}
});
}
void Core::MessageHandler::onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path)
{
for (const Shared::MessageInfo& info : msgs) {
if (info.account == acc->getName()) {
RosterItem* ri = acc->rh->getRosterItem(info.jid);
if (ri != 0) {
Shared::Message msg = ri->getMessage(info.messageId);
sendMessageWithLocalUploadedFile(msg, path, false);
} 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";
}
}
}
}
void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage)
{
msg.setOutOfBandUrl(url);
if (msg.getBody().size() == 0) {
msg.setBody(url);
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
}
sendMessage(msg);
performSending(msg, newMessage);
//TODO removal/progress update
}
static const std::set<QString> allowerToChangeKeys({
"attachPath",
"outOfBandUrl",
"state",
"errorText"
});
void Core::MessageHandler::requestChangeMessage(const QString& jid, const QString& messageId, const QMap<QString, QVariant>& data)
{
RosterItem* cnt = acc->rh->getRosterItem(jid);
if (cnt != 0) {
bool allSupported = true;
QString unsupportedString;
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
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
break; //because the underlying tech assumes that the change is initiated by user
} //not by system
}
if (allSupported) {
cnt->changeMessage(messageId, data);
emit acc->changeMessage(jid, messageId, data);
} else {
qDebug() << "A request to change message" << messageId << "of conversation" << jid << "with following data" << data;
qDebug() << "only limited set of dataFields are supported yet here, and" << unsupportedString << "isn't one of them, skipping";
}
}
}

View File

@ -28,6 +28,7 @@
#include <QXmppHttpUploadIq.h>
#include <shared/message.h>
#include <shared/messageinfo.h>
namespace Core {
@ -44,8 +45,7 @@ public:
MessageHandler(Account* account);
public:
void sendMessage(Shared::Message data);
void sendMessage(const Shared::Message& data, const QString& path);
void sendMessage(const Shared::Message& data);
void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const;
public slots:
@ -55,20 +55,24 @@ public slots:
void onReceiptReceived(const QString& jid, const QString& id);
void onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot);
void onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request);
void onFileUploaded(const QString& messageId, const QString& url);
void onFileUploadError(const QString& messageId, const QString& errMsg);
void onDownloadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path);
void onUploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path);
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);
private:
bool handleChatMessage(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 sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url);
void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage = true);
void performSending(Shared::Message data, bool newMessage = true);
void prepareUpload(const Shared::Message& data);
void handleUploadError(const QString& jid, const QString& messageId, const QString& errorText);
private:
Account* acc;
std::map<QString, QString> pendingStateMessages;
std::map<QString, Shared::Message> pendingMessages;
std::deque<std::pair<QString, Shared::Message>> uploadingSlotsQueue;
std::map<QString, QString> pendingStateMessages; //key is message id, value is JID
std::deque<std::pair<QString, QString>> uploadingSlotsQueue;
};
}

View File

@ -190,7 +190,7 @@ void Core::RosterHandler::removeContactRequest(const QString& jid)
void Core::RosterHandler::handleNewRosterItem(Core::RosterItem* contact)
{
connect(contact, &RosterItem::needHistory, this->acc, &Account::onContactNeedHistory);
connect(contact, &RosterItem::historyResponse, this, &RosterHandler::onContactHistoryResponse);
connect(contact, &RosterItem::historyResponse, this->acc, &Account::onContactHistoryResponse);
connect(contact, &RosterItem::nameChanged, this, &RosterHandler::onContactNameChanged);
connect(contact, &RosterItem::avatarChanged, this, &RosterHandler::onContactAvatarChanged);
connect(contact, &RosterItem::requestVCard, this->acc, &Account::requestVCard);
@ -315,14 +315,6 @@ void Core::RosterHandler::removeFromGroup(const QString& jid, const QString& gro
}
}
void Core::RosterHandler::onContactHistoryResponse(const std::list<Shared::Message>& list)
{
RosterItem* contact = static_cast<RosterItem*>(sender());
qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements";
emit acc->responseArchive(contact->jid, list);
}
Core::RosterItem * Core::RosterHandler::getRosterItem(const QString& jid)
{
RosterItem* item = 0;

View File

@ -86,7 +86,6 @@ private slots:
void onContactGroupRemoved(const QString& group);
void onContactNameChanged(const QString& name);
void onContactSubscriptionStateChanged(Shared::SubscriptionState state);
void onContactHistoryResponse(const std::list<Shared::Message>& list);
void onContactAvatarChanged(Shared::Avatar, const QString& path);
private:

View File

@ -16,13 +16,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QtWidgets/QApplication>
#include <QtCore/QDir>
#include "networkaccess.h"
Core::NetworkAccess::NetworkAccess(QObject* parent):
QObject(parent),
running(false),
manager(0),
files("files"),
storage("fileURLStorage"),
downloads(),
uploads()
{
@ -33,60 +37,31 @@ Core::NetworkAccess::~NetworkAccess()
stop();
}
void Core::NetworkAccess::fileLocalPathRequest(const QString& messageId, const QString& url)
void Core::NetworkAccess::downladFile(const QString& url)
{
std::map<QString, Transfer*>::iterator itr = downloads.find(url);
if (itr != downloads.end()) {
Transfer* dwn = itr->second;
std::set<QString>::const_iterator mItr = dwn->messages.find(messageId);
if (mItr == dwn->messages.end()) {
dwn->messages.insert(messageId);
}
emit downloadFileProgress(messageId, dwn->progress);
qDebug() << "NetworkAccess received a request to download a file" << url << ", but the file is currently downloading, skipping";
} else {
try {
QString path = files.getRecord(url);
QFileInfo info(path);
std::pair<QString, std::list<Shared::MessageInfo>> p = storage.getPath(url);
if (p.first.size() > 0) {
QFileInfo info(p.first);
if (info.exists() && info.isFile()) {
emit fileLocalPathResponse(messageId, path);
emit downloadFileComplete(p.second, p.first);
} else {
files.removeRecord(url);
emit fileLocalPathResponse(messageId, "");
startDownload(p.second, url);
}
} else {
startDownload(p.second, url);
}
} catch (const Archive::NotFound& e) {
emit fileLocalPathResponse(messageId, "");
qDebug() << "NetworkAccess received a request to download a file" << url << ", but there is now record of which message uses that file, downloading anyway";
storage.addFile(url);
startDownload(std::list<Shared::MessageInfo>(), url);
} catch (const Archive::Unknown& e) {
qDebug() << "Error requesting file path:" << e.what();
emit fileLocalPathResponse(messageId, "");
}
}
}
void Core::NetworkAccess::downladFileRequest(const QString& messageId, const QString& url)
{
std::map<QString, Transfer*>::iterator itr = downloads.find(url);
if (itr != downloads.end()) {
Transfer* dwn = itr->second;
std::set<QString>::const_iterator mItr = dwn->messages.find(messageId);
if (mItr == dwn->messages.end()) {
dwn->messages.insert(messageId);
}
emit downloadFileProgress(messageId, dwn->progress);
} else {
try {
QString path = files.getRecord(url);
QFileInfo info(path);
if (info.exists() && info.isFile()) {
emit fileLocalPathResponse(messageId, path);
} else {
files.removeRecord(url);
startDownload(messageId, url);
}
} catch (const Archive::NotFound& e) {
startDownload(messageId, url);
} catch (const Archive::Unknown& e) {
qDebug() << "Error requesting file path:" << e.what();
emit downloadFileError(messageId, QString("Database error: ") + e.what());
emit loadFileError(std::list<Shared::MessageInfo>(), QString("Database error: ") + e.what(), false);
}
}
}
@ -95,7 +70,7 @@ void Core::NetworkAccess::start()
{
if (!running) {
manager = new QNetworkAccessManager();
files.open();
storage.open();
running = true;
}
}
@ -103,7 +78,7 @@ void Core::NetworkAccess::start()
void Core::NetworkAccess::stop()
{
if (running) {
files.close();
storage.close();
manager->deleteLater();
manager = 0;
running = false;
@ -128,9 +103,7 @@ void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesT
qreal total = bytesTotal;
qreal progress = received/total;
dwn->progress = progress;
for (std::set<QString>::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) {
emit downloadFileProgress(*mItr, progress);
}
emit loadFileProgress(dwn->messages, progress, false);
}
}
@ -146,9 +119,7 @@ void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code)
if (errorText.size() > 0) {
itr->second->success = false;
Transfer* dwn = itr->second;
for (std::set<QString>::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) {
emit downloadFileError(*mItr, errorText);
}
emit loadFileError(dwn->messages, errorText, false);
}
}
}
@ -276,50 +247,43 @@ QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code)
void Core::NetworkAccess::onDownloadFinished()
{
QString path("");
QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
QString url = rpl->url().toString();
std::map<QString, Transfer*>::const_iterator itr = downloads.find(url);
if (itr == downloads.end()) {
qDebug() << "an error downloading" << url << ": the request is done but seems like noone is waiting for it, skipping";
qDebug() << "an error downloading" << url << ": the request is done but there is no record of it being downloaded, ignoring";
} else {
Transfer* dwn = itr->second;
if (dwn->success) {
qDebug() << "download success for" << url;
QStringList hops = url.split("/");
QString fileName = hops.back();
QStringList parts = fileName.split(".");
path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/";
QString suffix("");
QStringList::const_iterator sItr = parts.begin();
QString realName = *sItr;
++sItr;
for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) {
suffix += "." + (*sItr);
}
QString postfix("");
QFileInfo proposedName(path + realName + postfix + suffix);
int counter = 0;
while (proposedName.exists()) {
postfix = QString("(") + std::to_string(++counter).c_str() + ")";
proposedName = QFileInfo(path + realName + postfix + suffix);
QString jid;
if (dwn->messages.size() > 0) {
jid = dwn->messages.front().jid;
}
QString path = prepareDirectory(jid);
if (path.size() > 0) {
path = checkFileName(fileName, path);
path = proposedName.absoluteFilePath();
QFile file(path);
if (file.open(QIODevice::WriteOnly)) {
file.write(dwn->reply->readAll());
file.close();
files.addRecord(url, path);
storage.setPath(url, path);
qDebug() << "file" << path << "was successfully downloaded";
} else {
qDebug() << "couldn't save file" << path;
path = "";
path = QString();
}
}
for (std::set<QString>::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) {
emit fileLocalPathResponse(*mItr, path);
if (path.size() > 0) {
emit downloadFileComplete(dwn->messages, path);
} else {
//TODO do I need to handle the failure here or it's already being handled in error?
//emit loadFileError(dwn->messages, path, false);
}
}
dwn->reply->deleteLater();
@ -328,9 +292,9 @@ void Core::NetworkAccess::onDownloadFinished()
}
}
void Core::NetworkAccess::startDownload(const QString& messageId, const QString& url)
void Core::NetworkAccess::startDownload(const std::list<Shared::MessageInfo>& msgs, const QString& url)
{
Transfer* dwn = new Transfer({{messageId}, 0, 0, true, "", url, 0});
Transfer* dwn = new Transfer({msgs, 0, 0, true, "", url, 0});
QNetworkRequest req(url);
dwn->reply = manager->get(req);
connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress);
@ -341,7 +305,7 @@ void Core::NetworkAccess::startDownload(const QString& messageId, const QString&
#endif
connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished);
downloads.insert(std::make_pair(url, dwn));
emit downloadFileProgress(messageId, 0);
emit loadFileProgress(dwn->messages, 0, false);
}
void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code)
@ -350,16 +314,16 @@ void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code)
QString url = rpl->url().toString();
std::map<QString, Transfer*>::const_iterator itr = uploads.find(url);
if (itr == uploads.end()) {
qDebug() << "an error uploading" << url << ": the request is reporting an error but seems like noone is waiting for it, skipping";
qDebug() << "an error uploading" << url << ": the request is reporting an error but there is no record of it being uploading, ignoring";
} else {
QString errorText = getErrorText(code);
if (errorText.size() > 0) {
itr->second->success = false;
Transfer* upl = itr->second;
for (std::set<QString>::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) {
emit uploadFileError(*mItr, errorText);
}
emit loadFileError(upl->messages, errorText, true);
}
//TODO deletion?
}
}
@ -369,17 +333,14 @@ void Core::NetworkAccess::onUploadFinished()
QString url = rpl->url().toString();
std::map<QString, Transfer*>::const_iterator itr = uploads.find(url);
if (itr == downloads.end()) {
qDebug() << "an error uploading" << url << ": the request is done but seems like no one is waiting for it, skipping";
qDebug() << "an error uploading" << url << ": the request is done there is no record of it being uploading, ignoring";
} else {
Transfer* upl = itr->second;
if (upl->success) {
qDebug() << "upload success for" << url;
files.addRecord(upl->url, upl->path);
for (std::set<QString>::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) {
emit fileLocalPathResponse(*mItr, upl->path);
emit uploadFileComplete(*mItr, upl->url);
}
storage.addFile(upl->messages, upl->url, upl->path);
emit uploadFileComplete(upl->messages, upl->url);
}
upl->reply->deleteLater();
@ -403,94 +364,29 @@ void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTot
qreal total = bytesTotal;
qreal progress = received/total;
upl->progress = progress;
for (std::set<QString>::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) {
emit uploadFileProgress(*mItr, progress);
}
}
}
void Core::NetworkAccess::startUpload(const QString& messageId, const QString& url, const QString& path)
{
Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, url, 0});
QNetworkRequest req(url);
QFile* file = new QFile(path);
if (file->open(QIODevice::ReadOnly)) {
upl->reply = manager->put(req, file);
connect(upl->reply, &QNetworkReply::uploadProgress, this, &NetworkAccess::onUploadProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
connect(upl->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred), this, &NetworkAccess::onUploadError);
#else
connect(upl->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::error), this, &NetworkAccess::onUploadError);
#endif
connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished);
uploads.insert(std::make_pair(url, upl));
emit downloadFileProgress(messageId, 0);
} else {
qDebug() << "couldn't upload file" << path;
emit uploadFileError(messageId, "Error opening file");
delete file;
}
}
void Core::NetworkAccess::uploadFileRequest(const QString& messageId, const QString& url, const QString& path)
{
std::map<QString, Transfer*>::iterator itr = uploads.find(url);
if (itr != uploads.end()) {
Transfer* upl = itr->second;
std::set<QString>::const_iterator mItr = upl->messages.find(messageId);
if (mItr == upl->messages.end()) {
upl->messages.insert(messageId);
}
emit uploadFileProgress(messageId, upl->progress);
} else {
try {
QString ePath = files.getRecord(url);
if (ePath == path) {
QFileInfo info(path);
if (info.exists() && info.isFile()) {
emit fileLocalPathResponse(messageId, path);
} else {
files.removeRecord(url);
startUpload(messageId, url, path);
}
} else {
QFileInfo info(path);
if (info.exists() && info.isFile()) {
files.changeRecord(url, path);
emit fileLocalPathResponse(messageId, path);
} else {
files.removeRecord(url);
startUpload(messageId, url, path);
}
}
} catch (const Archive::NotFound& e) {
startUpload(messageId, url, path);
} catch (const Archive::Unknown& e) {
qDebug() << "Error requesting file path on upload:" << e.what();
emit uploadFileError(messageId, QString("Database error: ") + e.what());
}
emit loadFileProgress(upl->messages, progress, true);
}
}
QString Core::NetworkAccess::getFileRemoteUrl(const QString& path)
{
return ""; //TODO this is a way not to upload some file more then 1 time, here I'm supposed to return that file GET url
QString p;
try {
p = storage.getUrl(path);
} catch (const Archive::NotFound& err) {
} catch (...) {
throw;
}
return p;
}
bool Core::NetworkAccess::isUploading(const QString& path, const QString& messageId)
{
return false; //TODO this is a way to avoid parallel uploading of the same files by different chats
// message is is supposed to be added to the uploading messageids list
// the result should be true if there was an uploading file with this path
// message id can be empty, then it's just to check and not to add
}
void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap<QString, QString> headers)
void Core::NetworkAccess::uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap<QString, QString> headers)
{
QFile* file = new QFile(path);
Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, get.toString(), file});
Transfer* upl = new Transfer({{info}, 0, 0, true, path, get.toString(), file});
QNetworkRequest req(put);
for (QMap<QString, QString>::const_iterator itr = headers.begin(), end = headers.end(); itr != end; itr++) {
req.setRawHeader(itr.key().toUtf8(), itr.value().toUtf8());
@ -506,10 +402,99 @@ void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& pa
#endif
connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished);
uploads.insert(std::make_pair(put.toString(), upl));
emit downloadFileProgress(messageId, 0);
emit loadFileProgress(upl->messages, 0, true);
} else {
qDebug() << "couldn't upload file" << path;
emit uploadFileError(messageId, "Error opening file");
emit loadFileError(upl->messages, "Error opening file", true);
delete file;
delete upl;
}
}
void Core::NetworkAccess::registerFile(const QString& url, const QString& account, const QString& jid, const QString& id)
{
storage.addFile(url, account, jid, id);
std::map<QString, Transfer*>::iterator itr = downloads.find(url);
if (itr != downloads.end()) {
itr->second->messages.emplace_back(account, jid, id); //TODO notification is going to happen the next tick, is that okay?
}
}
void Core::NetworkAccess::registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id)
{
storage.addFile(url, path, account, jid, id);
}
bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path)
{
for (const std::pair<const QString, Transfer*>& pair : uploads) {
Transfer* info = pair.second;
if (pair.second->path == path) {
std::list<Shared::MessageInfo>& messages = info->messages;
bool dup = false;
for (const Shared::MessageInfo& info : messages) {
if (info.account == acc && info.jid == jid && info.messageId == id) {
dup = true;
break;
}
}
if (!dup) {
info->messages.emplace_back(acc, jid, id); //TODO notification is going to happen the next tick, is that okay?
return true;
}
}
}
return false;
}
QString Core::NetworkAccess::prepareDirectory(const QString& jid)
{
QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
path += "/" + QApplication::applicationName();
if (jid.size() > 0) {
path += "/" + jid;
}
QDir location(path);
if (!location.exists()) {
bool res = location.mkpath(path);
if (!res) {
return "";
} else {
return path;
}
}
return path;
}
QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path)
{
QStringList parts = name.split(".");
QString suffix("");
QStringList::const_iterator sItr = parts.begin();
QString realName = *sItr;
++sItr;
for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) {
suffix += "." + (*sItr);
}
QString postfix("");
QFileInfo proposedName(path + "/" + realName + suffix);
int counter = 0;
while (proposedName.exists()) {
QString count = QString("(") + std::to_string(++counter).c_str() + ")";
proposedName = QFileInfo(path + "/" + realName + count + suffix);
}
return proposedName.absoluteFilePath();
}
QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id)
{
return storage.addMessageAndCheckForPath(url, account, jid, id);
}
std::list<Shared::MessageInfo> Core::NetworkAccess::reportPathInvalid(const QString& path)
{
return storage.deletedFile(path);
}

View File

@ -29,13 +29,15 @@
#include <set>
#include "storage.h"
#include "urlstorage.h"
namespace Core {
/**
* @todo write docs
*/
//TODO Need to describe how to get rid of records when file is no longer reachable;
class NetworkAccess : public QObject
{
Q_OBJECT
@ -48,26 +50,27 @@ public:
void stop();
QString getFileRemoteUrl(const QString& path);
bool isUploading(const QString& path, const QString& messageId = "");
void uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap<QString, QString> headers);
QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id);
void uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap<QString, QString> headers);
bool checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path);
std::list<Shared::MessageInfo> reportPathInvalid(const QString& path);
signals:
void fileLocalPathResponse(const QString& messageId, const QString& path);
void downloadFileProgress(const QString& messageId, qreal value);
void downloadFileError(const QString& messageId, const QString& path);
void uploadFileProgress(const QString& messageId, qreal value);
void uploadFileError(const QString& messageId, const QString& path);
void uploadFileComplete(const QString& messageId, const QString& url);
void loadFileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up);
void loadFileError(const std::list<Shared::MessageInfo>& msgs, const QString& text, bool up);
void uploadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& url);
void downloadFileComplete(const std::list<Shared::MessageInfo>& msgs, const QString& path);
public slots:
void fileLocalPathRequest(const QString& messageId, const QString& url);
void downladFileRequest(const QString& messageId, const QString& url);
void uploadFileRequest(const QString& messageId, const QString& url, const QString& path);
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& path, const QString& account, const QString& jid, const QString& id);
private:
void startDownload(const QString& messageId, const QString& url);
void startUpload(const QString& messageId, const QString& url, const QString& path);
void startDownload(const std::list<Shared::MessageInfo>& msgs, const QString& url);
QString getErrorText(QNetworkReply::NetworkError code);
QString prepareDirectory(const QString& jid);
QString checkFileName(const QString& name, const QString& path);
private slots:
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
@ -80,12 +83,12 @@ private slots:
private:
bool running;
QNetworkAccessManager* manager;
Storage files;
UrlStorage storage;
std::map<QString, Transfer*> downloads;
std::map<QString, Transfer*> uploads;
struct Transfer {
std::set<QString> messages;
std::list<Shared::MessageInfo> messages;
qreal progress;
QNetworkReply* reply;
bool success;

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.3)
project(pse)
if (WITH_KWALLET)

View File

@ -122,7 +122,22 @@ void Core::RosterItem::nextRequest()
{
if (syncronizing) {
if (requestedCount != -1) {
emit historyResponse(responseCache);
bool last = false;
if (archiveState == beginning || archiveState == complete) {
QString firstId = archive->oldestId();
if (responseCache.size() == 0) {
if (requestedBefore == firstId) {
last = true;
}
} else {
if (responseCache.front().getId() == firstId) {
last = true;
}
}
} else if (archiveState == empty && responseCache.size() == 0) {
last = true;
}
emit historyResponse(responseCache, last);
}
}
if (requestCache.size() > 0) {
@ -360,6 +375,11 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs
archiveState = complete;
archive->setFromTheBeginning(true);
}
if (added == 0 && wasEmpty) {
archiveState = empty;
nextRequest();
break;
}
if (requestedCount != -1) {
QString before;
if (responseCache.size() > 0) {
@ -378,7 +398,7 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs
} catch (const Archive::Empty& e) {
}
if (!found || requestedCount > responseCache.size()) {
if (!found || requestedCount > int(responseCache.size())) {
if (archiveState == complete) {
nextRequest();
} else {
@ -529,7 +549,7 @@ void Core::RosterItem::clearArchiveRequests()
requestedBefore = "";
for (const std::pair<int, QString>& pair : requestCache) {
if (pair.first != -1) {
emit historyResponse(responseCache); //just to notify those who still waits with whatever happened to be left in caches yet
emit historyResponse(responseCache, false); //just to notify those who still waits with whatever happened to be left in caches yet
}
responseCache.clear();
}
@ -549,3 +569,20 @@ void Core::RosterItem::downgradeDatabaseState()
archiveState = ArchiveState::chunk;
}
}
Shared::Message Core::RosterItem::getMessage(const QString& id)
{
for (const Shared::Message& msg : appendCache) {
if (msg.getId() == id) {
return msg;
}
}
for (Shared::Message& msg : hisoryCache) {
if (msg.getId() == id) {
return msg;
}
}
return archive->getElement(id);
}

View File

@ -78,10 +78,12 @@ public:
void clearArchiveRequests();
void downgradeDatabaseState();
Shared::Message getMessage(const QString& id);
signals:
void nameChanged(const QString& name);
void subscriptionStateChanged(Shared::SubscriptionState state);
void historyResponse(const std::list<Shared::Message>& messages);
void historyResponse(const std::list<Shared::Message>& messages, bool last);
void needHistory(const QString& before, const QString& after, const QDateTime& afterTime = QDateTime());
void avatarChanged(Shared::Avatar, const QString& path);
void requestVCard(const QString& jid);

View File

@ -32,11 +32,10 @@ Core::Squawk::Squawk(QObject* parent):
,kwallet()
#endif
{
connect(&network, &NetworkAccess::fileLocalPathResponse, this, &Squawk::fileLocalPathResponse);
connect(&network, &NetworkAccess::downloadFileProgress, this, &Squawk::downloadFileProgress);
connect(&network, &NetworkAccess::downloadFileError, this, &Squawk::downloadFileError);
connect(&network, &NetworkAccess::uploadFileProgress, this, &Squawk::uploadFileProgress);
connect(&network, &NetworkAccess::uploadFileError, this, &Squawk::uploadFileError);
connect(&network, &NetworkAccess::loadFileProgress, this, &Squawk::fileProgress);
connect(&network, &NetworkAccess::loadFileError, this, &Squawk::fileError);
connect(&network, &NetworkAccess::downloadFileComplete, this, &Squawk::fileDownloadComplete);
connect(&network, &NetworkAccess::uploadFileComplete, this, &Squawk::fileUploadComplete);
#ifdef WITH_KWALLET
if (kwallet.supportState() == PSE::KWallet::success) {
@ -168,7 +167,7 @@ void Core::Squawk::addAccount(
connect(acc, &Account::receivedVCard, this, &Squawk::responseVCard);
connect(acc, &Account::uploadFileError, this, &Squawk::uploadFileError);
connect(acc, &Account::uploadFileError, this, &Squawk::onAccountUploadFileError);
QMap<QString, QVariant> map = {
{"login", login},
@ -336,17 +335,6 @@ void Core::Squawk::sendMessage(const QString& account, const Shared::Message& da
itr->second->sendMessage(data);
}
void Core::Squawk::sendMessage(const QString& account, const Shared::Message& data, const QString& path)
{
AccountsMap::const_iterator itr = amap.find(account);
if (itr == amap.end()) {
qDebug("An attempt to send a message with non existing account, skipping");
return;
}
itr->second->sendMessage(data, path);
}
void Core::Squawk::requestArchive(const QString& account, const QString& jid, int count, const QString& before)
{
AccountsMap::const_iterator itr = amap.find(account);
@ -357,10 +345,10 @@ void Core::Squawk::requestArchive(const QString& account, const QString& jid, in
itr->second->requestArchive(jid, count, before);
}
void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list<Shared::Message>& list)
void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list<Shared::Message>& list, bool last)
{
Account* acc = static_cast<Account*>(sender());
emit responseArchive(acc->getName(), jid, list);
emit responseArchive(acc->getName(), jid, list, last);
}
void Core::Squawk::modifyAccountRequest(const QString& name, const QMap<QString, QVariant>& map)
@ -604,14 +592,9 @@ void Core::Squawk::addRoomRequest(const QString& account, const QString& jid, co
itr->second->addRoomRequest(jid, nick, password, autoJoin);
}
void Core::Squawk::fileLocalPathRequest(const QString& messageId, const QString& url)
void Core::Squawk::fileDownloadRequest(const QString& url)
{
network.fileLocalPathRequest(messageId, url);
}
void Core::Squawk::downloadFileRequest(const QString& messageId, const QString& url)
{
network.downladFileRequest(messageId, url);
network.downladFile(url);
}
void Core::Squawk::addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName)
@ -762,3 +745,26 @@ void Core::Squawk::onWalletResponsePassword(const QString& login, const QString&
emit changeAccount(login, {{"password", password}});
accountReady();
}
void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText)
{
Account* acc = static_cast<Account*>(sender());
emit fileError({{acc->getName(), jid, id}}, errorText, true);
}
void Core::Squawk::onLocalPathInvalid(const QString& path)
{
std::list<Shared::MessageInfo> list = network.reportPathInvalid(path);
QMap<QString, QVariant> data({
{"attachPath", ""}
});
for (const Shared::MessageInfo& info : list) {
AccountsMap::const_iterator itr = amap.find(info.account);
if (itr != amap.end()) {
itr->second->requestChangeMessage(info.jid, info.messageId, data);
} else {
qDebug() << "Reacting on failure to reach file" << path << "there was an attempt to change message in account" << info.account << "which doesn't exist, skipping";
}
}
}

View File

@ -51,31 +51,39 @@ public:
signals:
void quit();
void ready();
void newAccount(const QMap<QString, QVariant>&);
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);
void removeContact(const QString& account, const QString& jid, const QString& group);
void changeContact(const QString& account, const QString& jid, const QMap<QString, QVariant>& data);
void addPresence(const QString& account, const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void removePresence(const QString& account, const QString& jid, const QString& name);
void stateChanged(Shared::Availability state);
void accountMessage(const QString& account, const Shared::Message& data);
void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list);
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 fileLocalPathResponse(const QString& messageId, const QString& path);
void downloadFileError(const QString& messageId, const QString& error);
void downloadFileProgress(const QString& messageId, qreal value);
void uploadFileError(const QString& messageId, const QString& error);
void uploadFileProgress(const QString& messageId, qreal value);
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& path);
void responseVCard(const QString& jid, const Shared::VCard& card);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void requestPassword(const QString& account);
@ -83,15 +91,18 @@ signals:
public slots:
void start();
void stop();
void newAccountRequest(const QMap<QString, QVariant>& map);
void modifyAccountRequest(const QString& name, const QMap<QString, QVariant>& map);
void removeAccountRequest(const QString& name);
void connectAccount(const QString& account);
void disconnectAccount(const QString& account);
void changeState(Shared::Availability state);
void sendMessage(const QString& account, const Shared::Message& data);
void sendMessage(const QString& account, const Shared::Message& data, const QString& path);
void requestArchive(const QString& account, const QString& jid, int count, const QString& before);
void subscribeContact(const QString& account, const QString& jid, const QString& reason);
void unsubscribeContact(const QString& account, const QString& jid, const QString& reason);
void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName);
@ -99,15 +110,18 @@ public slots:
void removeContactRequest(const QString& account, const QString& jid);
void renameContactRequest(const QString& account, const QString& jid, const QString& newName);
void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet<QString>& groups);
void setRoomJoined(const QString& account, const QString& jid, bool joined);
void setRoomAutoJoin(const QString& account, const QString& jid, bool joined);
void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin);
void removeRoomRequest(const QString& account, const QString& jid);
void fileLocalPathRequest(const QString& messageId, const QString& url);
void downloadFileRequest(const QString& messageId, const QString& url);
void fileDownloadRequest(const QString& url);
void requestVCard(const QString& account, const QString& jid);
void uploadVCard(const QString& account, const Shared::VCard& card);
void responsePassword(const QString& account, const QString& password);
void onLocalPathInvalid(const QString& path);
private:
typedef std::deque<Account*> Accounts;
@ -146,7 +160,7 @@ private slots:
void onAccountAddPresence(const QString& jid, const QString& name, const QMap<QString, QVariant>& data);
void onAccountRemovePresence(const QString& jid, const QString& name);
void onAccountMessage(const Shared::Message& data);
void onAccountResponseArchive(const QString& jid, const std::list<Shared::Message>& list);
void onAccountResponseArchive(const QString& jid, const std::list<Shared::Message>& list, bool last);
void onAccountAddRoom(const QString jid, const QMap<QString, QVariant>& data);
void onAccountChangeRoom(const QString jid, const QMap<QString, QVariant>& data);
void onAccountRemoveRoom(const QString jid);
@ -155,6 +169,8 @@ private slots:
void onAccountRemoveRoomPresence(const QString& jid, const QString& nick);
void onAccountChangeMessage(const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText);
void onWalletOpened(bool success);
void onWalletResponsePassword(const QString& login, const QString& password);
void onWalletRejectPassword(const QString& login);

491
core/urlstorage.cpp Normal file
View File

@ -0,0 +1,491 @@
/*
* 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 <QStandardPaths>
#include <QDir>
#include <QDebug>
#include "urlstorage.h"
Core::UrlStorage::UrlStorage(const QString& p_name):
name(p_name),
opened(false),
environment(),
base(),
map()
{
}
Core::UrlStorage::~UrlStorage()
{
close();
}
void Core::UrlStorage::open()
{
if (!opened) {
mdb_env_create(&environment);
QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
path += "/" + name;
QDir cache(path);
if (!cache.exists()) {
bool res = cache.mkpath(path);
if (!res) {
throw Archive::Directory(path.toStdString());
}
}
mdb_env_set_maxdbs(environment, 2);
mdb_env_set_mapsize(environment, 10UL * 1024UL * 1024UL);
mdb_env_open(environment, path.toStdString().c_str(), 0, 0664);
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
mdb_dbi_open(txn, "base", MDB_CREATE, &base);
mdb_dbi_open(txn, "map", MDB_CREATE, &map);
mdb_txn_commit(txn);
opened = true;
}
}
void Core::UrlStorage::close()
{
if (opened) {
mdb_dbi_close(environment, map);
mdb_dbi_close(environment, base);
mdb_env_close(environment);
opened = false;
}
}
void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, bool overwrite)
{
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
try {
writeInfo(key, info, txn, overwrite);
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
}
void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, MDB_txn* txn, bool overwrite)
{
QByteArray ba;
QDataStream ds(&ba, QIODevice::WriteOnly);
info.serialize(ds);
const std::string& id = key.toStdString();
MDB_val lmdbKey, lmdbData;
lmdbKey.mv_size = id.size();
lmdbKey.mv_data = (char*)id.c_str();
lmdbData.mv_size = ba.size();
lmdbData.mv_data = (uint8_t*)ba.data();
int rc;
rc = mdb_put(txn, base, &lmdbKey, &lmdbData, overwrite ? 0 : MDB_NOOVERWRITE);
if (rc != 0) {
if (rc == MDB_KEYEXIST) {
if (!overwrite) {
throw Archive::Exist(name.toStdString(), id);
}
} else {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
}
if (info.hasPath()) {
std::string sp = info.getPath().toStdString();
lmdbData.mv_size = sp.size();
lmdbData.mv_data = (char*)sp.c_str();
rc = mdb_put(txn, map, &lmdbData, &lmdbKey, 0);
if (rc != 0) {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
}
}
void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info, MDB_txn* txn)
{
const std::string& id = key.toStdString();
MDB_val lmdbKey, lmdbData;
lmdbKey.mv_size = id.size();
lmdbKey.mv_data = (char*)id.c_str();
int rc = mdb_get(txn, base, &lmdbKey, &lmdbData);
if (rc == 0) {
QByteArray ba((char*)lmdbData.mv_data, lmdbData.mv_size);
QDataStream ds(&ba, QIODevice::ReadOnly);
info.deserialize(ds);
} else if (rc == MDB_NOTFOUND) {
throw Archive::NotFound(id, name.toStdString());
} else {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
}
void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info)
{
MDB_txn *txn;
mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn);
try {
readInfo(key, info, txn);
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
}
void Core::UrlStorage::addFile(const QString& url)
{
if (!opened) {
throw Archive::Closed("addFile(no message, no path)", name.toStdString());
}
addToInfo(url, "", "", "");
}
void Core::UrlStorage::addFile(const QString& url, const QString& path)
{
if (!opened) {
throw Archive::Closed("addFile(no message, with path)", name.toStdString());
}
addToInfo(url, "", "", "", path);
}
void Core::UrlStorage::addFile(const QString& url, const QString& account, const QString& jid, const QString& id)
{
if (!opened) {
throw Archive::Closed("addFile(with message, no path)", name.toStdString());
}
addToInfo(url, account, jid, id);
}
void Core::UrlStorage::addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id)
{
if (!opened) {
throw Archive::Closed("addFile(with message, with path)", name.toStdString());
}
addToInfo(url, account, jid, id, path);
}
void Core::UrlStorage::addFile(const std::list<Shared::MessageInfo>& msgs, const QString& url, const QString& path)
{
if (!opened) {
throw Archive::Closed("addFile(with list)", name.toStdString());
}
UrlInfo info (path, msgs);
writeInfo(url, info, true);;
}
QString Core::UrlStorage::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id)
{
if (!opened) {
throw Archive::Closed("addMessageAndCheckForPath", name.toStdString());
}
return addToInfo(url, account, jid, id).getPath();
}
Core::UrlStorage::UrlInfo Core::UrlStorage::addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path)
{
UrlInfo info;
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
try {
readInfo(url, info, txn);
} catch (const Archive::NotFound& e) {
} catch (...) {
mdb_txn_abort(txn);
throw;
}
bool pathChange = false;
bool listChange = false;
if (path != "-s") {
if (info.getPath() != path) {
info.setPath(path);
pathChange = true;
}
}
if (account.size() > 0 && jid.size() > 0 && id.size() > 0) {
listChange = info.addMessage(account, jid, id);
}
if (pathChange || listChange) {
try {
writeInfo(url, info, txn, true);
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
} else {
mdb_txn_abort(txn);
}
return info;
}
std::list<Shared::MessageInfo> Core::UrlStorage::setPath(const QString& url, const QString& path)
{
std::list<Shared::MessageInfo> list;
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
UrlInfo info;
try {
readInfo(url, info, txn);
info.getMessages(list);
} catch (const Archive::NotFound& e) {
} catch (...) {
mdb_txn_abort(txn);
throw;
}
info.setPath(path);
try {
writeInfo(url, info, txn, true);
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
return list;
}
std::list<Shared::MessageInfo> Core::UrlStorage::removeFile(const QString& url)
{
std::list<Shared::MessageInfo> list;
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
UrlInfo info;
try {
std::string id = url.toStdString();
readInfo(url, info, txn);
info.getMessages(list);
MDB_val lmdbKey;
lmdbKey.mv_size = id.size();
lmdbKey.mv_data = (char*)id.c_str();
int rc = mdb_del(txn, base, &lmdbKey, NULL);
if (rc != 0) {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
if (info.hasPath()) {
std::string path = info.getPath().toStdString();
lmdbKey.mv_size = path.size();
lmdbKey.mv_data = (char*)path.c_str();
int rc = mdb_del(txn, map, &lmdbKey, NULL);
if (rc != 0) {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
}
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
return list;
}
std::list<Shared::MessageInfo> Core::UrlStorage::deletedFile(const QString& path)
{
std::list<Shared::MessageInfo> list;
MDB_txn *txn;
mdb_txn_begin(environment, NULL, 0, &txn);
try {
std::string spath = path.toStdString();
MDB_val lmdbKey, lmdbData;
lmdbKey.mv_size = spath.size();
lmdbKey.mv_data = (char*)spath.c_str();
QString url;
int rc = mdb_get(txn, map, &lmdbKey, &lmdbData);
if (rc == 0) {
std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size);
url = QString(surl.c_str());
} else if (rc == MDB_NOTFOUND) {
qDebug() << "Have been asked to remove file" << path << ", which isn't in the database, skipping";
mdb_txn_abort(txn);
return list;
} else {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
UrlInfo info;
std::string id = url.toStdString();
readInfo(url, info, txn);
info.getMessages(list);
info.setPath(QString());
writeInfo(url, info, txn, true);
rc = mdb_del(txn, map, &lmdbKey, NULL);
if (rc != 0) {
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
mdb_txn_commit(txn);
} catch (...) {
mdb_txn_abort(txn);
throw;
}
return list;
}
QString Core::UrlStorage::getUrl(const QString& path)
{
std::list<Shared::MessageInfo> list;
MDB_txn *txn;
mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn);
std::string spath = path.toStdString();
MDB_val lmdbKey, lmdbData;
lmdbKey.mv_size = spath.size();
lmdbKey.mv_data = (char*)spath.c_str();
QString url;
int rc = mdb_get(txn, map, &lmdbKey, &lmdbData);
if (rc == 0) {
std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size);
url = QString(surl.c_str());
mdb_txn_abort(txn);
return url;
} else if (rc == MDB_NOTFOUND) {
mdb_txn_abort(txn);
throw Archive::NotFound(spath, name.toStdString());
} else {
mdb_txn_abort(txn);
throw Archive::Unknown(name.toStdString(), mdb_strerror(rc));
}
}
std::pair<QString, std::list<Shared::MessageInfo>> Core::UrlStorage::getPath(const QString& url)
{
UrlInfo info;
readInfo(url, info);
std::list<Shared::MessageInfo> container;
info.getMessages(container);
return std::make_pair(info.getPath(), container);
}
Core::UrlStorage::UrlInfo::UrlInfo():
localPath(),
messages() {}
Core::UrlStorage::UrlInfo::UrlInfo(const QString& path):
localPath(path),
messages() {}
Core::UrlStorage::UrlInfo::UrlInfo(const QString& path, const std::list<Shared::MessageInfo>& msgs):
localPath(path),
messages(msgs) {}
Core::UrlStorage::UrlInfo::~UrlInfo() {}
bool Core::UrlStorage::UrlInfo::addMessage(const QString& acc, const QString& jid, const QString& id)
{
for (const Shared::MessageInfo& info : messages) {
if (info.account == acc && info.jid == jid && info.messageId == id) {
return false;
}
}
messages.emplace_back(acc, jid, id);
return true;
}
void Core::UrlStorage::UrlInfo::serialize(QDataStream& data) const
{
data << localPath;
std::list<Shared::MessageInfo>::size_type size = messages.size();
data << quint32(size);
for (const Shared::MessageInfo& info : messages) {
data << info.account;
data << info.jid;
data << info.messageId;
}
}
void Core::UrlStorage::UrlInfo::deserialize(QDataStream& data)
{
data >> localPath;
quint32 size;
data >> size;
for (quint32 i = 0; i < size; ++i) {
messages.emplace_back();
Shared::MessageInfo& info = messages.back();
data >> info.account;
data >> info.jid;
data >> info.messageId;
}
}
void Core::UrlStorage::UrlInfo::getMessages(std::list<Shared::MessageInfo>& container) const
{
for (const Shared::MessageInfo& info : messages) {
container.emplace_back(info);
}
}
QString Core::UrlStorage::UrlInfo::getPath() const
{
return localPath;
}
bool Core::UrlStorage::UrlInfo::hasPath() const
{
return localPath.size() > 0;
}
void Core::UrlStorage::UrlInfo::setPath(const QString& path)
{
localPath = path;
}

99
core/urlstorage.h Normal file
View File

@ -0,0 +1,99 @@
/*
* 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_URLSTORAGE_H
#define CORE_URLSTORAGE_H
#include <QString>
#include <QDataStream>
#include <lmdb.h>
#include <list>
#include "archive.h"
#include <shared/messageinfo.h>
namespace Core {
/**
* @todo write docs
*/
class UrlStorage
{
class UrlInfo;
public:
UrlStorage(const QString& name);
~UrlStorage();
void open();
void close();
void addFile(const QString& url);
void addFile(const QString& url, const QString& path);
void addFile(const QString& url, const QString& account, const QString& jid, const QString& id);
void addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id);
void addFile(const std::list<Shared::MessageInfo>& msgs, const QString& url, const QString& path); //this one overwrites all that was
std::list<Shared::MessageInfo> removeFile(const QString& url); //removes entry like it never was in the database, returns affected message infos
std::list<Shared::MessageInfo> deletedFile(const QString& path); //empties the localPath of the entry, returns affected message infos
std::list<Shared::MessageInfo> setPath(const QString& url, const QString& path);
QString getUrl(const QString& path);
QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id);
std::pair<QString, std::list<Shared::MessageInfo>> getPath(const QString& url);
private:
QString name;
bool opened;
MDB_env* environment;
MDB_dbi base;
MDB_dbi map;
private:
void writeInfo(const QString& key, const UrlInfo& info, bool overwrite = false);
void writeInfo(const QString& key, const UrlInfo& info, MDB_txn* txn, bool overwrite = false);
void readInfo(const QString& key, UrlInfo& info);
void readInfo(const QString& key, UrlInfo& info, MDB_txn* txn);
UrlInfo addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path = "-s");
private:
class UrlInfo {
public:
UrlInfo(const QString& path);
UrlInfo(const QString& path, const std::list<Shared::MessageInfo>& msgs);
UrlInfo();
~UrlInfo();
void serialize(QDataStream& data) const;
void deserialize(QDataStream& data);
QString getPath() const;
bool hasPath() const;
void setPath(const QString& path);
bool addMessage(const QString& acc, const QString& jid, const QString& id);
void getMessages(std::list<Shared::MessageInfo>& container) const;
private:
QString localPath;
std::list<Shared::MessageInfo> messages;
};
};
}
#endif // CORE_URLSTORAGE_H

View File

@ -20,6 +20,7 @@
#include "core/squawk.h"
#include "signalcatcher.h"
#include "shared/global.h"
#include "shared/messageinfo.h"
#include <QtWidgets/QApplication>
#include <QtCore/QThread>
#include <QtCore/QObject>
@ -31,8 +32,10 @@
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");
@ -96,10 +99,7 @@ int main(int argc, char *argv[])
QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount);
QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount);
QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState);
QObject::connect(&w, qOverload<const QString&, const Shared::Message&>(&Squawk::sendMessage),
squawk, qOverload<const QString&, const Shared::Message&>(&Core::Squawk::sendMessage));
QObject::connect(&w, qOverload<const QString&, const Shared::Message&, const QString&>(&Squawk::sendMessage),
squawk, qOverload<const QString&, const Shared::Message&, const QString&>(&Core::Squawk::sendMessage));
QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage);
QObject::connect(&w, &Squawk::requestArchive, squawk, &Core::Squawk::requestArchive);
QObject::connect(&w, &Squawk::subscribeContact, squawk, &Core::Squawk::subscribeContact);
QObject::connect(&w, &Squawk::unsubscribeContact, squawk, &Core::Squawk::unsubscribeContact);
@ -109,14 +109,14 @@ int main(int argc, char *argv[])
QObject::connect(&w, &Squawk::setRoomAutoJoin, squawk, &Core::Squawk::setRoomAutoJoin);
QObject::connect(&w, &Squawk::removeRoomRequest, squawk, &Core::Squawk::removeRoomRequest);
QObject::connect(&w, &Squawk::addRoomRequest, squawk, &Core::Squawk::addRoomRequest);
QObject::connect(&w, &Squawk::fileLocalPathRequest, squawk, &Core::Squawk::fileLocalPathRequest);
QObject::connect(&w, &Squawk::downloadFileRequest, squawk, &Core::Squawk::downloadFileRequest);
QObject::connect(&w, &Squawk::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);
@ -141,11 +141,10 @@ int main(int argc, char *argv[])
QObject::connect(squawk, &Core::Squawk::addRoomParticipant, &w, &Squawk::addRoomParticipant);
QObject::connect(squawk, &Core::Squawk::changeRoomParticipant, &w, &Squawk::changeRoomParticipant);
QObject::connect(squawk, &Core::Squawk::removeRoomParticipant, &w, &Squawk::removeRoomParticipant);
QObject::connect(squawk, &Core::Squawk::fileLocalPathResponse, &w, &Squawk::fileLocalPathResponse);
QObject::connect(squawk, &Core::Squawk::downloadFileProgress, &w, &Squawk::fileProgress);
QObject::connect(squawk, &Core::Squawk::downloadFileError, &w, &Squawk::fileError);
QObject::connect(squawk, &Core::Squawk::uploadFileProgress, &w, &Squawk::fileProgress);
QObject::connect(squawk, &Core::Squawk::uploadFileError, &w, &Squawk::fileError);
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);

26
plugins/CMakeLists.txt Normal file
View File

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.3)
project(plugins)
if (WITH_KIO)
set(CMAKE_AUTOMOC ON)
find_package(Qt5Core CONFIG REQUIRED)
set(openFileManagerWindowJob_SRC
openfilemanagerwindowjob.cpp
)
add_library(openFileManagerWindowJob SHARED ${openFileManagerWindowJob_SRC})
get_target_property(Qt5CORE_INTERFACE_INCLUDE_DIRECTORIES Qt5::Core INTERFACE_INCLUDE_DIRECTORIES)
get_target_property(KIO_WIDGETS_INTERFACE_INCLUDE_DIRECTORIES KF5::KIOWidgets INTERFACE_INCLUDE_DIRECTORIES)
get_target_property(CORE_ADDONS_INTERFACE_INCLUDE_DIRECTORIES KF5::CoreAddons INTERFACE_INCLUDE_DIRECTORIES)
target_include_directories(openFileManagerWindowJob PUBLIC ${KIO_WIDGETS_INTERFACE_INCLUDE_DIRECTORIES})
target_include_directories(openFileManagerWindowJob PUBLIC ${CORE_ADDONS_INTERFACE_INCLUDE_DIRECTORIES})
target_include_directories(openFileManagerWindowJob PUBLIC ${Qt5CORE_INTERFACE_INCLUDE_DIRECTORIES})
target_link_libraries(openFileManagerWindowJob KF5::KIOWidgets)
target_link_libraries(openFileManagerWindowJob Qt5::Core)
install(TARGETS openFileManagerWindowJob DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()

View File

@ -0,0 +1,8 @@
#include <QUrl>
#include <QObject>
#include <KIO/OpenFileManagerWindowJob>
extern "C" void highlightInFileManager(const QUrl& url) {
KIO::OpenFileManagerWindowJob* job = KIO::highlightInFileManager({url});
QObject::connect(job, &KIO::OpenFileManagerWindowJob::result, job, &KIO::OpenFileManagerWindowJob::deleteLater);
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE svg>
<svg viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 4 3 L 4 19 L 15 19 L 15 18 L 5 18 L 5 4 L 13 4 L 13 8 L 17 8 L 17 16 L 18 16 L 18 7 L 14 3 L 4 3 Z M 13 11 C 11.338 11 10 12.338 10 14 C 10 15.662 11.338 17 13 17 C 13.6494 17 14.2464 16.7914 14.7363 16.4434 L 17.293 19 L 18 18.293 L 15.4434 15.7363 C 15.7914 15.2464 16 14.6494 16 14 C 16 12.338 14.662 11 13 11 Z M 13 12 C 14.108 12 15 12.892 15 14 C 15 15.108 14.108 16 13 16 C 11.892 16 11 15.108 11 14 C 11 12.892 11.892 12 13 12 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="32" version="1.1" xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 32 32" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<defs id="defs5455">
<linearGradient inkscape:collect="always" id="linearGradient4172-5">
<stop style="stop-color:#3daee9" id="stop4174-6"/>
<stop offset="1" style="stop-color:#6cc1ef" id="stop4176-6"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4172-5" id="linearGradient4342" y1="29" y2="8" x2="0" gradientUnits="userSpaceOnUse"/>
</defs>
<metadata id="metadata5458"/>
<g inkscape:label="Capa 1" inkscape:groupmode="layer" id="layer1" transform="matrix(1 0 0 1 -384.57143 -515.798)">
<path inkscape:connector-curvature="0" style="fill:#147eb8;fill-rule:evenodd" id="path4308" d="m 386.57144,518.798 0,7 0,1 28,0 0,-6 -14.00001,0 -2,-2 z"/>
<path inkscape:connector-curvature="0" style="fill-opacity:0.235294;fill-rule:evenodd" id="path4306" d="m 397.57143,523.798 -1.99999,1 -9,0 0,1 6.99999,0 3,0 z"/>
<path style="fill:url(#linearGradient4342)" id="rect4294" d="M 13 8 L 11 10 L 2 10 L 1 10 L 1 29 L 12 29 L 13 29 L 31 29 L 31 8 L 13 8 z " transform="matrix(1 0 0 1 384.57143 515.798)"/>
<path inkscape:connector-curvature="0" style="fill:#ffffff;fill-opacity:0.235294;fill-rule:evenodd" id="path4304" d="m 397.57143,523.798 -2,2 -10,0 0,1 11,0 z"/>
<path inkscape:connector-curvature="0" style="fill:#ffffff;fill-opacity:0.235294;fill-rule:evenodd" id="path4310" d="m 398.57143,518.798 1,3 15.00001,0 0,-1 -14.00001,0 z"/>
<rect width="30" x="385.57144" y="543.79797" height="1" style="fill-opacity:0.235294" id="rect4292"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,12 @@
<!DOCTYPE svg>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 3 2 L 3 14 L 10 14 L 10 13 L 4 13 L 4 3 L 9 3 L 9 6 L 12 6 L 12 11 L 13 11 L 13 5 L 10 2 L 3 2 Z"/>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 8.48828 7 C 7.10757 7 5.98828 8.11929 5.98828 9.5 C 5.98828 10.8807 7.10757 12 8.48828 12 C 8.97811 11.9992 9.45691 11.8546 9.86523 11.584 L 12.2813 14 L 12.9883 13.293 L 10.5723 10.877 C 10.8428 10.4686 10.9875 9.98983 10.9883 9.5 C 10.9883 8.11929 9.86899 7 8.48828 7 Z M 8.48828 8 C 9.31671 8 9.98828 8.67157 9.98828 9.5 C 9.98828 10.3284 9.31671 11 8.48828 11 C 7.65985 11 6.98828 10.3284 6.98828 9.5 C 6.98828 8.67157 7.65985 8 8.48828 8 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 3 L 2 6 L 2 7 L 2 13 L 2 14 L 14 14 L 14 13 L 14 6 L 14 5 L 14 4 L 9.0078125 4 L 7.0078125 2 L 7 2.0078125 L 7 2 L 3 2 L 2 2 z M 3 3 L 6.5917969 3 L 7.59375 4 L 7 4 L 7 4.0078125 L 6.9921875 4 L 4.9921875 6 L 3 6 L 3 3 z M 3 7 L 13 7 L 13 13 L 3 13 L 3 7 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1,11 @@
<!DOCTYPE svg>
<svg viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#eff0f1;
}
</style>
</defs>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 4 3 L 4 19 L 15 19 L 15 18 L 5 18 L 5 4 L 13 4 L 13 8 L 17 8 L 17 16 L 18 16 L 18 7 L 14 3 L 4 3 Z M 13 11 C 11.338 11 10 12.338 10 14 C 10 15.662 11.338 17 13 17 C 13.6494 17 14.2464 16.7914 14.7363 16.4434 L 17.293 19 L 18 18.293 L 15.4434 15.7363 C 15.7914 15.2464 16 14.6494 16 14 C 16 12.338 14.662 11 13 11 Z M 13 12 C 14.108 12 15 12.892 15 14 C 15 15.108 14.108 16 13 16 C 11.892 16 11 15.108 11 14 C 11 12.892 11.892 12 13 12 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="32" version="1.1" xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 32 32" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<defs id="defs5455">
<linearGradient inkscape:collect="always" id="linearGradient4172-5">
<stop style="stop-color:#3daee9" id="stop4174-6"/>
<stop offset="1" style="stop-color:#6cc1ef" id="stop4176-6"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4172-5" id="linearGradient4342" y1="29" y2="8" x2="0" gradientUnits="userSpaceOnUse"/>
</defs>
<metadata id="metadata5458"/>
<g inkscape:label="Capa 1" inkscape:groupmode="layer" id="layer1" transform="matrix(1 0 0 1 -384.57143 -515.798)">
<path inkscape:connector-curvature="0" style="fill:#147eb8;fill-rule:evenodd" id="path4308" d="m 386.57144,518.798 0,7 0,1 28,0 0,-6 -14.00001,0 -2,-2 z"/>
<path inkscape:connector-curvature="0" style="fill-opacity:0.235294;fill-rule:evenodd" id="path4306" d="m 397.57143,523.798 -1.99999,1 -9,0 0,1 6.99999,0 3,0 z"/>
<path style="fill:url(#linearGradient4342)" id="rect4294" d="M 13 8 L 11 10 L 2 10 L 1 10 L 1 29 L 12 29 L 13 29 L 31 29 L 31 8 L 13 8 z " transform="matrix(1 0 0 1 384.57143 515.798)"/>
<path inkscape:connector-curvature="0" style="fill:#ffffff;fill-opacity:0.235294;fill-rule:evenodd" id="path4304" d="m 397.57143,523.798 -2,2 -10,0 0,1 11,0 z"/>
<path inkscape:connector-curvature="0" style="fill:#ffffff;fill-opacity:0.235294;fill-rule:evenodd" id="path4310" d="m 398.57143,518.798 1,3 15.00001,0 0,-1 -14.00001,0 z"/>
<rect width="30" x="385.57144" y="543.79797" height="1" style="fill-opacity:0.235294" id="rect4292"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,12 @@
<!DOCTYPE svg>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#eff0f1;
}
</style>
</defs>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 3 2 L 3 14 L 10 14 L 10 13 L 4 13 L 4 3 L 9 3 L 9 6 L 12 6 L 12 11 L 13 11 L 13 5 L 10 2 L 3 2 Z"/>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 8.48828 7 C 7.10757 7 5.98828 8.11929 5.98828 9.5 C 5.98828 10.8807 7.10757 12 8.48828 12 C 8.97811 11.9992 9.45691 11.8546 9.86523 11.584 L 12.2813 14 L 12.9883 13.293 L 10.5723 10.877 C 10.8428 10.4686 10.9875 9.98983 10.9883 9.5 C 10.9883 8.11929 9.86899 7 8.48828 7 Z M 8.48828 8 C 9.31671 8 9.98828 8.67157 9.98828 9.5 C 9.98828 10.3284 9.31671 11 8.48828 11 C 7.65985 11 6.98828 10.3284 6.98828 9.5 C 6.98828 8.67157 7.65985 8 8.48828 8 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#eff0f1;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 3 L 2 6 L 2 7 L 2 13 L 2 14 L 14 14 L 14 13 L 14 6 L 14 5 L 14 4 L 9.0078125 4 L 7.0078125 2 L 7 2.0078125 L 7 2 L 3 2 L 2 2 z M 3 3 L 6.5917969 3 L 7.59375 4 L 7 4 L 7 4.0078125 L 6.9921875 4 L 4.9921875 6 L 3 6 L 3 3 z M 3 7 L 13 7 L 13 13 L 3 13 L 3 7 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -40,6 +40,8 @@
<file>images/fallback/dark/big/favorite.svg</file>
<file>images/fallback/dark/big/unfavorite.svg</file>
<file>images/fallback/dark/big/add.svg</file>
<file>images/fallback/dark/big/folder.svg</file>
<file>images/fallback/dark/big/document-preview.svg</file>
<file>images/fallback/dark/small/absent.svg</file>
@ -80,6 +82,8 @@
<file>images/fallback/dark/small/favorite.svg</file>
<file>images/fallback/dark/small/unfavorite.svg</file>
<file>images/fallback/dark/small/add.svg</file>
<file>images/fallback/dark/small/folder.svg</file>
<file>images/fallback/dark/small/document-preview.svg</file>
<file>images/fallback/light/big/absent.svg</file>
@ -120,6 +124,8 @@
<file>images/fallback/light/big/favorite.svg</file>
<file>images/fallback/light/big/unfavorite.svg</file>
<file>images/fallback/light/big/add.svg</file>
<file>images/fallback/light/big/folder.svg</file>
<file>images/fallback/light/big/document-preview.svg</file>
<file>images/fallback/light/small/absent.svg</file>
@ -160,5 +166,7 @@
<file>images/fallback/light/small/favorite.svg</file>
<file>images/fallback/light/small/unfavorite.svg</file>
<file>images/fallback/light/small/add.svg</file>
<file>images/fallback/light/small/folder.svg</file>
<file>images/fallback/light/small/document-preview.svg</file>
</qresource>
</RCC>

View File

@ -25,5 +25,6 @@
#include "shared/message.h"
#include "shared/vcard.h"
#include "shared/global.h"
#include "shared/messageinfo.h"
#endif // SHARED_H

View File

@ -23,6 +23,11 @@
Shared::Global* Shared::Global::instance = 0;
const std::set<QString> Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"};
#ifdef WITH_KIO
QLibrary Shared::Global::openFileManagerWindowJob("openFileManagerWindowJob");
Shared::Global::HighlightInFileManager Shared::Global::hfm = 0;
#endif
Shared::Global::Global():
availability({
tr("Online", "Availability"),
@ -80,16 +85,61 @@ Shared::Global::Global():
tr("Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions", "AccountPasswordDescription")
}),
pluginSupport({
{"KWallet", false}
})
{"KWallet", false},
{"openFileManagerWindowJob", false}
}),
fileCache()
{
if (instance != 0) {
throw 551;
}
instance = this;
#ifdef WITH_KIO
openFileManagerWindowJob.load();
if (openFileManagerWindowJob.isLoaded()) {
hfm = (HighlightInFileManager) openFileManagerWindowJob.resolve("highlightInFileManager");
if (hfm) {
setSupported("openFileManagerWindowJob", true);
qDebug() << "KIO::OpenFileManagerWindow support enabled";
} else {
qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't resolve required methods in the library";
}
} else {
qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't load the library" << openFileManagerWindowJob.errorString();
}
#endif
}
Shared::Global::FileInfo Shared::Global::getFileInfo(const QString& path)
{
std::map<QString, FileInfo>::const_iterator itr = instance->fileCache.find(path);
if (itr == instance->fileCache.end()) {
QMimeDatabase db;
QMimeType type = db.mimeTypeForFile(path);
QStringList parts = type.name().split("/");
QString big = parts.front();
QFileInfo info(path);
FileInfo::Preview p = FileInfo::Preview::none;
QSize size;
if (big == "image") {
if (parts.back() == "gif") {
//TODO need to consider GIF as a movie
}
p = FileInfo::Preview::picture;
QImage img(path);
size = img.size();
}
itr = instance->fileCache.insert(std::make_pair(path, FileInfo({info.fileName(), size, type, p}))).first;
}
return itr->second;
}
Shared::Global * Shared::Global::getInstance()
{
return instance;
@ -152,6 +202,69 @@ QString Shared::Global::getDescription(Shared::AccountPassword ap)
return instance->accountPasswordDescription[static_cast<int>(ap)];
}
static const QStringList query = {"query", "default", "inode/directory"};
static const QRegularExpression dolphinReg("[Dd]olphin");
static const QRegularExpression nautilusReg("[Nn]autilus");
static const QRegularExpression cajaReg("[Cc]aja");
static const QRegularExpression nemoReg("[Nn]emo");
static const QRegularExpression konquerorReg("kfmclient");
static const QRegularExpression pcmanfmQtReg("pcmanfm-qt");
static const QRegularExpression pcmanfmReg("pcmanfm");
static const QRegularExpression thunarReg("thunar");
void Shared::Global::highlightInFileManager(const QString& path)
{
#ifdef WITH_KIO
if (supported("openFileManagerWindowJob")) {
hfm(path);
return;
} else {
qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: KIO plugin isn't loaded, trying fallback";
}
#else
qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: squawk wasn't compiled to support it, trying fallback";
#endif
QFileInfo info = path;
if (info.exists()) {
QProcess proc;
proc.start("xdg-mime", query);
proc.waitForFinished();
QString output = proc.readLine().simplified();
QString folder;
if (info.isDir()) {
folder = info.canonicalFilePath();
} else {
folder = info.canonicalPath();
}
if (output.contains(dolphinReg)) {
//there is a bug on current (21.04.0) dolphin, it works correct only if you already have dolphin launched
proc.startDetached("dolphin", QStringList() << "--select" << info.canonicalFilePath());
//KIO::highlightInFileManager({QUrl(info.canonicalFilePath())});
} else if (output.contains(nautilusReg)) {
proc.startDetached("nautilus", QStringList() << "--select" << info.canonicalFilePath()); //this worked on nautilus
} else if (output.contains(cajaReg)) {
proc.startDetached("caja", QStringList() << folder); //caja doesn't seem to support file selection command line, gonna just open directory
} else if (output.contains(nemoReg)) {
proc.startDetached("nemo", QStringList() << info.canonicalFilePath()); //nemo supports selecting files without keys
} else if (output.contains(konquerorReg)) {
proc.startDetached("konqueror", QStringList() << "--select" << info.canonicalFilePath()); //this worked on konqueror
} else if (output.contains(pcmanfmQtReg)) {
proc.startDetached("pcmanfm-qt", QStringList() << folder); //pcmanfm-qt doesn't seem to support open with selection, gonna just open directory
} else if (output.contains(pcmanfmReg)) {
proc.startDetached("pcmanfm", QStringList() << folder); //pcmanfm also doesn't seem to support open with selection, gonna just open directory
} else if (output.contains(thunarReg)) {
proc.startDetached("thunar", QStringList() << folder); //thunar doesn't seem to support open with selection, gonna just open directory
} else {
QDesktopServices::openUrl(QUrl::fromLocalFile(folder));
}
}
}
#define FROM_INT_INPL(Enum) \
template<> \
Enum Shared::Global::fromInt(int src) \

View File

@ -29,6 +29,17 @@
#include <QCoreApplication>
#include <QDebug>
#include <QMimeType>
#include <QMimeDatabase>
#include <QFileInfo>
#include <QImage>
#include <QSize>
#include <QUrl>
#include <QLibrary>
#include <QFileInfo>
#include <QProcess>
#include <QDesktopServices>
#include <QRegularExpression>
namespace Shared {
@ -36,6 +47,19 @@ namespace Shared {
Q_DECLARE_TR_FUNCTIONS(Global)
public:
struct FileInfo {
enum class Preview {
none,
picture,
movie
};
QString name;
QSize size;
QMimeType mime;
Preview preview;
};
Global();
static Global* getInstance();
@ -64,6 +88,9 @@ namespace Shared {
static const std::set<QString> supportedImagesExts;
static FileInfo getFileInfo(const QString& path);
static void highlightInFileManager(const QString& path);
template<typename T>
static T fromInt(int src);
@ -87,6 +114,15 @@ namespace Shared {
static Global* instance;
std::map<QString, bool> pluginSupport;
std::map<QString, FileInfo> fileCache;
#ifdef WITH_KIO
static QLibrary openFileManagerWindowJob;
typedef void (*HighlightInFileManager)(const QUrl &);
static HighlightInFileManager hfm;
#endif
};
}

View File

@ -170,6 +170,8 @@ static const std::map<QString, std::pair<QString, QString>> icons = {
{"favorite", {"favorite", "favorite"}},
{"unfavorite", {"draw-star", "unfavorite"}},
{"list-add", {"list-add", "add"}},
{"folder", {"folder", "folder"}},
{"document-preview", {"document-preview", "document-preview"}}
};
}

View File

@ -36,7 +36,8 @@ Shared::Message::Message(Shared::Message::Type p_type):
errorText(),
originalMessage(),
lastModified(),
stanzaId()
stanzaId(),
attachPath()
{}
Shared::Message::Message():
@ -56,7 +57,8 @@ Shared::Message::Message():
errorText(),
originalMessage(),
lastModified(),
stanzaId()
stanzaId(),
attachPath()
{}
QString Shared::Message::getBody() const
@ -311,6 +313,7 @@ void Shared::Message::serialize(QDataStream& data) const
data << lastModified;
}
data << stanzaId;
data << attachPath;
}
void Shared::Message::deserialize(QDataStream& data)
@ -341,6 +344,7 @@ void Shared::Message::deserialize(QDataStream& data)
data >> lastModified;
}
data >> stanzaId;
data >> attachPath;
}
bool Shared::Message::change(const QMap<QString, QVariant>& data)
@ -350,6 +354,16 @@ bool Shared::Message::change(const QMap<QString, QVariant>& data)
setState(static_cast<State>(itr.value().toUInt()));
}
itr = data.find("outOfBandUrl");
if (itr != data.end()) {
setOutOfBandUrl(itr.value().toString());
}
itr = data.find("attachPath");
if (itr != data.end()) {
setAttachPath(itr.value().toString());
}
if (state == State::error) {
itr = data.find("errorText");
if (itr != data.end()) {
@ -380,6 +394,8 @@ bool Shared::Message::change(const QMap<QString, QVariant>& data)
itr = data.find("body");
if (itr != data.end()) {
QString b = itr.value().toString();
if (body != b) {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
QDateTime correctionDate;
if (dItr != data.end()) {
@ -390,10 +406,19 @@ bool Shared::Message::change(const QMap<QString, QVariant>& data)
if (!edited || lastModified < correctionDate) {
originalMessage = body;
lastModified = correctionDate;
setBody(itr.value().toString());
setBody(body);
setEdited(true);
}
}
} else {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
if (dItr != data.end()) {
QDateTime ntime = dItr.value().toDateTime();
if (time != ntime) {
setTime(ntime);
}
}
}
return idChanged;
}
@ -420,7 +445,7 @@ void Shared::Message::setOutOfBandUrl(const QString& url)
bool Shared::Message::storable() const
{
return id.size() > 0 && (body.size() > 0 || oob.size()) > 0;
return id.size() > 0 && (body.size() > 0 || oob.size() > 0 || attachPath.size() > 0);
}
void Shared::Message::setStanzaId(const QString& sid)
@ -432,3 +457,33 @@ QString Shared::Message::getStanzaId() const
{
return stanzaId;
}
QString Shared::Message::getAttachPath() const
{
return attachPath;
}
void Shared::Message::setAttachPath(const QString& path)
{
attachPath = path;
}
Shared::Message::Change::Change(const QMap<QString, QVariant>& _data):
data(_data),
idModified(false) {}
void Shared::Message::Change::operator()(Shared::Message& msg)
{
idModified = msg.change(data);
}
void Shared::Message::Change::operator()(Shared::Message* msg)
{
idModified = msg->change(data);
}
bool Shared::Message::Change::hasIdBeenModified() const
{
return idModified;
}

View File

@ -16,15 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SHAPER_MESSAGE_H
#define SHAPER_MESSAGE_H
#include <QString>
#include <QDateTime>
#include <QVariant>
#include <QMap>
#include <QDataStream>
#ifndef SHAPER_MESSAGE_H
#define SHAPER_MESSAGE_H
namespace Shared {
/**
@ -46,9 +46,22 @@ public:
delivered,
error
};
static const State StateHighest = State::error;
static const State StateLowest = State::pending;
struct Change //change functor, stores in idModified if ID has been modified during change
{
Change(const QMap<QString, QVariant>& _data);
void operator() (Message& msg);
void operator() (Message* msg);
bool hasIdBeenModified() const;
private:
const QMap<QString, QVariant>& data;
bool idModified;
};
Message(Type p_type);
Message();
@ -72,6 +85,7 @@ public:
void setErrorText(const QString& err);
bool change(const QMap<QString, QVariant>& data);
void setStanzaId(const QString& sid);
void setAttachPath(const QString& path);
QString getFrom() const;
QString getFromJid() const;
@ -100,6 +114,7 @@ public:
QDateTime getLastModified() const;
QString getOriginalBody() const;
QString getStanzaId() const;
QString getAttachPath() const;
void serialize(QDataStream& data) const;
void deserialize(QDataStream& data);
@ -123,6 +138,7 @@ private:
QString originalMessage;
QDateTime lastModified;
QString stanzaId;
QString attachPath;
};
}

45
shared/messageinfo.cpp Normal file
View File

@ -0,0 +1,45 @@
/*
* 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 "messageinfo.h"
using namespace Shared;
Shared::MessageInfo::MessageInfo():
account(),
jid(),
messageId() {}
Shared::MessageInfo::MessageInfo(const QString& acc, const QString& j, const QString& id):
account(acc),
jid(j),
messageId(id) {}
Shared::MessageInfo::MessageInfo(const Shared::MessageInfo& other):
account(other.account),
jid(other.jid),
messageId(other.messageId) {}
Shared::MessageInfo & Shared::MessageInfo::operator=(const Shared::MessageInfo& other)
{
account = other.account;
jid = other.jid;
messageId = other.messageId;
return *this;
}

43
shared/messageinfo.h Normal file
View File

@ -0,0 +1,43 @@
/*
* 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 SHARED_MESSAGEINFO_H
#define SHARED_MESSAGEINFO_H
#include <QString>
namespace Shared {
/**
* @todo write docs
*/
struct MessageInfo {
MessageInfo();
MessageInfo(const QString& acc, const QString& j, const QString& id);
MessageInfo(const MessageInfo& other);
QString account;
QString jid;
QString messageId;
MessageInfo& operator=(const MessageInfo& other);
};
}
#endif // SHARED_MESSAGEINFO_H

View File

@ -20,9 +20,12 @@
#define SHARED_UTILS_H
#include <QString>
#include <QStringList>
#include <QColor>
#include <QRegularExpression>
//#include "KIO/OpenFileManagerWindowJob"
#include <uuid/uuid.h>
#include <vector>

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.3)
project(squawkUI)
# Instruct CMake to run moc automatically when needed.
@ -7,9 +7,14 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
# Find the QtWidgets library
find_package(Qt5Widgets CONFIG REQUIRED)
find_package(Qt5DBus CONFIG REQUIRED)
find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets DBus Core)
find_package(Boost 1.36.0 CONFIG REQUIRED COMPONENTS
date_time filesystem iostreams)
if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS})
endif()
add_subdirectory(utils)
add_subdirectory(widgets)
set(squawkUI_SRC
@ -25,15 +30,8 @@ set(squawkUI_SRC
models/abstractparticipant.cpp
models/participant.cpp
models/reference.cpp
utils/messageline.cpp
utils//message.cpp
utils/resizer.cpp
utils/image.cpp
utils/flowlayout.cpp
utils/badge.cpp
utils/progress.cpp
utils/comboboxdelegate.cpp
utils/dropshadoweffect.cpp
models/messagefeed.cpp
models/element.cpp
)
# Tell CMake to create the helloworld executable
@ -41,5 +39,6 @@ add_library(squawkUI ${squawkUI_SRC})
# Use the Widgets module from Qt 5.
target_link_libraries(squawkUI squawkWidgets)
target_link_libraries(squawkUI squawkUIUtils)
target_link_libraries(squawkUI Qt5::Widgets)
target_link_libraries(squawkUI Qt5::DBus)

View File

@ -231,7 +231,7 @@ void Models::Account::toOfflineState()
Item::toOfflineState();
}
QString Models::Account::getAvatarPath()
QString Models::Account::getAvatarPath() const
{
return avatarPath;
}

View File

@ -57,7 +57,7 @@ namespace Models {
QString getError() const;
void setAvatarPath(const QString& path);
QString getAvatarPath();
QString getAvatarPath() const;
void setAvailability(Shared::Availability p_avail);
void setAvailability(unsigned int p_avail);

View File

@ -17,55 +17,26 @@
*/
#include "contact.h"
#include "account.h"
#include <QDebug>
Models::Contact::Contact(const Account* acc, const QString& p_jid ,const QMap<QString, QVariant> &data, Item *parentItem):
Item(Item::contact, data, parentItem),
jid(p_jid),
Element(Item::contact, acc, p_jid, data, parentItem),
availability(Shared::Availability::offline),
state(Shared::SubscriptionState::none),
avatarState(Shared::Avatar::empty),
presences(),
messages(),
childMessages(0),
status(),
avatarPath(),
account(acc)
status()
{
QMap<QString, QVariant>::const_iterator itr = data.find("state");
if (itr != data.end()) {
setState(itr.value().toUInt());
}
itr = data.find("avatarState");
if (itr != data.end()) {
setAvatarState(itr.value().toUInt());
}
itr = data.find("avatarPath");
if (itr != data.end()) {
setAvatarPath(itr.value().toString());
}
}
Models::Contact::~Contact()
{
}
QString Models::Contact::getJid() const
{
return jid;
}
void Models::Contact::setJid(const QString p_jid)
{
if (jid != p_jid) {
jid = p_jid;
changed(1);
}
}
void Models::Contact::setAvailability(unsigned int p_state)
{
setAvailability(Shared::Global::fromInt<Shared::Availability>(p_state));
@ -144,16 +115,12 @@ void Models::Contact::update(const QString& field, const QVariant& value)
{
if (field == "name") {
setName(value.toString());
} else if (field == "jid") {
setJid(value.toString());
} else if (field == "availability") {
setAvailability(value.toUInt());
} else if (field == "state") {
setState(value.toUInt());
} else if (field == "avatarState") {
setAvatarState(value.toUInt());
} else if (field == "avatarPath") {
setAvatarPath(value.toString());
} else {
Element::update(field, value);
}
}
@ -192,11 +159,9 @@ void Models::Contact::refresh()
{
QDateTime lastActivity;
Presence* presence = 0;
unsigned int count = 0;
for (QMap<QString, Presence*>::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) {
Presence* pr = itr.value();
QDateTime la = pr->getLastActivity();
count += pr->getMessagesCount();
if (la > lastActivity) {
lastActivity = la;
@ -211,11 +176,6 @@ void Models::Contact::refresh()
setAvailability(Shared::Availability::offline);
setStatus("");
}
if (childMessages != count) {
childMessages = count;
changed(4);
}
}
void Models::Contact::_removeChild(int index)
@ -257,81 +217,6 @@ QIcon Models::Contact::getStatusIcon(bool big) const
}
}
void Models::Contact::addMessage(const Shared::Message& data)
{
const QString& res = data.getPenPalResource();
if (res.size() > 0) {
QMap<QString, Presence*>::iterator itr = presences.find(res);
if (itr == presences.end()) {
// this is actually the place when I can spot someone's invisible presence, and there is nothing criminal in it, cuz the sender sent us a message
// therefore he have revealed himself
// the only issue is to find out when the sender is gone offline
Presence* pr = new Presence({});
pr->setName(res);
pr->setAvailability(Shared::Availability::invisible);
pr->setLastActivity(QDateTime::currentDateTimeUtc());
presences.insert(res, pr);
appendChild(pr);
pr->addMessage(data);
return;
}
itr.value()->addMessage(data);
} else {
messages.emplace_back(data);
changed(4);
}
}
void Models::Contact::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
bool found = false;
for (Shared::Message& msg : messages) {
if (msg.getId() == id) {
msg.change(data);
found = true;
break;
}
}
if (!found) {
for (Presence* pr : presences) {
found = pr->changeMessage(id, data);
if (found) {
break;
}
}
}
}
unsigned int Models::Contact::getMessagesCount() const
{
return messages.size() + childMessages;
}
void Models::Contact::dropMessages()
{
if (messages.size() > 0) {
messages.clear();
changed(4);
}
for (QMap<QString, Presence*>::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) {
itr.value()->dropMessages();
}
}
void Models::Contact::getMessages(Models::Contact::Messages& container) const
{
for (Messages::const_iterator itr = messages.begin(), end = messages.end(); itr != end; ++itr) {
const Shared::Message& msg = *itr;
container.push_back(msg);
}
for (QMap<QString, Presence*>::const_iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) {
itr.value()->getMessages(container);
}
}
void Models::Contact::toOfflineState()
{
std::deque<Item*>::size_type size = childItems.size();
@ -355,75 +240,3 @@ QString Models::Contact::getDisplayedName() const
return getContactName();
}
bool Models::Contact::columnInvolvedInDisplay(int col)
{
return Item::columnInvolvedInDisplay(col) && col == 1;
}
Models::Contact * Models::Contact::copy() const
{
Contact* cnt = new Contact(*this);
return cnt;
}
Models::Contact::Contact(const Models::Contact& other):
Item(other),
jid(other.jid),
availability(other.availability),
state(other.state),
presences(),
messages(other.messages),
childMessages(0),
account(other.account)
{
for (const Presence* pres : other.presences) {
Presence* pCopy = new Presence(*pres);
presences.insert(pCopy->getName(), pCopy);
Item::appendChild(pCopy);
connect(pCopy, &Item::childChanged, this, &Contact::refresh);
}
refresh();
}
QString Models::Contact::getAvatarPath() const
{
return avatarPath;
}
Shared::Avatar Models::Contact::getAvatarState() const
{
return avatarState;
}
void Models::Contact::setAvatarPath(const QString& path)
{
if (path != avatarPath) {
avatarPath = path;
changed(7);
}
}
void Models::Contact::setAvatarState(Shared::Avatar p_state)
{
if (avatarState != p_state) {
avatarState = p_state;
changed(6);
}
}
void Models::Contact::setAvatarState(unsigned int p_state)
{
if (p_state <= static_cast<quint8>(Shared::Avatar::valid)) {
Shared::Avatar state = static_cast<Shared::Avatar>(p_state);
setAvatarState(state);
} else {
qDebug() << "An attempt to set invalid avatar state" << p_state << "to the contact" << jid << ", skipping";
}
}
const Models::Account * Models::Contact::getParentAccount() const
{
return account;
}

View File

@ -19,7 +19,7 @@
#ifndef MODELS_CONTACT_H
#define MODELS_CONTACT_H
#include "item.h"
#include "element.h"
#include "presence.h"
#include "shared/enums.h"
#include "shared/message.h"
@ -31,49 +31,34 @@
#include <deque>
namespace Models {
class Account;
class Contact : public Item
class Contact : public Element
{
Q_OBJECT
public:
typedef std::deque<Shared::Message> Messages;
Contact(const Account* acc, const QString& p_jid, const QMap<QString, QVariant> &data, Item *parentItem = 0);
Contact(const Contact& other);
~Contact();
QString getJid() const;
Shared::Availability getAvailability() const;
Shared::SubscriptionState getState() const;
Shared::Avatar getAvatarState() const;
QString getAvatarPath() const;
QIcon getStatusIcon(bool big = false) const;
int columnCount() const override;
QVariant data(int column) const override;
void update(const QString& field, const QVariant& value);
void update(const QString& field, const QVariant& value) override;
void addPresence(const QString& name, const QMap<QString, QVariant>& data);
void removePresence(const QString& name);
QString getContactName() const;
QString getStatus() const;
void addMessage(const Shared::Message& data);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
unsigned int getMessagesCount() const;
void dropMessages();
void getMessages(Messages& container) const;
QString getDisplayedName() const override;
Contact* copy() const;
protected:
void _removeChild(int index) override;
void _appendChild(Models::Item * child) override;
bool columnInvolvedInDisplay(int col) override;
const Account* getParentAccount() const override;
protected slots:
void refresh();
@ -84,23 +69,13 @@ protected:
void setAvailability(unsigned int p_state);
void setState(Shared::SubscriptionState p_state);
void setState(unsigned int p_state);
void setAvatarState(Shared::Avatar p_state);
void setAvatarState(unsigned int p_state);
void setAvatarPath(const QString& path);
void setJid(const QString p_jid);
void setStatus(const QString& p_state);
private:
QString jid;
Shared::Availability availability;
Shared::SubscriptionState state;
Shared::Avatar avatarState;
QMap<QString, Presence*> presences;
Messages messages;
unsigned int childMessages;
QString status;
QString avatarPath;
const Account* account;
};
}

184
ui/models/element.cpp Normal file
View File

@ -0,0 +1,184 @@
/*
* 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 "element.h"
#include "account.h"
#include <QDebug>
Models::Element::Element(Type p_type, const Models::Account* acc, const QString& p_jid, const QMap<QString, QVariant>& data, Models::Item* parentItem):
Item(p_type, data, parentItem),
jid(p_jid),
avatarPath(),
avatarState(Shared::Avatar::empty),
account(acc),
feed(new MessageFeed(this))
{
connect(feed, &MessageFeed::requestArchive, this, &Element::requestArchive);
connect(feed, &MessageFeed::fileDownloadRequest, this, &Element::fileDownloadRequest);
connect(feed, &MessageFeed::unreadMessagesCountChanged, this, &Element::onFeedUnreadMessagesCountChanged);
connect(feed, &MessageFeed::unnoticedMessage, this, &Element::onFeedUnnoticedMessage);
connect(feed, &MessageFeed::localPathInvalid, this, &Element::localPathInvalid);
QMap<QString, QVariant>::const_iterator itr = data.find("avatarState");
if (itr != data.end()) {
setAvatarState(itr.value().toUInt());
}
itr = data.find("avatarPath");
if (itr != data.end()) {
setAvatarPath(itr.value().toString());
}
}
Models::Element::~Element()
{
delete feed;
}
QString Models::Element::getJid() const
{
return jid;
}
void Models::Element::setJid(const QString& p_jid)
{
if (jid != p_jid) {
jid = p_jid;
changed(1);
}
}
void Models::Element::update(const QString& field, const QVariant& value)
{
if (field == "jid") {
setJid(value.toString());
} else if (field == "avatarState") {
setAvatarState(value.toUInt());
} else if (field == "avatarPath") {
setAvatarPath(value.toString());
}
}
QString Models::Element::getAvatarPath() const
{
return avatarPath;
}
Shared::Avatar Models::Element::getAvatarState() const
{
return avatarState;
}
void Models::Element::setAvatarPath(const QString& path)
{
if (path != avatarPath) {
avatarPath = path;
if (type == contact) {
changed(7);
} else if (type == room) {
changed(8);
}
}
}
void Models::Element::setAvatarState(Shared::Avatar p_state)
{
if (avatarState != p_state) {
avatarState = p_state;
if (type == contact) {
changed(6);
} else if (type == room) {
changed(7);
}
}
}
void Models::Element::setAvatarState(unsigned int p_state)
{
if (p_state <= static_cast<quint8>(Shared::Avatar::valid)) {
Shared::Avatar state = static_cast<Shared::Avatar>(p_state);
setAvatarState(state);
} else {
qDebug() << "An attempt to set invalid avatar state" << p_state << "to the element" << jid << ", skipping";
}
}
bool Models::Element::columnInvolvedInDisplay(int col)
{
return Item::columnInvolvedInDisplay(col) && col == 1;
}
const Models::Account * Models::Element::getParentAccount() const
{
return account;
}
unsigned int Models::Element::getMessagesCount() const
{
return feed->unreadMessagesCount();
}
void Models::Element::addMessage(const Shared::Message& data)
{
feed->addMessage(data);
}
void Models::Element::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
feed->changeMessage(id, data);
}
void Models::Element::responseArchive(const std::list<Shared::Message> list, bool last)
{
feed->responseArchive(list, last);
}
bool Models::Element::isRoom() const
{
return type != contact;
}
void Models::Element::fileProgress(const QString& messageId, qreal value, bool up)
{
feed->fileProgress(messageId, value, up);
}
void Models::Element::fileComplete(const QString& messageId, bool up)
{
feed->fileComplete(messageId, up);
}
void Models::Element::fileError(const QString& messageId, const QString& error, bool up)
{
feed->fileError(messageId, error, up);
}
void Models::Element::onFeedUnreadMessagesCountChanged()
{
if (type == contact) {
changed(4);
} else if (type == room) {
changed(5);
}
}
void Models::Element::onFeedUnnoticedMessage(const Shared::Message& msg)
{
emit unnoticedMessage(getAccountName(), msg);
}

81
ui/models/element.h Normal file
View File

@ -0,0 +1,81 @@
/*
* 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 ELEMENT_H
#define ELEMENT_H
#include "item.h"
#include "messagefeed.h"
namespace Models {
class Element : public Item
{
Q_OBJECT
protected:
Element(Type p_type, const Account* acc, const QString& p_jid, const QMap<QString, QVariant> &data, Item *parentItem = 0);
~Element();
public:
QString getJid() const;
Shared::Avatar getAvatarState() const;
QString getAvatarPath() const;
virtual void update(const QString& field, const QVariant& value);
void addMessage(const Shared::Message& data);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
unsigned int getMessagesCount() const;
void responseArchive(const std::list<Shared::Message> list, bool last);
bool isRoom() const;
void fileProgress(const QString& messageId, qreal value, bool up);
void fileError(const QString& messageId, const QString& error, bool up);
void fileComplete(const QString& messageId, bool up);
signals:
void requestArchive(const QString& before);
void fileDownloadRequest(const QString& url);
void unnoticedMessage(const QString& account, const Shared::Message& msg);
void localPathInvalid(const QString& path);
protected:
void setJid(const QString& p_jid);
void setAvatarState(Shared::Avatar p_state);
void setAvatarState(unsigned int p_state);
void setAvatarPath(const QString& path);
bool columnInvolvedInDisplay(int col) override;
const Account* getParentAccount() const override;
protected slots:
void onFeedUnreadMessagesCountChanged();
void onFeedUnnoticedMessage(const Shared::Message& msg);
protected:
QString jid;
QString avatarPath;
Shared::Avatar avatarState;
const Account* account;
public:
MessageFeed* feed;
};
}
#endif // ELEMENT_H

View File

@ -283,6 +283,15 @@ Shared::ConnectionState Models::Item::getAccountConnectionState() const
return acc->getState();
}
QString Models::Item::getAccountAvatarPath() const
{
const Account* acc = getParentAccount();
if (acc == nullptr) {
return "";
}
return acc->getAvatarPath();
}
QString Models::Item::getDisplayedName() const
{
return name;

View File

@ -80,6 +80,7 @@ class Item : public QObject{
QString getAccountName() const;
QString getAccountJid() const;
QString getAccountResource() const;
QString getAccountAvatarPath() const;
Shared::ConnectionState getAccountConnectionState() const;
Shared::Availability getAccountAvailability() const;

576
ui/models/messagefeed.cpp Normal file
View File

@ -0,0 +1,576 @@
/*
* 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 "messagefeed.h"
#include "element.h"
#include "room.h"
#include <QDebug>
const QHash<int, QByteArray> Models::MessageFeed::roles = {
{Text, "text"},
{Sender, "sender"},
{Date, "date"},
{DeliveryState, "deliveryState"},
{Correction, "correction"},
{SentByMe,"sentByMe"},
{Avatar, "avatar"},
{Attach, "attach"},
{Id, "id"},
{Error, "error"},
{Bulk, "bulk"}
};
Models::MessageFeed::MessageFeed(const Element* ri, QObject* parent):
QAbstractListModel(parent),
storage(),
indexById(storage.get<id>()),
indexByTime(storage.get<time>()),
rosterItem(ri),
syncState(incomplete),
uploads(),
downloads(),
unreadMessages(new std::set<QString>()),
observersAmount(0)
{
}
Models::MessageFeed::~MessageFeed()
{
delete unreadMessages;
for (Shared::Message* message : storage) {
delete message;
}
}
void Models::MessageFeed::addMessage(const Shared::Message& msg)
{
QString id = msg.getId();
StorageById::const_iterator itr = indexById.find(id);
if (itr != indexById.end()) {
qDebug() << "received more then one message with the same id, skipping yet the new one";
return;
}
Shared::Message* copy = new Shared::Message(msg);
StorageByTime::const_iterator tItr = indexByTime.upper_bound(msg.getTime());
int position;
if (tItr == indexByTime.end()) {
position = storage.size();
} else {
position = indexByTime.rank(tItr);
}
beginInsertRows(QModelIndex(), position, position);
storage.insert(copy);
endInsertRows();
emit newMessage(msg);
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
emit unreadMessagesCountChanged();
emit unnoticedMessage(msg);
}
}
void Models::MessageFeed::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
StorageById::iterator itr = indexById.find(id);
if (itr == indexById.end()) {
qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
return;
}
Shared::Message* msg = *itr;
std::set<MessageRoles> changeRoles = detectChanges(*msg, data);
QModelIndex index = modelIndexByTime(id, msg->getTime());
Shared::Message::Change functor(data);
bool success = indexById.modify(itr, functor);
if (!success) {
qDebug() << "received a command to change a message, but something went wrong modifying message in the feed, throwing error";
throw 872;
}
if (functor.hasIdBeenModified()) {
changeRoles.insert(MessageRoles::Id);
std::set<QString>::const_iterator umi = unreadMessages->find(id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
unreadMessages->insert(msg->getId());
}
}
if (changeRoles.size() > 0) {
//change message is a final event in download/upload event train
//only after changeMessage we can consider the download is done
Progress::const_iterator dItr = downloads.find(id);
bool attachOrError = changeRoles.count(MessageRoles::Attach) > 0 || changeRoles.count(MessageRoles::Error);
if (dItr != downloads.end()) {
if (attachOrError) {
downloads.erase(dItr);
} else if (changeRoles.count(MessageRoles::Id) > 0) {
qreal progress = dItr->second;
downloads.erase(dItr);
downloads.insert(std::make_pair(msg->getId(), progress));
}
} else {
dItr = uploads.find(id);
if (dItr != uploads.end()) {
if (attachOrError) {
uploads.erase(dItr);
} else if (changeRoles.count(MessageRoles::Id) > 0) {
qreal progress = dItr->second;
uploads.erase(dItr);
uploads.insert(std::make_pair(msg->getId(), progress));
}
}
}
QVector<int> cr;
for (MessageRoles role : changeRoles) {
cr.push_back(role);
}
emit dataChanged(index, index, cr);
}
}
std::set<Models::MessageFeed::MessageRoles> Models::MessageFeed::detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const
{
std::set<MessageRoles> roles;
Shared::Message::State state = msg.getState();
QMap<QString, QVariant>::const_iterator itr = data.find("state");
if (itr != data.end() && static_cast<Shared::Message::State>(itr.value().toUInt()) != state) {
roles.insert(MessageRoles::DeliveryState);
}
itr = data.find("outOfBandUrl");
bool att = false;
if (itr != data.end() && itr.value().toString() != msg.getOutOfBandUrl()) {
roles.insert(MessageRoles::Attach);
att = true;
}
if (!att) {
itr = data.find("attachPath");
if (itr != data.end() && itr.value().toString() != msg.getAttachPath()) {
roles.insert(MessageRoles::Attach);
}
}
if (state == Shared::Message::State::error) {
itr = data.find("errorText");
if (itr != data.end() && itr.value().toString() != msg.getErrorText()) {
roles.insert(MessageRoles::Error);
}
}
itr = data.find("body");
if (itr != data.end() && itr.value().toString() != msg.getBody()) {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
QDateTime correctionDate;
if (dItr != data.end()) {
correctionDate = dItr.value().toDateTime();
} else {
correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied
}
if (!msg.getEdited() || msg.getLastModified() < correctionDate) {
roles.insert(MessageRoles::Text);
roles.insert(MessageRoles::Correction);
}
} else {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
if (dItr != data.end()) {
QDateTime ntime = dItr.value().toDateTime();
if (msg.getTime() != ntime) {
roles.insert(MessageRoles::Date);
}
}
}
return roles;
}
void Models::MessageFeed::removeMessage(const QString& id)
{
}
QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
{
int i = index.row();
QVariant answer;
StorageByTime::const_iterator itr = indexByTime.nth(i);
if (itr != indexByTime.end()) {
const Shared::Message* msg = *itr;
switch (role) {
case Qt::DisplayRole:
case Text: {
QString body = msg->getBody();
if (body != msg->getOutOfBandUrl()) {
answer = body;
}
}
break;
case Sender:
if (sentByMe(*msg)) {
answer = rosterItem->getAccountName();
} else {
if (rosterItem->isRoom()) {
answer = msg->getFromResource();
} else {
answer = rosterItem->getDisplayedName();
}
}
break;
case Date:
answer = msg->getTime();
break;
case DeliveryState:
answer = static_cast<unsigned int>(msg->getState());
break;
case Correction:
answer = msg->getEdited();
break;
case SentByMe:
answer = sentByMe(*msg);
break;
case Avatar: {
QString path;
if (sentByMe(*msg)) {
path = rosterItem->getAccountAvatarPath();
} else if (!rosterItem->isRoom()) {
if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
path = rosterItem->getAvatarPath();
}
} else {
const Room* room = static_cast<const Room*>(rosterItem);
path = room->getParticipantIconPath(msg->getFromResource());
}
if (path.size() == 0) {
answer = Shared::iconPath("user", true);
} else {
answer = path;
}
}
break;
case Attach:
answer.setValue(fillAttach(*msg));
break;
case Id:
answer.setValue(msg->getId());
break;
break;
case Error:
answer.setValue(msg->getErrorText());
break;
case Bulk: {
FeedItem item;
item.id = msg->getId();
std::set<QString>::const_iterator umi = unreadMessages->find(item.id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCount();
}
item.sentByMe = sentByMe(*msg);
item.date = msg->getTime();
item.state = msg->getState();
item.error = msg->getErrorText();
item.correction = msg->getEdited();
QString body = msg->getBody();
if (body != msg->getOutOfBandUrl()) {
item.text = body;
}
item.avatar.clear();
if (item.sentByMe) {
item.sender = rosterItem->getAccountName();
item.avatar = rosterItem->getAccountAvatarPath();
} else {
if (rosterItem->isRoom()) {
item.sender = msg->getFromResource();
const Room* room = static_cast<const Room*>(rosterItem);
item.avatar = room->getParticipantIconPath(msg->getFromResource());
} else {
item.sender = rosterItem->getDisplayedName();
if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
item.avatar = rosterItem->getAvatarPath();
}
}
}
if (item.avatar.size() == 0) {
item.avatar = Shared::iconPath("user", true);
}
item.attach = fillAttach(*msg);
answer.setValue(item);
}
break;
default:
break;
}
}
return answer;
}
int Models::MessageFeed::rowCount(const QModelIndex& parent) const
{
return storage.size();
}
unsigned int Models::MessageFeed::unreadMessagesCount() const
{
return unreadMessages->size();
}
bool Models::MessageFeed::canFetchMore(const QModelIndex& parent) const
{
return syncState == incomplete;
}
void Models::MessageFeed::fetchMore(const QModelIndex& parent)
{
if (syncState == incomplete) {
syncState = syncing;
emit syncStateChange(syncState);
emit requestStateChange(true);
if (storage.size() == 0) {
emit requestArchive("");
} else {
emit requestArchive((*indexByTime.rbegin())->getId());
}
}
}
void Models::MessageFeed::responseArchive(const std::list<Shared::Message> list, bool last)
{
Storage::size_type size = storage.size();
beginInsertRows(QModelIndex(), size, size + list.size() - 1);
for (const Shared::Message& msg : list) {
Shared::Message* copy = new Shared::Message(msg);
storage.insert(copy);
}
endInsertRows();
if (syncState == syncing) {
if (last) {
syncState = complete;
} else {
syncState = incomplete;
}
emit syncStateChange(syncState);
emit requestStateChange(false);
}
}
QModelIndex Models::MessageFeed::index(int row, int column, const QModelIndex& parent) const
{
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
StorageByTime::iterator itr = indexByTime.nth(row);
if (itr != indexByTime.end()) {
Shared::Message* msg = *itr;
return createIndex(row, column, msg);
} else {
return QModelIndex();
}
}
QHash<int, QByteArray> Models::MessageFeed::roleNames() const
{
return roles;
}
bool Models::MessageFeed::sentByMe(const Shared::Message& msg) const
{
if (rosterItem->isRoom()) {
const Room* room = static_cast<const Room*>(rosterItem);
return room->getNick().toLower() == msg.getFromResource().toLower();
} else {
return msg.getOutgoing();
}
}
Models::Attachment Models::MessageFeed::fillAttach(const Shared::Message& msg) const
{
::Models::Attachment att;
att.localPath = msg.getAttachPath();
att.remotePath = msg.getOutOfBandUrl();
if (att.remotePath.size() == 0) {
if (att.localPath.size() == 0) {
att.state = none;
} else {
Progress::const_iterator itr = uploads.find(msg.getId());
if (itr == uploads.end()) {
att.state = local;
} else {
att.state = uploading;
att.progress = itr->second;
}
}
} else {
if (att.localPath.size() == 0) {
Progress::const_iterator itr = downloads.find(msg.getId());
if (itr == downloads.end()) {
att.state = remote;
} else {
att.state = downloading;
att.progress = itr->second;
}
} else {
att.state = ready;
}
}
return att;
}
void Models::MessageFeed::downloadAttachment(const QString& messageId)
{
QModelIndex ind = modelIndexById(messageId);
if (ind.isValid()) {
std::pair<Progress::iterator, bool> progressPair = downloads.insert(std::make_pair(messageId, 0));
if (progressPair.second) { //Only to take action if we weren't already downloading it
Shared::Message* msg = static_cast<Shared::Message*>(ind.internalPointer());
emit dataChanged(ind, ind, {MessageRoles::Attach});
emit fileDownloadRequest(msg->getOutOfBandUrl());
} else {
qDebug() << "Attachment download for message with id" << messageId << "is already in progress, skipping";
}
} else {
qDebug() << "An attempt to download an attachment for the message that doesn't exist. ID:" << messageId;
}
}
void Models::MessageFeed::uploadAttachment(const QString& messageId)
{
qDebug() << "request to upload attachment of the message" << messageId;
}
bool Models::MessageFeed::registerUpload(const QString& messageId)
{
return uploads.insert(std::make_pair(messageId, 0)).second;
}
void Models::MessageFeed::fileProgress(const QString& messageId, qreal value, bool up)
{
Progress* pr = 0;
if (up) {
pr = &uploads;
} else {
pr = &downloads;
}
Progress::iterator itr = pr->find(messageId);
if (itr != pr->end()) {
itr->second = value;
QModelIndex ind = modelIndexById(messageId);
emit dataChanged(ind, ind); //the type of the attach didn't change, so, there is no need to relayout, there is no role in event
}
}
void Models::MessageFeed::fileComplete(const QString& messageId, bool up)
{
fileProgress(messageId, 1, up);
}
void Models::MessageFeed::fileError(const QString& messageId, const QString& error, bool up)
{
//TODO
}
void Models::MessageFeed::incrementObservers()
{
++observersAmount;
}
void Models::MessageFeed::decrementObservers()
{
--observersAmount;
}
QModelIndex Models::MessageFeed::modelIndexById(const QString& id) const
{
StorageById::const_iterator itr = indexById.find(id);
if (itr != indexById.end()) {
Shared::Message* msg = *itr;
return modelIndexByTime(id, msg->getTime());
}
return QModelIndex();
}
QModelIndex Models::MessageFeed::modelIndexByTime(const QString& id, const QDateTime& time) const
{
StorageByTime::const_iterator tItr = indexByTime.upper_bound(time);
StorageByTime::const_iterator tBeg = indexByTime.begin();
bool found = false;
while (tItr != tBeg) {
if (id == (*tItr)->getId()) {
found = true;
break;
}
--tItr;
}
if (found || id == (*tItr)->getId()) {
int position = indexByTime.rank(tItr);
return createIndex(position, 0, *tItr);
}
return QModelIndex();
}
void Models::MessageFeed::reportLocalPathInvalid(const QString& messageId)
{
StorageById::iterator itr = indexById.find(messageId);
if (itr == indexById.end()) {
qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
return;
}
Shared::Message* msg = *itr;
emit localPathInvalid(msg->getAttachPath());
//gonna change the message in current model right away, to prevent spam on each attemt to draw element
QModelIndex index = modelIndexByTime(messageId, msg->getTime());
msg->setAttachPath("");
emit dataChanged(index, index, {MessageRoles::Attach});
}
Models::MessageFeed::SyncState Models::MessageFeed::getSyncState() const
{
return syncState;
}

195
ui/models/messagefeed.h Normal file
View File

@ -0,0 +1,195 @@
/*
* 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 MESSAGEFEED_H
#define MESSAGEFEED_H
#include <QAbstractListModel>
#include <QDateTime>
#include <QString>
#include <set>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/ranked_index.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <shared/message.h>
#include <shared/icons.h>
namespace Models {
class Element;
struct Attachment;
class MessageFeed : public QAbstractListModel
{
Q_OBJECT
public:
enum SyncState {
incomplete,
syncing,
complete
};
MessageFeed(const Element* rosterItem, QObject *parent = nullptr);
~MessageFeed();
void addMessage(const Shared::Message& msg);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void removeMessage(const QString& id);
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
bool canFetchMore(const QModelIndex & parent) const override;
void fetchMore(const QModelIndex & parent) override;
QHash<int, QByteArray> roleNames() const override;
QModelIndex index(int row, int column, const QModelIndex & parent) const override;
void responseArchive(const std::list<Shared::Message> list, bool last);
void downloadAttachment(const QString& messageId);
void uploadAttachment(const QString& messageId);
bool registerUpload(const QString& messageId);
void reportLocalPathInvalid(const QString& messageId);
unsigned int unreadMessagesCount() const;
void fileProgress(const QString& messageId, qreal value, bool up);
void fileError(const QString& messageId, const QString& error, bool up);
void fileComplete(const QString& messageId, bool up);
void incrementObservers();
void decrementObservers();
SyncState getSyncState() const;
signals:
void requestArchive(const QString& before);
void requestStateChange(bool requesting);
void fileDownloadRequest(const QString& url);
void unreadMessagesCountChanged();
void newMessage(const Shared::Message& msg);
void unnoticedMessage(const Shared::Message& msg);
void localPathInvalid(const QString& path);
void syncStateChange(SyncState state);
public:
enum MessageRoles {
Text = Qt::UserRole + 1,
Sender,
Date,
DeliveryState,
Correction,
SentByMe,
Avatar,
Attach,
Id,
Error,
Bulk
};
protected:
bool sentByMe(const Shared::Message& msg) const;
Attachment fillAttach(const Shared::Message& msg) const;
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const;
private:
//tags
struct id {};
struct time {};
typedef boost::multi_index_container<
Shared::Message*,
boost::multi_index::indexed_by<
boost::multi_index::ordered_unique<
boost::multi_index::tag<id>,
boost::multi_index::const_mem_fun<
Shared::Message,
QString,
&Shared::Message::getId
>
>,
boost::multi_index::ranked_non_unique<
boost::multi_index::tag<time>,
boost::multi_index::const_mem_fun<
Shared::Message,
QDateTime,
&Shared::Message::getTime
>,
std::greater<QDateTime>
>
>
> Storage;
typedef Storage::index<id>::type StorageById;
typedef Storage::index<time>::type StorageByTime;
Storage storage;
StorageById& indexById;
StorageByTime& indexByTime;
const Element* rosterItem;
SyncState syncState;
typedef std::map<QString, qreal> Progress;
Progress uploads;
Progress downloads;
std::set<QString>* unreadMessages;
uint16_t observersAmount;
static const QHash<int, QByteArray> roles;
};
enum AttachmentType {
none,
remote,
local,
downloading,
uploading,
errorDownload,
errorUpload,
ready
};
struct Attachment {
AttachmentType state;
qreal progress;
QString localPath;
QString remotePath;
};
struct FeedItem {
QString id;
QString text;
QString sender;
QString avatar;
QString error;
bool sentByMe;
bool correction;
QDateTime date;
Shared::Message::State state;
Attachment attach;
};
};
Q_DECLARE_METATYPE(Models::Attachment);
Q_DECLARE_METATYPE(Models::FeedItem);
#endif // MESSAGEFEED_H

View File

@ -20,82 +20,15 @@
#include "shared/icons.h"
Models::Presence::Presence(const QMap<QString, QVariant>& data, Item* parentItem):
AbstractParticipant(Item::presence, data, parentItem),
messages()
AbstractParticipant(Item::presence, data, parentItem)
{
}
Models::Presence::Presence(const Models::Presence& other):
AbstractParticipant(other),
messages(other.messages)
{
}
Models::Presence::~Presence()
{
}
int Models::Presence::columnCount() const
{
return 5;
}
QVariant Models::Presence::data(int column) const
{
switch (column) {
case 4:
return getMessagesCount();
default:
return AbstractParticipant::data(column);
}
}
unsigned int Models::Presence::getMessagesCount() const
{
return messages.size();
}
void Models::Presence::addMessage(const Shared::Message& data)
{
messages.emplace_back(data);
changed(4);
}
bool Models::Presence::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
bool found = false;
for (Shared::Message& msg : messages) {
if (msg.getId() == id) {
msg.change(data);
found = true;
break;
}
}
return found;
}
void Models::Presence::dropMessages()
{
if (messages.size() > 0) {
messages.clear();
changed(4);
}
}
QIcon Models::Presence::getStatusIcon(bool big) const
{
if (getMessagesCount() > 0) {
return Shared::icon("mail-message", big);
} else {
return AbstractParticipant::getStatusIcon();
}
}
void Models::Presence::getMessages(Models::Presence::Messages& container) const
{
for (Messages::const_iterator itr = messages.begin(), end = messages.end(); itr != end; ++itr) {
const Shared::Message& msg = *itr;
container.push_back(msg);
}
return 4;
}

View File

@ -32,25 +32,10 @@ class Presence : public Models::AbstractParticipant
{
Q_OBJECT
public:
typedef std::deque<Shared::Message> Messages;
explicit Presence(const QMap<QString, QVariant> &data, Item *parentItem = 0);
Presence(const Presence& other);
~Presence();
int columnCount() const override;
QVariant data(int column) const override;
QIcon getStatusIcon(bool big = false) const override;
unsigned int getMessagesCount() const;
void dropMessages();
void addMessage(const Shared::Message& data);
bool changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void getMessages(Messages& container) const;
private:
Messages messages;
};
}

View File

@ -22,16 +22,12 @@
#include <QIcon>
#include <QDebug>
Models::Room::Room(const QString& p_jid, const QMap<QString, QVariant>& data, Models::Item* parentItem):
Item(room, data, parentItem),
Models::Room::Room(const Account* acc, const QString& p_jid, const QMap<QString, QVariant>& data, Models::Item* parentItem):
Element(room, acc, p_jid, data, parentItem),
autoJoin(false),
joined(false),
jid(p_jid),
nick(""),
subject(""),
avatarState(Shared::Avatar::empty),
avatarPath(""),
messages(),
participants(),
exParticipantAvatars()
{
@ -55,16 +51,6 @@ Models::Room::Room(const QString& p_jid, const QMap<QString, QVariant>& data, Mo
setSubject(itr.value().toString());
}
itr = data.find("avatarState");
if (itr != data.end()) {
setAvatarState(itr.value().toUInt());
}
itr = data.find("avatarPath");
if (itr != data.end()) {
setAvatarPath(itr.value().toString());
}
itr = data.find("avatars");
if (itr != data.end()) {
QMap<QString, QVariant> avs = itr.value().toMap();
@ -78,21 +64,11 @@ Models::Room::~Room()
{
}
unsigned int Models::Room::getUnreadMessagesCount() const
{
return messages.size();
}
int Models::Room::columnCount() const
{
return 7;
}
QString Models::Room::getJid() const
{
return jid;
}
bool Models::Room::getAutoJoin() const
{
return autoJoin;
@ -151,14 +127,6 @@ void Models::Room::setAutoJoin(bool p_autoJoin)
}
}
void Models::Room::setJid(const QString& p_jid)
{
if (jid != p_jid) {
jid = p_jid;
changed(1);
}
}
void Models::Room::setJoined(bool p_joined)
{
if (joined != p_joined) {
@ -182,8 +150,6 @@ void Models::Room::update(const QString& field, const QVariant& value)
{
if (field == "name") {
setName(value.toString());
} else if (field == "jid") {
setJid(value.toString());
} else if (field == "joined") {
setJoined(value.toBool());
} else if (field == "autoJoin") {
@ -192,16 +158,14 @@ void Models::Room::update(const QString& field, const QVariant& value)
setNick(value.toString());
} else if (field == "subject") {
setSubject(value.toString());
} else if (field == "avatarState") {
setAvatarState(value.toUInt());
} else if (field == "avatarPath") {
setAvatarPath(value.toString());
} else {
Element::update(field, value);
}
}
QIcon Models::Room::getStatusIcon(bool big) const
{
if (messages.size() > 0) {
if (getMessagesCount() > 0) {
return Shared::icon("mail-message", big);
} else {
if (autoJoin) {
@ -237,42 +201,6 @@ QString Models::Room::getStatusText() const
}
}
unsigned int Models::Room::getMessagesCount() const
{
return messages.size();
}
void Models::Room::addMessage(const Shared::Message& data)
{
messages.emplace_back(data);
changed(5);
}
void Models::Room::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
for (Shared::Message& msg : messages) {
if (msg.getId() == id) {
msg.change(data);
break;
}
}
}
void Models::Room::dropMessages()
{
if (messages.size() > 0) {
messages.clear();
changed(5);
}
}
void Models::Room::getMessages(Models::Room::Messages& container) const
{
for (Messages::const_iterator itr = messages.begin(), end = messages.end(); itr != end; ++itr) {
const Shared::Message& msg = *itr;
container.push_back(msg);
}
}
void Models::Room::toOfflineState()
{
@ -367,47 +295,6 @@ QString Models::Room::getDisplayedName() const
return getRoomName();
}
bool Models::Room::columnInvolvedInDisplay(int col)
{
return Item::columnInvolvedInDisplay(col) && col == 1;
}
QString Models::Room::getAvatarPath() const
{
return avatarPath;
}
Shared::Avatar Models::Room::getAvatarState() const
{
return avatarState;
}
void Models::Room::setAvatarPath(const QString& path)
{
if (avatarPath != path) {
avatarPath = path;
changed(8);
}
}
void Models::Room::setAvatarState(Shared::Avatar p_state)
{
if (avatarState != p_state) {
avatarState = p_state;
changed(7);
}
}
void Models::Room::setAvatarState(unsigned int p_state)
{
if (p_state <= static_cast<quint8>(Shared::Avatar::valid)) {
Shared::Avatar state = static_cast<Shared::Avatar>(p_state);
setAvatarState(state);
} else {
qDebug() << "An attempt to set invalid avatar state" << p_state << "to the room" << jid << ", skipping";
}
}
std::map<QString, const Models::Participant &> Models::Room::getParticipants() const
{
std::map<QString, const Models::Participant&> result;
@ -423,8 +310,13 @@ QString Models::Room::getParticipantIconPath(const QString& name) const
{
std::map<QString, Models::Participant*>::const_iterator itr = participants.find(name);
if (itr == participants.end()) {
std::map<QString, QString>::const_iterator eitr = exParticipantAvatars.find(name);
if (eitr != exParticipantAvatars.end()) {
return eitr->second;
} else {
return "";
}
}
return itr->second->getAvatarPath();
}

View File

@ -19,7 +19,7 @@
#ifndef MODELS_ROOM_H
#define MODELS_ROOM_H
#include "item.h"
#include "element.h"
#include "participant.h"
#include "shared/enums.h"
#include "shared/message.h"
@ -29,21 +29,18 @@ namespace Models {
/**
* @todo write docs
*/
class Room : public Models::Item
class Room : public Element
{
Q_OBJECT
public:
typedef std::deque<Shared::Message> Messages;
Room(const QString& p_jid, const QMap<QString, QVariant> &data, Item *parentItem = 0);
Room(const Account* acc, const QString& p_jid, const QMap<QString, QVariant> &data, Item *parentItem = 0);
~Room();
int columnCount() const override;
QVariant data(int column) const override;
unsigned int getUnreadMessagesCount() const;
bool getJoined() const;
bool getAutoJoin() const;
QString getJid() const;
QString getNick() const;
QString getRoomName() const;
QString getSubject() const;
@ -53,17 +50,10 @@ public:
void setJoined(bool p_joined);
void setAutoJoin(bool p_autoJoin);
void setJid(const QString& p_jid);
void setNick(const QString& p_nick);
void setSubject(const QString& sub);
void update(const QString& field, const QVariant& value);
void addMessage(const Shared::Message& data);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
unsigned int getMessagesCount() const;
void dropMessages();
void getMessages(Messages& container) const;
void update(const QString& field, const QVariant& value) override;
void addParticipant(const QString& name, const QMap<QString, QVariant>& data);
void changeParticipant(const QString& name, const QMap<QString, QVariant>& data);
@ -71,8 +61,6 @@ public:
void toOfflineState() override;
QString getDisplayedName() const override;
Shared::Avatar getAvatarState() const;
QString getAvatarPath() const;
std::map<QString, const Participant&> getParticipants() const;
QString getParticipantIconPath(const QString& name) const;
std::map<QString, QString> getExParticipantAvatars() const;
@ -84,24 +72,14 @@ signals:
private:
void handleParticipantUpdate(std::map<QString, Participant*>::const_iterator itr, const QMap<QString, QVariant>& data);
protected:
bool columnInvolvedInDisplay(int col) override;
void setAvatarState(Shared::Avatar p_state);
void setAvatarState(unsigned int p_state);
void setAvatarPath(const QString& path);
private:
bool autoJoin;
bool joined;
QString jid;
QString nick;
QString subject;
Shared::Avatar avatarState;
QString avatarPath;
Messages messages;
std::map<QString, Participant*> participants;
std::map<QString, QString> exParticipantAvatars;
};
}

View File

@ -215,11 +215,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
break;
case Item::presence: {
Presence* contact = static_cast<Presence*>(item);
QString str("");
int mc = contact->getMessagesCount();
if (mc > 0) {
str += tr("New messages: ") + std::to_string(mc).c_str() + "\n";
}
QString str;
Shared::Availability av = contact->getAvailability();
str += tr("Availability: ") + Shared::Global::getName(av);
QString s = contact->getStatus();
@ -232,7 +228,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
break;
case Item::participant: {
Participant* p = static_cast<Participant*>(item);
QString str("");
QString str;
Shared::Availability av = p->getAvailability();
str += tr("Availability: ") + Shared::Global::getName(av) + "\n";
QString s = p->getStatus();
@ -260,7 +256,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
break;
case Item::room: {
Room* rm = static_cast<Room*>(item);
unsigned int count = rm->getUnreadMessagesCount();
unsigned int count = rm->getMessagesCount();
QString str("");
if (count > 0) {
str += tr("New messages: ") + std::to_string(count).c_str() + "\n";
@ -450,6 +446,10 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons
std::map<ElId, Contact*>::iterator itr = contacts.find(id);
if (itr == contacts.end()) {
contact = new Contact(acc, jid, data);
connect(contact, &Contact::requestArchive, this, &Roster::onElementRequestArchive);
connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid);
contacts.insert(std::make_pair(id, contact));
} else {
contact = itr->second;
@ -535,35 +535,19 @@ 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)
{
ElId id(account, jid);
std::map<ElId, Contact*>::iterator cItr = contacts.find(id);
if (cItr != contacts.end()) {
Element* el = getElement({account, jid});
if (el != NULL) {
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
cItr->second->update(itr.key(), itr.value());
}
} else {
std::map<ElId, Room*>::iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
rItr->second->update(itr.key(), itr.value());
}
el->update(itr.key(), itr.value());
}
}
}
void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data)
{
ElId elid(account, jid);
std::map<ElId, Contact*>::iterator cItr = contacts.find(elid);
if (cItr != contacts.end()) {
cItr->second->changeMessage(id, data);
} else {
std::map<ElId, Room*>::iterator rItr = rooms.find(elid);
if (rItr != rooms.end()) {
rItr->second->changeMessage(id, data);
}
Element* el = getElement({account, jid});
if (el != NULL) {
el->changeMessage(id, data);
}
}
@ -627,7 +611,6 @@ void Models::Roster::removeContact(const QString& account, const QString& jid, c
} else {
delete ref;
}
if (gr->childCount() == 0) {
removeGroup(account, group);
}
@ -708,29 +691,9 @@ void Models::Roster::removePresence(const QString& account, const QString& jid,
void Models::Roster::addMessage(const QString& account, const Shared::Message& data)
{
ElId id(account, data.getPenPalJid());
std::map<ElId, Contact*>::iterator itr = contacts.find(id);
if (itr != contacts.end()) {
itr->second->addMessage(data);
} else {
std::map<ElId, Room*>::const_iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
rItr->second->addMessage(data);
}
}
}
void Models::Roster::dropMessages(const QString& account, const QString& jid)
{
ElId id(account, jid);
std::map<ElId, Contact*>::iterator itr = contacts.find(id);
if (itr != contacts.end()) {
itr->second->dropMessages();
} else {
std::map<ElId, Room*>::const_iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
rItr->second->dropMessages();
}
Element* el = getElement({account, data.getPenPalJid()});
if (el != NULL) {
el->addMessage(data);
}
}
@ -821,7 +784,11 @@ void Models::Roster::addRoom(const QString& account, const QString jid, const QM
return;
}
Room* room = new Room(jid, data);
Room* room = new Room(acc, jid, data);
connect(room, &Contact::requestArchive, this, &Roster::onElementRequestArchive);
connect(room, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest);
connect(room, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage);
connect(room, &Contact::localPathInvalid, this, &Roster::localPathInvalid);
rooms.insert(std::make_pair(id, room));
acc->appendChild(room);
}
@ -974,3 +941,65 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString&
}
}
}
void Models::Roster::onElementRequestArchive(const QString& before)
{
Element* el = static_cast<Element*>(sender());
emit requestArchive(el->getAccountName(), el->getJid(), before);
}
void Models::Roster::responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last)
{
ElId id(account, jid);
Element* el = getElement(id);
if (el != NULL) {
el->responseArchive(list, last);
}
}
void Models::Roster::fileProgress(const std::list<Shared::MessageInfo>& msgs, qreal value, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
el->fileProgress(info.messageId, value, up);
}
}
}
void Models::Roster::fileComplete(const std::list<Shared::MessageInfo>& msgs, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
el->fileComplete(info.messageId, up);
}
}
}
void Models::Roster::fileError(const std::list<Shared::MessageInfo>& msgs, const QString& err, bool up)
{
for (const Shared::MessageInfo& info : msgs) {
Element* el = getElement({info.account, info.jid});
if (el != NULL) {
el->fileError(info.messageId, err, up);
}
}
}
Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id)
{
std::map<ElId, Contact*>::iterator cItr = contacts.find(id);
if (cItr != contacts.end()) {
return cItr->second;
} else {
std::map<ElId, Room*>::iterator rItr = rooms.find(id);
if (rItr != rooms.end()) {
return rItr->second;
}
}
return NULL;
}

View File

@ -26,6 +26,7 @@
#include "shared/message.h"
#include "shared/global.h"
#include "shared/messageinfo.h"
#include "accounts.h"
#include "item.h"
#include "account.h"
@ -58,7 +59,6 @@ public:
void removePresence(const QString& account, const QString& jid, const QString& name);
void addMessage(const QString& account, const Shared::Message& data);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void dropMessages(const QString& account, const QString& jid);
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);
@ -81,15 +81,22 @@ public:
Account* getAccount(const QString& name);
QModelIndex getAccountIndex(const QString& name);
QModelIndex getGroupIndex(const QString& account, const QString& name);
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 fileError(const std::list<Shared::MessageInfo>& msgs, const QString& err, bool up);
void fileComplete(const std::list<Shared::MessageInfo>& msgs, bool up);
Accounts* accountsModel;
signals:
void requestArchive(const QString& account, const QString& jid, const QString& before);
void fileDownloadRequest(const QString& url);
void unnoticedMessage(const QString& account, const Shared::Message& msg);
void localPathInvalid(const QString& path);
private:
Item* root;
std::map<QString, Account*> accounts;
std::map<ElId, Group*> groups;
std::map<ElId, Contact*> contacts;
std::map<ElId, Room*> rooms;
Element* getElement(const ElId& id);
private slots:
void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector<int>& roles);
@ -100,6 +107,14 @@ private slots:
void onChildRemoved();
void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex);
void onChildMoved();
void onElementRequestArchive(const QString& before);
private:
Item* root;
std::map<QString, Account*> accounts;
std::map<ElId, Group*> groups;
std::map<ElId, Contact*> contacts;
std::map<ElId, Room*> rooms;
public:
class ElId {

View File

@ -29,7 +29,6 @@ Squawk::Squawk(QWidget *parent) :
conversations(),
contextMenu(new QMenu()),
dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()),
requestedFiles(),
vCards(),
requestedAccountsForPasswords(),
prompt(0),
@ -60,8 +59,12 @@ Squawk::Squawk(QWidget *parent) :
connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu);
connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed);
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, &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);
//m_ui->mainToolBar->addWidget(m_ui->comboBox);
@ -336,17 +339,14 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
Models::Account* acc = rosterModel.getAccount(id->account);
Conversation* conv = 0;
bool created = false;
Models::Contact::Messages deque;
if (itr != conversations.end()) {
conv = itr->second;
} else if (contact != 0) {
created = true;
conv = new Chat(acc, contact);
contact->getMessages(deque);
} else if (room != 0) {
created = true;
conv = new Room(acc, room);
room->getMessages(deque);
if (!room->getJoined()) {
emit setRoomJoined(id->account, id->name, true);
@ -358,12 +358,6 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
conv->setAttribute(Qt::WA_DeleteOnClose);
subscribeConversation(conv);
conversations.insert(std::make_pair(*id, conv));
if (created) {
for (Models::Contact::Messages::const_iterator itr = deque.begin(), end = deque.end(); itr != end; ++itr) {
conv->addMessage(*itr);
}
}
}
conv->show();
@ -380,12 +374,6 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
}
}
void Squawk::onConversationShown()
{
Conversation* conv = static_cast<Conversation*>(sender());
rosterModel.dropMessages(conv->getAccount(), conv->getJid());
}
void Squawk::onConversationClosed(QObject* parent)
{
Conversation* conv = static_cast<Conversation*>(sender());
@ -402,164 +390,40 @@ void Squawk::onConversationClosed(QObject* parent)
}
}
void Squawk::onConversationDownloadFile(const QString& messageId, const QString& url)
void Squawk::fileProgress(const std::list<Shared::MessageInfo> msgs, qreal value, bool up)
{
Conversation* conv = static_cast<Conversation*>(sender());
std::map<QString, std::set<Models::Roster::ElId>>::iterator itr = requestedFiles.find(messageId);
bool created = false;
if (itr == requestedFiles.end()) {
itr = requestedFiles.insert(std::make_pair(messageId, std::set<Models::Roster::ElId>())).first;
created = true;
}
itr->second.insert(Models::Roster::ElId(conv->getAccount(), conv->getJid()));
if (created) {
emit downloadFileRequest(messageId, url);
}
rosterModel.fileProgress(msgs, value, up);
}
void Squawk::fileProgress(const QString& messageId, qreal value)
void Squawk::fileDownloadComplete(const std::list<Shared::MessageInfo> msgs, const QString& path)
{
std::map<QString, std::set<Models::Roster::ElId>>::const_iterator itr = requestedFiles.find(messageId);
if (itr == requestedFiles.end()) {
qDebug() << "fileProgress in UI Squawk but there is nobody waiting for that id" << messageId << ", skipping";
return;
} else {
const std::set<Models::Roster::ElId>& convs = itr->second;
for (std::set<Models::Roster::ElId>::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) {
const Models::Roster::ElId& id = *cItr;
Conversations::const_iterator c = conversations.find(id);
if (c != conversations.end()) {
c->second->responseFileProgress(messageId, value);
}
if (currentConversation != 0 && currentConversation->getId() == id) {
currentConversation->responseFileProgress(messageId, value);
}
}
}
rosterModel.fileComplete(msgs, false);
}
void Squawk::fileError(const QString& messageId, const QString& error)
void Squawk::fileError(const std::list<Shared::MessageInfo> msgs, const QString& error, bool up)
{
std::map<QString, std::set<Models::Roster::ElId>>::const_iterator itr = requestedFiles.find(messageId);
if (itr == requestedFiles.end()) {
qDebug() << "fileError in UI Squawk but there is nobody waiting for that id" << messageId << ", skipping";
return;
} else {
const std::set<Models::Roster::ElId>& convs = itr->second;
for (std::set<Models::Roster::ElId>::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) {
const Models::Roster::ElId& id = *cItr;
Conversations::const_iterator c = conversations.find(id);
if (c != conversations.end()) {
c->second->fileError(messageId, error);
}
if (currentConversation != 0 && currentConversation->getId() == id) {
currentConversation->fileError(messageId, error);
}
}
requestedFiles.erase(itr);
}
rosterModel.fileError(msgs, error, up);
}
void Squawk::fileLocalPathResponse(const QString& messageId, const QString& path)
void Squawk::fileUploadComplete(const std::list<Shared::MessageInfo> msgs, const QString& path)
{
std::map<QString, std::set<Models::Roster::ElId>>::const_iterator itr = requestedFiles.find(messageId);
if (itr == requestedFiles.end()) {
qDebug() << "fileLocalPathResponse in UI Squawk but there is nobody waiting for that path, skipping";
return;
} else {
const std::set<Models::Roster::ElId>& convs = itr->second;
for (std::set<Models::Roster::ElId>::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) {
const Models::Roster::ElId& id = *cItr;
Conversations::const_iterator c = conversations.find(id);
if (c != conversations.end()) {
c->second->responseLocalFile(messageId, path);
}
if (currentConversation != 0 && currentConversation->getId() == id) {
currentConversation->responseLocalFile(messageId, path);
}
}
requestedFiles.erase(itr);
}
}
void Squawk::onConversationRequestLocalFile(const QString& messageId, const QString& url)
{
Conversation* conv = static_cast<Conversation*>(sender());
std::map<QString, std::set<Models::Roster::ElId>>::iterator itr = requestedFiles.find(messageId);
bool created = false;
if (itr == requestedFiles.end()) {
itr = requestedFiles.insert(std::make_pair(messageId, std::set<Models::Roster::ElId>())).first;
created = true;
}
itr->second.insert(Models::Roster::ElId(conv->getAccount(), conv->getJid()));
if (created) {
emit fileLocalPathRequest(messageId, url);
}
rosterModel.fileComplete(msgs, true);
}
void Squawk::accountMessage(const QString& account, const Shared::Message& data)
{
const QString& from = data.getPenPalJid();
Models::Roster::ElId id({account, from});
Conversations::iterator itr = conversations.find(id);
bool found = false;
if (currentConversation != 0 && currentConversation->getId() == id) {
currentConversation->addMessage(data);
QApplication::alert(this);
if (!isVisible() && !data.getForwarded()) {
notify(account, data);
}
found = true;
}
if (itr != conversations.end()) {
Conversation* conv = itr->second;
conv->addMessage(data);
QApplication::alert(conv);
if (!found && conv->isMinimized()) {
rosterModel.addMessage(account, data);
}
if (!conv->isVisible() && !data.getForwarded()) {
notify(account, data);
}
found = true;
}
}
if (!found) {
rosterModel.addMessage(account, data);
if (!data.getForwarded()) {
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);
notify(account, data);
}
}
}
void Squawk::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data)
{
Models::Roster::ElId eid({account, jid});
bool found = false;
if (currentConversation != 0 && currentConversation->getId() == eid) {
currentConversation->changeMessage(id, data);
QApplication::alert(this);
found = true;
}
Conversations::iterator itr = conversations.find(eid);
if (itr != conversations.end()) {
Conversation* conv = itr->second;
conv->changeMessage(id, data);
if (!found && conv->isMinimized()) {
rosterModel.changeMessage(account, jid, id, data);
}
found = true;
}
if (!found) {
rosterModel.changeMessage(account, jid, id, data);
}
}
void Squawk::notify(const QString& account, const Shared::Message& msg)
@ -596,60 +460,20 @@ void Squawk::notify(const QString& account, const Shared::Message& msg)
void Squawk::onConversationMessage(const Shared::Message& msg)
{
Conversation* conv = static_cast<Conversation*>(sender());
emit sendMessage(conv->getAccount(), msg);
Models::Roster::ElId id = conv->getId();
QString acc = conv->getAccount();
if (currentConversation != 0 && currentConversation->getId() == id) {
if (conv == currentConversation) {
Conversations::iterator itr = conversations.find(id);
if (itr != conversations.end()) {
itr->second->addMessage(msg);
}
} else {
currentConversation->addMessage(msg);
}
}
rosterModel.addMessage(acc, msg);
emit sendMessage(acc, msg);
}
void Squawk::onConversationMessage(const Shared::Message& msg, const QString& path)
void Squawk::onRequestArchive(const QString& account, const QString& jid, const QString& before)
{
Conversation* conv = static_cast<Conversation*>(sender());
Models::Roster::ElId id = conv->getId();
std::map<QString, std::set<Models::Roster::ElId>>::iterator itr = requestedFiles.insert(std::make_pair(msg.getId(), std::set<Models::Roster::ElId>())).first;
itr->second.insert(id);
if (currentConversation != 0 && currentConversation->getId() == id) {
if (conv == currentConversation) {
Conversations::iterator itr = conversations.find(id);
if (itr != conversations.end()) {
itr->second->appendMessageWithUpload(msg, path);
}
} else {
currentConversation->appendMessageWithUpload(msg, path);
}
}
emit sendMessage(conv->getAccount(), msg, path);
emit requestArchive(account, jid, 20, before); //TODO amount as a settings value
}
void Squawk::onConversationRequestArchive(const QString& before)
void Squawk::responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list, bool last)
{
Conversation* conv = static_cast<Conversation*>(sender());
requestArchive(conv->getAccount(), conv->getJid(), 20, before); //TODO amount as a settings value
}
void Squawk::responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list)
{
Models::Roster::ElId id(account, jid);
if (currentConversation != 0 && currentConversation->getId() == id) {
currentConversation->responseArchive(list);
}
Conversations::const_iterator itr = conversations.find(id);
if (itr != conversations.end()) {
itr->second->responseArchive(list);
}
rosterModel.responseArchive(account, jid, list, last);
}
void Squawk::removeAccount(const QString& account)
@ -661,8 +485,6 @@ void Squawk::removeAccount(const QString& account)
++itr;
Conversation* conv = lItr->second;
disconnect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed);
disconnect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive);
disconnect(conv, &Conversation::shown, this, &Squawk::onConversationShown);
conv->close();
conversations.erase(lItr);
} else {
@ -926,7 +748,6 @@ void Squawk::onActivateVCard(const QString& account, const QString& jid, bool ed
{
std::map<QString, VCard*>::const_iterator itr = vCards.find(jid);
VCard* card;
Models::Contact::Messages deque;
if (itr != vCards.end()) {
card = itr->second;
} else {
@ -1092,13 +913,8 @@ void Squawk::onPasswordPromptRejected()
void Squawk::subscribeConversation(Conversation* conv)
{
connect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed);
connect(conv, qOverload<const Shared::Message&>(&Conversation::sendMessage), this, qOverload<const Shared::Message&>(&Squawk::onConversationMessage));
connect(conv, qOverload<const Shared::Message&, const QString&>(&Conversation::sendMessage),
this, qOverload<const Shared::Message&, const QString&>(&Squawk::onConversationMessage));
connect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive);
connect(conv, &Conversation::requestLocalFile, this, &Squawk::onConversationRequestLocalFile);
connect(conv, &Conversation::downloadFile, this, &Squawk::onConversationDownloadFile);
connect(conv, &Conversation::shown, this, &Squawk::onConversationShown);
connect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage);
connect(conv, &Conversation::notifyableMessage, this, &Squawk::notify);
}
void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
@ -1168,13 +984,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
}
Models::Account* acc = rosterModel.getAccount(id->account);
Models::Contact::Messages deque;
if (contact != 0) {
currentConversation = new Chat(acc, contact);
contact->getMessages(deque);
} else if (room != 0) {
currentConversation = new Room(acc, room);
room->getMessages(deque);
if (!room->getJoined()) {
emit setRoomJoined(id->account, id->name, true);
@ -1185,9 +998,6 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn
}
subscribeConversation(currentConversation);
for (Models::Contact::Messages::const_iterator itr = deque.begin(), end = deque.end(); itr != end; ++itr) {
currentConversation->addMessage(*itr);
}
if (res.size() > 0) {
currentConversation->setPalResource(res);

View File

@ -63,7 +63,6 @@ signals:
void disconnectAccount(const QString&);
void changeState(Shared::Availability state);
void sendMessage(const QString& account, const Shared::Message& data);
void sendMessage(const QString& account, const Shared::Message& data, const QString& path);
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);
@ -76,11 +75,11 @@ signals:
void setRoomAutoJoin(const QString& account, const QString& jid, bool joined);
void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin);
void removeRoomRequest(const QString& account, const QString& jid);
void fileLocalPathRequest(const QString& messageId, const QString& url);
void downloadFileRequest(const QString& messageId, const QString& url);
void fileDownloadRequest(const QString& url);
void requestVCard(const QString& account, const QString& jid);
void uploadVCard(const QString& account, const Shared::VCard& card);
void responsePassword(const QString& account, const QString& password);
void localPathInvalid(const QString& path);
public slots:
void readSettings();
@ -97,16 +96,17 @@ public slots:
void removePresence(const QString& account, const QString& jid, const QString& name);
void stateChanged(Shared::Availability state);
void accountMessage(const QString& account, const Shared::Message& data);
void responseArchive(const QString& account, const QString& jid, const std::list<Shared::Message>& list);
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 fileLocalPathResponse(const QString& messageId, const QString& path);
void fileError(const QString& messageId, const QString& error);
void fileProgress(const QString& messageId, qreal value);
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& path);
void responseVCard(const QString& jid, const Shared::VCard& card);
void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap<QString, QVariant>& data);
void requestPassword(const QString& account);
@ -120,7 +120,6 @@ private:
Conversations conversations;
QMenu* contextMenu;
QDBusInterface dbus;
std::map<QString, std::set<Models::Roster::ElId>> requestedFiles;
std::map<QString, VCard*> vCards;
std::deque<QString> requestedAccountsForPasswords;
QInputDialog* prompt;
@ -130,6 +129,8 @@ private:
protected:
void closeEvent(QCloseEvent * event) override;
protected slots:
void notify(const QString& account, const Shared::Message& msg);
private slots:
@ -147,18 +148,16 @@ private slots:
void onComboboxActivated(int index);
void onRosterItemDoubleClicked(const QModelIndex& item);
void onConversationMessage(const Shared::Message& msg);
void onConversationMessage(const Shared::Message& msg, const QString& path);
void onConversationRequestArchive(const QString& before);
void onRequestArchive(const QString& account, const QString& jid, const QString& before);
void onRosterContextMenu(const QPoint& point);
void onConversationShown();
void onConversationRequestLocalFile(const QString& messageId, const QString& url);
void onConversationDownloadFile(const QString& messageId, const QString& url);
void onItemCollepsed(const QModelIndex& index);
void onPasswordPromptAccepted();
void onPasswordPromptRejected();
void onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
void onContextAboutToHide();
void onUnnoticedMessage(const QString& account, const Shared::Message& msg);
private:
void checkNextAccountForPassword();
void onPasswordPromptDone();

32
ui/utils/CMakeLists.txt Normal file
View File

@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.3)
project(squawkUIUtils)
# Instruct CMake to run moc automatically when needed.
set(CMAKE_AUTOMOC ON)
# Instruct CMake to create code from Qt designer ui files
set(CMAKE_AUTOUIC ON)
# Find the QtWidgets library
find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets Core)
set(squawkUIUtils_SRC
# messageline.cpp
# message.cpp
resizer.cpp
# image.cpp
flowlayout.cpp
badge.cpp
progress.cpp
comboboxdelegate.cpp
feedview.cpp
messagedelegate.cpp
exponentialblur.cpp
shadowoverlay.cpp
)
# Tell CMake to create the helloworld executable
add_library(squawkUIUtils STATIC ${squawkUIUtils_SRC})
# Use the Widgets module from Qt 5.
target_link_libraries(squawkUIUtils squawkWidgets)
target_link_libraries(squawkUIUtils Qt5::Widgets)

View File

@ -37,7 +37,7 @@ QWidget* ComboboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewI
{
QComboBox *cb = new QComboBox(parent);
for (const std::pair<QString, QIcon> pair : entries) {
for (const std::pair<QString, QIcon>& pair : entries) {
cb->addItem(pair.second, pair.first);
}

View File

@ -1,93 +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 DROPSHADOWEFFECT_H
#define DROPSHADOWEFFECT_H
#include <QGraphicsEffect>
#include <QPainter>
#include <QPointF>
#include <QColor>
class PixmapFilter : public QObject
{
Q_OBJECT
public:
PixmapFilter(QObject *parent = nullptr);
virtual ~PixmapFilter() = 0;
virtual QRectF boundingRectFor(const QRectF &rect) const;
virtual void draw(QPainter *painter, const QPointF &p, const QPixmap &src, const QRectF &srcRect = QRectF()) const = 0;
};
class PixmapDropShadowFilter : public PixmapFilter
{
Q_OBJECT
public:
PixmapDropShadowFilter(QObject *parent = nullptr);
~PixmapDropShadowFilter();
void draw(QPainter *p, const QPointF &pos, const QPixmap &px, const QRectF &src = QRectF()) const override;
qreal blurRadius() const;
void setBlurRadius(qreal radius);
QColor color() const;
void setColor(const QColor &color);
qreal thickness() const;
void setThickness(qreal thickness);
void setFrame(bool top, bool right, bool bottom, bool left);
protected:
QColor mColor;
qreal mRadius;
qreal mThickness;
bool top;
bool right;
bool bottom;
bool left;
};
class DropShadowEffect : public QGraphicsEffect
{
Q_OBJECT
public:
qreal blurRadius() const;
QColor color() const;
void setFrame(bool top, bool right, bool bottom, bool left);
void setThickness(qreal thickness);
signals:
void blurRadiusChanged(qreal blurRadius);
void colorChanged(const QColor &color);
public slots:
void setBlurRadius(qreal blurRadius);
void setColor(const QColor &color);
protected:
void draw(QPainter * painter) override;
protected:
PixmapDropShadowFilter filter;
};
#endif // DROPSHADOWEFFECT_H

View File

@ -16,8 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "dropshadoweffect.h"
#include "QtMath"
#include "exponentialblur.h"
static const int tileSize = 32;
template <class T>
@ -574,128 +573,7 @@ void expblur(QImage &img, qreal radius, bool improvedQuality = false, int transp
}
}
PixmapFilter::PixmapFilter(QObject* parent):QObject(parent) {}
PixmapFilter::~PixmapFilter(){}
QRectF PixmapFilter::boundingRectFor(const QRectF &rect) const {return rect;}
PixmapDropShadowFilter::PixmapDropShadowFilter(QObject *parent):
PixmapFilter(parent),
mColor(63, 63, 63, 180),
mRadius(1),
mThickness(2),
top(true),
right(true),
bottom(true),
left(true){}
PixmapDropShadowFilter::~PixmapDropShadowFilter() {}
qreal PixmapDropShadowFilter::blurRadius() const {return mRadius;}
void PixmapDropShadowFilter::setBlurRadius(qreal radius) {mRadius = radius;}
QColor PixmapDropShadowFilter::color() const {return mColor;}
void PixmapDropShadowFilter::setColor(const QColor &color) {mColor = color;}
qreal PixmapDropShadowFilter::thickness() const {return mThickness;}
void PixmapDropShadowFilter::setThickness(qreal thickness) {mThickness = thickness;}
void PixmapDropShadowFilter::setFrame(bool ptop, bool pright, bool pbottom, bool pleft)
void Utils::exponentialblur(QImage& img, qreal radius, bool improvedQuality, int transposed)
{
top = ptop;
right = pright;
bottom = pbottom;
left = pleft;
}
void DropShadowEffect::setThickness(qreal thickness)
{
if (filter.thickness() == thickness)
return;
filter.setThickness(thickness);
update();
}
void PixmapDropShadowFilter::draw(QPainter *p, const QPointF &pos, const QPixmap &px, const QRectF &src) const
{
if (px.isNull())
return;
QImage tmp({px.width(), px.height() + int(mThickness)}, QImage::Format_ARGB32_Premultiplied);
tmp.setDevicePixelRatio(px.devicePixelRatioF());
tmp.fill(0);
QPainter tmpPainter(&tmp);
tmpPainter.setCompositionMode(QPainter::CompositionMode_Source);
if (top) {
QRectF shadow(0, 0, px.width(), mThickness);
tmpPainter.fillRect(shadow, mColor);
}
if (right) {
QRectF shadow(px.width() - mThickness, 0, mThickness, px.height());
tmpPainter.fillRect(shadow, mColor);
}
if (bottom) {
QRectF shadow(0, px.height() - mThickness, px.width(), mThickness * 2); //i have no idea why, but it leaves some unpainted stripe without some spare space
tmpPainter.fillRect(shadow, mColor);
}
if (left) {
QRectF shadow(0, 0, mThickness, px.height());
tmpPainter.fillRect(shadow, mColor);
}
expblur<12, 10, false>(tmp, mRadius, false, 0);
tmpPainter.end();
// Draw the actual pixmap...
p->drawPixmap(pos, px, src);
// draw the blurred drop shadow...
p->drawImage(pos, tmp);
}
qreal DropShadowEffect::blurRadius() const {return filter.blurRadius();}
void DropShadowEffect::setBlurRadius(qreal blurRadius)
{
if (qFuzzyCompare(filter.blurRadius(), blurRadius))
return;
filter.setBlurRadius(blurRadius);
updateBoundingRect();
emit blurRadiusChanged(blurRadius);
}
void DropShadowEffect::setFrame(bool top, bool right, bool bottom, bool left)
{
filter.setFrame(top, right, bottom, left);
update();
}
QColor DropShadowEffect::color() const {return filter.color();}
void DropShadowEffect::setColor(const QColor &color)
{
if (filter.color() == color)
return;
filter.setColor(color);
update();
emit colorChanged(color);
}
void DropShadowEffect::draw(QPainter* painter)
{
if (filter.blurRadius() <= 0 && filter.thickness() == 0) {
drawSource(painter);
return;
}
PixmapPadMode mode = PadToEffectiveBoundingRect;
// Draw pixmap in device coordinates to avoid pixmap scaling.
QPoint offset;
const QPixmap pixmap = sourcePixmap(Qt::DeviceCoordinates, &offset, mode);
if (pixmap.isNull())
return;
QTransform restoreTransform = painter->worldTransform();
painter->setWorldTransform(QTransform());
filter.draw(painter, offset, pixmap);
painter->setWorldTransform(restoreTransform);
expblur<12, 10, false>(img, radius, improvedQuality, transposed);
}

View File

@ -0,0 +1,34 @@
/*
* 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 EXPONENTIALBLUR_H
#define EXPONENTIALBLUR_H
#include <QObject>
#include <QImage>
#include <QtMath>
/**
* @todo write docs
*/
namespace Utils {
void exponentialblur(QImage &img, qreal radius, bool improvedQuality = false, int transposed = 0);
};
#endif // EXPONENTIALBLUR_H

439
ui/utils/feedview.cpp Normal file
View File

@ -0,0 +1,439 @@
/*
* 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 "feedview.h"
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QDebug>
#include "messagedelegate.h"
#include "ui/models/messagefeed.h"
constexpr int maxMessageHeight = 10000;
constexpr int approximateSingleMessageHeight = 20;
constexpr int progressSize = 70;
const std::set<int> FeedView::geometryChangingRoles = {
Models::MessageFeed::Attach,
Models::MessageFeed::Text,
Models::MessageFeed::Id,
Models::MessageFeed::Error,
Models::MessageFeed::Date
};
FeedView::FeedView(QWidget* parent):
QAbstractItemView(parent),
hints(),
vo(0),
specialDelegate(false),
specialModel(false),
clearWidgetsMode(false),
modelState(Models::MessageFeed::complete),
progress()
{
horizontalScrollBar()->setRange(0, 0);
verticalScrollBar()->setSingleStep(approximateSingleMessageHeight);
setMouseTracking(true);
setSelectionBehavior(SelectItems);
// viewport()->setAttribute(Qt::WA_Hover, true);
progress.setParent(viewport());
progress.resize(progressSize, progressSize);
}
FeedView::~FeedView()
{
}
QModelIndex FeedView::indexAt(const QPoint& point) const
{
int32_t vh = viewport()->height();
uint32_t y = vh - point.y() + vo;
for (std::deque<Hint>::size_type i = 0; i < hints.size(); ++i) {
const Hint& hint = hints[i];
if (y <= hint.offset + hint.height) {
if (y > hint.offset) {
return model()->index(i, 0, rootIndex());
} else {
break;
}
}
}
return QModelIndex();
}
void FeedView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint)
{
}
QRect FeedView::visualRect(const QModelIndex& index) const
{
unsigned int row = index.row();
if (!index.isValid() || row >= hints.size()) {
qDebug() << "visualRect for" << row;
return QRect();
} else {
const Hint& hint = hints.at(row);
const QWidget* vp = viewport();
return QRect(0, vp->height() - hint.height - hint.offset + vo, vp->width(), hint.height);
}
}
int FeedView::horizontalOffset() const
{
return 0;
}
bool FeedView::isIndexHidden(const QModelIndex& index) const
{
return false;
}
QModelIndex FeedView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
return QModelIndex();
}
void FeedView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
{
}
int FeedView::verticalOffset() const
{
return vo;
}
QRegion FeedView::visualRegionForSelection(const QItemSelection& selection) const
{
return QRegion();
}
void FeedView::rowsInserted(const QModelIndex& parent, int start, int end)
{
QAbstractItemView::rowsInserted(parent, start, end);
scheduleDelayedItemsLayout();
}
void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
{
if (specialDelegate) {
for (int role : roles) {
if (geometryChangingRoles.count(role) != 0) {
scheduleDelayedItemsLayout(); //to recalculate layout only if there are some geometry changing modifications
break;
}
}
}
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
}
void FeedView::updateGeometries()
{
qDebug() << "updateGeometries";
QScrollBar* bar = verticalScrollBar();
const QStyle* st = style();
const QAbstractItemModel* m = model();
QSize layoutBounds = maximumViewportSize();
QStyleOptionViewItem option = viewOptions();
option.rect.setHeight(maxMessageHeight);
option.rect.setWidth(layoutBounds.width());
int frameAroundContents = 0;
int verticalScrollBarExtent = st->pixelMetric(QStyle::PM_ScrollBarExtent, 0, bar);
bool layedOut = false;
if (verticalScrollBarExtent != 0 && verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded && m->rowCount() * approximateSingleMessageHeight < layoutBounds.height()) {
hints.clear();
layedOut = tryToCalculateGeometriesWithNoScrollbars(option, m, layoutBounds.height());
}
if (layedOut) {
bar->setRange(0, 0);
vo = 0;
} else {
int verticalMargin = 0;
if (st->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) {
frameAroundContents = st->pixelMetric(QStyle::PM_DefaultFrameWidth) * 2;
}
if (verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded) {
verticalMargin = verticalScrollBarExtent + frameAroundContents;
}
layoutBounds.rwidth() -= verticalMargin;
option.features |= QStyleOptionViewItem::WrapText;
option.rect.setWidth(layoutBounds.width());
hints.clear();
uint32_t previousOffset = 0;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
int totalHeight = previousOffset - layoutBounds.height();
if (modelState != Models::MessageFeed::complete) {
totalHeight += progressSize;
}
vo = qMax(qMin(vo, totalHeight), 0);
bar->setRange(0, totalHeight);
bar->setPageStep(layoutBounds.height());
bar->setValue(totalHeight - vo);
}
positionProgress();
if (specialDelegate) {
clearWidgetsMode = true;
}
QAbstractItemView::updateGeometries();
}
bool FeedView::tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* m, uint32_t totalHeight)
{
uint32_t previousOffset = 0;
bool success = true;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
if (previousOffset + height > totalHeight) {
success = false;
break;
}
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
return success;
}
void FeedView::paintEvent(QPaintEvent* event)
{
//qDebug() << "paint" << event->rect();
const QAbstractItemModel* m = model();
QWidget* vp = viewport();
QRect zone = event->rect().translated(0, -vo);
uint32_t vph = vp->height();
int32_t y1 = zone.y();
int32_t y2 = y1 + zone.height();
bool inZone = false;
std::deque<QModelIndex> toRener;
for (std::deque<Hint>::size_type i = 0; i < hints.size(); ++i) {
const Hint& hint = hints[i];
int32_t relativeY1 = vph - hint.offset - hint.height;
if (!inZone) {
if (y2 > relativeY1) {
inZone = true;
}
}
if (inZone) {
toRener.emplace_back(m->index(i, 0, rootIndex()));
}
if (y1 > relativeY1) {
break;
}
}
QPainter painter(vp);
QStyleOptionViewItem option = viewOptions();
option.features = QStyleOptionViewItem::WrapText;
QPoint cursor = vp->mapFromGlobal(QCursor::pos());
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
if (clearWidgetsMode) {
del->beginClearWidgets();
}
}
for (const QModelIndex& index : toRener) {
option.rect = visualRect(index);
bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor);
option.state.setFlag(QStyle::State_MouseOver, mouseOver);
itemDelegate(index)->paint(&painter, option, index);
}
if (clearWidgetsMode && specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
del->endClearWidgets();
clearWidgetsMode = false;
}
if (event->rect().height() == vp->height()) {
// draw the blurred drop shadow...
}
}
void FeedView::verticalScrollbarValueChanged(int value)
{
vo = verticalScrollBar()->maximum() - value;
positionProgress();
if (specialDelegate) {
clearWidgetsMode = true;
}
if (modelState == Models::MessageFeed::incomplete && value < progressSize) {
model()->fetchMore(rootIndex());
}
QAbstractItemView::verticalScrollbarValueChanged(vo);
}
void FeedView::mouseMoveEvent(QMouseEvent* event)
{
if (!isVisible()) {
return;
}
QAbstractItemView::mouseMoveEvent(event);
}
void FeedView::resizeEvent(QResizeEvent* event)
{
QAbstractItemView::resizeEvent(event);
positionProgress();
emit resized();
}
void FeedView::positionProgress()
{
QSize layoutBounds = maximumViewportSize();
int progressPosition = layoutBounds.height() - progressSize;
std::deque<Hint>::size_type size = hints.size();
if (size > 0) {
const Hint& hint = hints[size - 1];
progressPosition -= hint.offset + hint.height;
}
progressPosition += vo;
progressPosition = qMin(progressPosition, 0);
progress.move((width() - progressSize) / 2, progressPosition);
}
QFont FeedView::getFont() const
{
return viewOptions().font;
}
void FeedView::setItemDelegate(QAbstractItemDelegate* delegate)
{
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
disconnect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
disconnect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
}
QAbstractItemView::setItemDelegate(delegate);
MessageDelegate* del = dynamic_cast<MessageDelegate*>(delegate);
if (del) {
specialDelegate = true;
connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
} else {
specialDelegate = false;
}
}
void FeedView::setModel(QAbstractItemModel* p_model)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
disconnect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange);
}
QAbstractItemView::setModel(p_model);
Models::MessageFeed* feed = dynamic_cast<Models::MessageFeed*>(p_model);
if (feed) {
onModelSyncStateChange(feed->getSyncState());
specialModel = true;
connect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange);
} else {
onModelSyncStateChange(Models::MessageFeed::complete);
specialModel = false;
}
}
void FeedView::onMessageButtonPushed(const QString& messageId, bool download)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
if (download) {
feed->downloadAttachment(messageId);
} else {
feed->uploadAttachment(messageId);
}
}
}
void FeedView::onMessageInvalidPath(const QString& messageId)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
feed->reportLocalPathInvalid(messageId);
}
}
void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state)
{
bool needToUpdateGeometry = false;
if (modelState != state) {
if (state == Models::MessageFeed::complete || modelState == Models::MessageFeed::complete) {
needToUpdateGeometry = true;
}
modelState = state;
if (state == Models::MessageFeed::syncing) {
progress.show();
progress.start();
} else {
progress.stop();
progress.hide();
}
}
if (needToUpdateGeometry) {
scheduleDelayedItemsLayout();
}
}

95
ui/utils/feedview.h Normal file
View File

@ -0,0 +1,95 @@
/*
* 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 FEEDVIEW_H
#define FEEDVIEW_H
#include <QAbstractItemView>
#include <deque>
#include <set>
#include <ui/models/messagefeed.h>
#include "progress.h"
/**
* @todo write docs
*/
class FeedView : public QAbstractItemView
{
Q_OBJECT
public:
FeedView(QWidget* parent = nullptr);
~FeedView();
QModelIndex indexAt(const QPoint & point) const override;
void scrollTo(const QModelIndex & index, QAbstractItemView::ScrollHint hint) override;
QRect visualRect(const QModelIndex & index) const override;
bool isIndexHidden(const QModelIndex & index) const override;
QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
void setSelection(const QRect & rect, QItemSelectionModel::SelectionFlags command) override;
QRegion visualRegionForSelection(const QItemSelection & selection) const override;
void setItemDelegate(QAbstractItemDelegate* delegate);
void setModel(QAbstractItemModel * model) override;
QFont getFont() const;
signals:
void resized();
public slots:
protected slots:
void rowsInserted(const QModelIndex & parent, int start, int end) override;
void verticalScrollbarValueChanged(int value) override;
void dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector<int> & roles) override;
void onMessageButtonPushed(const QString& messageId, bool download);
void onMessageInvalidPath(const QString& messageId);
void onModelSyncStateChange(Models::MessageFeed::SyncState state);
protected:
int verticalOffset() const override;
int horizontalOffset() const override;
void paintEvent(QPaintEvent * event) override;
void updateGeometries() override;
void mouseMoveEvent(QMouseEvent * event) override;
void resizeEvent(QResizeEvent * event) override;
private:
bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight);
void positionProgress();
private:
struct Hint {
bool dirty;
uint32_t offset;
uint32_t height;
};
std::deque<Hint> hints;
int vo;
bool specialDelegate;
bool specialModel;
bool clearWidgetsMode;
Models::MessageFeed::SyncState modelState;
Progress progress;
static const std::set<int> geometryChangingRoles;
};
#endif //FEEDVIEW_H

View File

@ -0,0 +1,549 @@
/*
* 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 <QDebug>
#include <QPainter>
#include <QApplication>
#include <QMouseEvent>
#include "messagedelegate.h"
#include "ui/models/messagefeed.h"
constexpr int avatarHeight = 50;
constexpr int margin = 6;
constexpr int textMargin = 2;
constexpr int statusIconSize = 16;
constexpr int maxAttachmentHeight = 500;
MessageDelegate::MessageDelegate(QObject* parent):
QStyledItemDelegate(parent),
bodyFont(),
nickFont(),
dateFont(),
bodyMetrics(bodyFont),
nickMetrics(nickFont),
dateMetrics(dateFont),
buttonHeight(0),
barHeight(0),
buttons(new std::map<QString, FeedButton*>()),
bars(new std::map<QString, QProgressBar*>()),
statusIcons(new std::map<QString, QLabel*>()),
bodies(new std::map<QString, QLabel*>()),
idsToKeep(new std::set<QString>()),
clearingWidgets(false)
{
QPushButton btn;
buttonHeight = btn.sizeHint().height();
QProgressBar bar;
barHeight = bar.sizeHint().height();
}
MessageDelegate::~MessageDelegate()
{
for (const std::pair<const QString, FeedButton*>& pair: *buttons){
delete pair.second;
}
for (const std::pair<const QString, QProgressBar*>& pair: *bars){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *statusIcons){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *bodies){
delete pair.second;
}
delete idsToKeep;
delete buttons;
delete bars;
delete bodies;
}
void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
if (!vi.isValid()) {
return;
}
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
if (option.state & QStyle::State_MouseOver) {
painter->fillRect(option.rect, option.palette.brush(QPalette::Inactive, QPalette::Highlight));
}
QIcon icon(data.avatar);
if (data.sentByMe) {
painter->drawPixmap(option.rect.width() - avatarHeight - margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight));
} else {
painter->drawPixmap(margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight));
}
QStyleOptionViewItem opt = option;
QRect messageRect = option.rect.adjusted(margin, margin / 2, -(avatarHeight + 2 * margin), -margin / 2);
if (!data.sentByMe) {
opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop;
messageRect.adjust(avatarHeight + margin, 0, avatarHeight + margin, 0);
} else {
opt.displayAlignment = Qt::AlignRight | Qt::AlignTop;
}
opt.rect = messageRect;
QSize messageSize(0, 0);
QSize bodySize(0, 0);
if (data.text.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size();
bodySize = messageSize;
}
messageSize.rheight() += nickMetrics.lineSpacing();
messageSize.rheight() += dateMetrics.height();
if (messageSize.width() < opt.rect.width()) {
QSize senderSize = nickMetrics.boundingRect(messageRect, 0, data.sender).size();
if (senderSize.width() > messageSize.width()) {
messageSize.setWidth(senderSize.width());
}
} else {
messageSize.setWidth(opt.rect.width());
}
QRect rect;
painter->setFont(nickFont);
painter->drawText(opt.rect, opt.displayAlignment, data.sender, &rect);
opt.rect.adjust(0, rect.height() + textMargin, 0, 0);
painter->save();
switch (data.attach.state) {
case Models::none:
clearHelperWidget(data); //i can't imagine the situation where it's gonna be needed
break; //but it's a possible performance problem
case Models::uploading:
case Models::downloading:
paintBar(getBar(data), painter, data.sentByMe, opt);
break;
case Models::remote:
case Models::local:
paintButton(getButton(data), painter, data.sentByMe, opt);
break;
case Models::ready:
clearHelperWidget(data);
paintPreview(data, painter, opt);
break;
case Models::errorDownload:
case Models::errorUpload:
break;
}
painter->restore();
int messageLeft = INT16_MAX;
QWidget* vp = static_cast<QWidget*>(painter->device());
if (data.text.size() > 0) {
QLabel* body = getBody(data);
body->setParent(vp);
body->setMaximumWidth(bodySize.width());
body->setMinimumWidth(bodySize.width());
body->setAlignment(opt.displayAlignment);
messageLeft = opt.rect.x();
if (data.sentByMe) {
messageLeft = opt.rect.topRight().x() - bodySize.width();
}
body->move(messageLeft, opt.rect.y());
body->show();
opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0);
}
painter->setFont(dateFont);
QColor q = painter->pen().color();
q.setAlpha(180);
painter->setPen(q);
painter->drawText(opt.rect, opt.displayAlignment, data.date.toLocalTime().toString(), &rect);
if (data.sentByMe) {
if (messageLeft > rect.x() - statusIconSize - margin) {
messageLeft = rect.x() - statusIconSize - margin;
}
QLabel* statusIcon = getStatusIcon(data);
statusIcon->setParent(vp);
statusIcon->move(messageLeft, opt.rect.y());
statusIcon->show();
opt.rect.adjust(0, statusIconSize + textMargin, 0, 0);
}
painter->restore();
if (clearingWidgets) {
idsToKeep->insert(data.id);
}
}
QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QRect messageRect = option.rect.adjusted(0, margin / 2, -(avatarHeight + 3 * margin), -margin / 2);
QStyleOptionViewItem opt = option;
opt.rect = messageRect;
QVariant va = index.data(Models::MessageFeed::Attach);
Models::Attachment attach = qvariant_cast<Models::Attachment>(va);
QString body = index.data(Models::MessageFeed::Text).toString();
QSize messageSize(0, 0);
if (body.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, body).size();
messageSize.rheight() += textMargin;
}
switch (attach.state) {
case Models::none:
break;
case Models::uploading:
case Models::downloading:
messageSize.rheight() += barHeight + textMargin;
break;
case Models::remote:
case Models::local:
messageSize.rheight() += buttonHeight + textMargin;
break;
case Models::ready:
messageSize.rheight() += calculateAttachSize(attach.localPath, messageRect).height() + textMargin;
break;
case Models::errorDownload:
case Models::errorUpload:
break;
}
messageSize.rheight() += nickMetrics.lineSpacing();
messageSize.rheight() += textMargin;
messageSize.rheight() += dateMetrics.height() > statusIconSize ? dateMetrics.height() : statusIconSize;
if (messageSize.height() < avatarHeight) {
messageSize.setHeight(avatarHeight);
}
messageSize.rheight() += margin;
return messageSize;
}
void MessageDelegate::initializeFonts(const QFont& font)
{
bodyFont = font;
nickFont = font;
dateFont = font;
nickFont.setBold(true);
float ndps = nickFont.pointSizeF();
if (ndps != -1) {
nickFont.setPointSizeF(ndps * 1.2);
} else {
nickFont.setPointSize(nickFont.pointSize() + 2);
}
dateFont.setItalic(true);
float dps = dateFont.pointSizeF();
if (dps != -1) {
dateFont.setPointSizeF(dps * 0.8);
} else {
dateFont.setPointSize(dateFont.pointSize() - 2);
}
bodyMetrics = QFontMetrics(bodyFont);
nickMetrics = QFontMetrics(nickFont);
dateMetrics = QFontMetrics(dateFont);
}
bool MessageDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index)
{
//qDebug() << event->type();
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
void MessageDelegate::paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const
{
QPoint start;
if (sentByMe) {
start = {option.rect.width() - btn->width(), option.rect.top()};
} else {
start = option.rect.topLeft();
}
QWidget* vp = static_cast<QWidget*>(painter->device());
btn->setParent(vp);
btn->move(start);
btn->show();
option.rect.adjust(0, buttonHeight + textMargin, 0, 0);
}
void MessageDelegate::paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const
{
QPoint start = option.rect.topLeft();
bar->resize(option.rect.width(), barHeight);
painter->translate(start);
bar->render(painter, QPoint(), QRegion(), QWidget::DrawChildren);
option.rect.adjust(0, barHeight + textMargin, 0, 0);
}
void MessageDelegate::paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const
{
Shared::Global::FileInfo info = Shared::Global::getFileInfo(data.attach.localPath);
if (info.preview == Shared::Global::FileInfo::Preview::picture) {
QSize size = constrainAttachSize(info.size, option.rect.size());
QPoint start;
if (data.sentByMe) {
start = {option.rect.width() - size.width(), option.rect.top()};
start.rx() += margin;
} else {
start = option.rect.topLeft();
}
QImage img(data.attach.localPath);
if (img.isNull()) {
emit invalidPath(data.id);
} else {
painter->drawImage(QRect(start, size), img);
}
option.rect.adjust(0, size.height() + textMargin, 0, 0);
}
}
QPushButton * MessageDelegate::getButton(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
FeedButton* result = 0;
if (itr != buttons->end()) {
if (
(data.attach.state == Models::remote && itr->second->download) ||
(data.attach.state == Models::local && !itr->second->download)
) {
result = itr->second;
} else {
delete itr->second;
buttons->erase(itr);
}
} else {
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
if (barItr != bars->end()) {
delete barItr->second;
bars->erase(barItr);
}
}
if (result == 0) {
result = new FeedButton();
result->messageId = data.id;
if (data.attach.state == Models::remote) {
result->setText(QCoreApplication::translate("MessageLine", "Download"));
result->download = true;
} else {
result->setText(QCoreApplication::translate("MessageLine", "Upload"));
result->download = false;
}
buttons->insert(std::make_pair(data.id, result));
connect(result, &QPushButton::clicked, this, &MessageDelegate::onButtonPushed);
}
return result;
}
QProgressBar * MessageDelegate::getBar(const Models::FeedItem& data) const
{
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
QProgressBar* result = 0;
if (barItr != bars->end()) {
result = barItr->second;
} else {
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
if (itr != buttons->end()) {
delete itr->second;
buttons->erase(itr);
}
}
if (result == 0) {
result = new QProgressBar();
result->setRange(0, 100);
bars->insert(std::make_pair(data.id, result));
}
result->setValue(data.attach.progress * 100);
return result;
}
QLabel * MessageDelegate::getStatusIcon(const Models::FeedItem& data) const
{
std::map<QString, QLabel*>::const_iterator itr = statusIcons->find(data.id);
QLabel* result = 0;
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(data.state)]));
QString tt = Shared::Global::getName(data.state);
if (data.state == Shared::Message::State::error) {
if (data.error > 0) {
tt += ": " + data.error;
}
}
if (itr != statusIcons->end()) {
result = itr->second;
if (result->toolTip() != tt) { //If i just assign pixmap every time unconditionally
result->setPixmap(q.pixmap(statusIconSize)); //it involves into an infinite cycle of repaint
result->setToolTip(tt); //may be it's better to subclass and store last condition in int?
}
} else {
result = new QLabel();
statusIcons->insert(std::make_pair(data.id, result));
result->setPixmap(q.pixmap(statusIconSize));
result->setToolTip(tt);
}
result->setToolTip(tt);
//result->setText(std::to_string((int)data.state).c_str());
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;
}
void MessageDelegate::endClearWidgets()
{
if (clearingWidgets) {
std::set<QString> toRemoveButtons;
std::set<QString> toRemoveBars;
std::set<QString> toRemoveIcons;
std::set<QString> toRemoveBodies;
for (const std::pair<const QString, FeedButton*>& pair: *buttons) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveButtons.insert(pair.first);
}
}
for (const std::pair<const QString, QProgressBar*>& pair: *bars) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveBars.insert(pair.first);
}
}
for (const std::pair<const QString, QLabel*>& pair: *statusIcons) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveIcons.insert(pair.first);
}
}
for (const std::pair<const QString, QLabel*>& pair: *bodies) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveBodies.insert(pair.first);
}
}
for (const QString& key : toRemoveButtons) {
buttons->erase(key);
}
for (const QString& key : toRemoveBars) {
bars->erase(key);
}
for (const QString& key : toRemoveIcons) {
statusIcons->erase(key);
}
for (const QString& key : toRemoveBodies) {
bodies->erase(key);
}
idsToKeep->clear();
clearingWidgets = false;
}
}
void MessageDelegate::onButtonPushed() const
{
FeedButton* btn = static_cast<FeedButton*>(sender());
emit buttonPushed(btn->messageId, btn->download);
}
void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
if (itr != buttons->end()) {
delete itr->second;
buttons->erase(itr);
} else {
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
if (barItr != bars->end()) {
delete barItr->second;
bars->erase(barItr);
}
}
}
QSize MessageDelegate::calculateAttachSize(const QString& path, const QRect& bounds) const
{
Shared::Global::FileInfo info = Shared::Global::getFileInfo(path);
return constrainAttachSize(info.size, bounds.size());
}
QSize MessageDelegate::constrainAttachSize(QSize src, QSize bounds) const
{
bounds.setHeight(maxAttachmentHeight);
if (src.width() > bounds.width() || src.height() > bounds.height()) {
src.scale(bounds, Qt::KeepAspectRatio);
}
return src;
}
// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
// {
//
// }

102
ui/utils/messagedelegate.h Normal file
View File

@ -0,0 +1,102 @@
/*
* 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 MESSAGEDELEGATE_H
#define MESSAGEDELEGATE_H
#include <map>
#include <set>
#include <QStyledItemDelegate>
#include <QStyleOptionButton>
#include <QFont>
#include <QFontMetrics>
#include <QPushButton>
#include <QProgressBar>
#include <QLabel>
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
namespace Models {
struct FeedItem;
};
class MessageDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MessageDelegate(QObject *parent = nullptr);
~MessageDelegate();
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
//void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
void initializeFonts(const QFont& font);
bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override;
void endClearWidgets();
void beginClearWidgets();
signals:
void buttonPushed(const QString& messageId, bool download) const;
void invalidPath(const QString& messageId) const;
protected:
void paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
QPushButton* getButton(const Models::FeedItem& data) const;
QProgressBar* getBar(const Models::FeedItem& data) const;
QLabel* getStatusIcon(const Models::FeedItem& data) const;
QLabel* getBody(const Models::FeedItem& data) const;
void clearHelperWidget(const Models::FeedItem& data) const;
QSize calculateAttachSize(const QString& path, const QRect& bounds) const;
QSize constrainAttachSize(QSize src, QSize bounds) const;
protected slots:
void onButtonPushed() const;
private:
class FeedButton : public QPushButton {
public:
QString messageId;
bool download;
};
QFont bodyFont;
QFont nickFont;
QFont dateFont;
QFontMetrics bodyMetrics;
QFontMetrics nickMetrics;
QFontMetrics dateMetrics;
int buttonHeight;
int barHeight;
std::map<QString, FeedButton*>* buttons;
std::map<QString, QProgressBar*>* bars;
std::map<QString, QLabel*>* statusIcons;
std::map<QString, QLabel*>* bodies;
std::set<QString>* idsToKeep;
bool clearingWidgets;
};
#endif // MESSAGEDELEGATE_H

View File

@ -0,0 +1,91 @@
/*
* 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 "shadowoverlay.h"
ShadowOverlay::ShadowOverlay(unsigned int r, unsigned int t, const QColor& c, QWidget* parent):
QWidget(parent),
top(false),
right(false),
bottom(false),
left(false),
thickness(t),
radius(r),
color(c),
shadow(1, 1, QImage::Format_ARGB32_Premultiplied)
{
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_TransparentForMouseEvents);
}
void ShadowOverlay::paintEvent(QPaintEvent* event)
{
QWidget::paintEvent(event);
QPainter painter(this);
painter.drawImage(0, 0, shadow);
}
void ShadowOverlay::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
updateImage();
}
void ShadowOverlay::updateImage()
{
int w = width();
int h = height();
shadow = QImage({w, h + int(thickness)}, QImage::Format_ARGB32_Premultiplied);
shadow.fill(0);
QPainter tmpPainter(&shadow);
tmpPainter.setCompositionMode(QPainter::CompositionMode_Source);
if (top) {
QRectF shadow(0, 0, w, thickness);
tmpPainter.fillRect(shadow, color);
}
if (right) {
QRectF shadow(w - thickness, 0, thickness, h);
tmpPainter.fillRect(shadow, color);
}
if (bottom) {
QRectF shadow(0, h - thickness, w, thickness * 2); //i have no idea why, but it leaves some unpainted stripe without some spare space
tmpPainter.fillRect(shadow, color);
}
if (left) {
QRectF shadow(0, 0, thickness, h);
tmpPainter.fillRect(shadow, color);
}
Utils::exponentialblur(shadow, radius, false, 0);
tmpPainter.end();
}
void ShadowOverlay::setFrames(bool t, bool r, bool b, bool l)
{
top = t;
right = r;
bottom = b;
left = l;
updateImage();
update();
}

58
ui/utils/shadowoverlay.h Normal file
View File

@ -0,0 +1,58 @@
/*
* 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 SHADOWOVERLAY_H
#define SHADOWOVERLAY_H
#include <QWidget>
#include <QImage>
#include <QPainter>
#include <QColor>
#include <QPaintEvent>
#include <QResizeEvent>
#include <ui/utils/exponentialblur.h>
/**
* @todo write docs
*/
class ShadowOverlay : public QWidget {
public:
ShadowOverlay(unsigned int radius = 10, unsigned int thickness = 1, const QColor& color = Qt::black, QWidget* parent = nullptr);
void setFrames(bool top, bool right, bool bottom, bool left);
protected:
void updateImage();
void paintEvent(QPaintEvent * event) override;
void resizeEvent(QResizeEvent * event) override;
private:
bool top;
bool right;
bool bottom;
bool left;
unsigned int thickness;
unsigned int radius;
QColor color;
QImage shadow;
};
#endif // SHADOWOVERLAY_H

View File

@ -7,7 +7,7 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
# Find the QtWidgets library
find_package(Qt5Widgets CONFIG REQUIRED)
find_package(Qt5Widgets CONFIG REQUIRED COMPONENTS Widgets Core)
add_subdirectory(vcard)
@ -21,9 +21,11 @@ set(squawkWidgets_SRC
joinconference.cpp
)
# Tell CMake to create the helloworld executable
add_library(squawkWidgets ${squawkWidgets_SRC})
add_library(squawkWidgets STATIC ${squawkWidgets_SRC})
# Use the Widgets module from Qt 5.
target_link_libraries(squawkWidgets vCardUI)
target_link_libraries(squawkWidgets squawkUIUtils)
target_link_libraries(squawkWidgets Qt5::Widgets)
qt5_use_modules(squawkWidgets Core Widgets)

View File

@ -19,7 +19,7 @@
#include "chat.h"
Chat::Chat(Models::Account* acc, Models::Contact* p_contact, QWidget* parent):
Conversation(false, acc, p_contact->getJid(), "", parent),
Conversation(false, acc, p_contact, p_contact->getJid(), "", parent),
contact(p_contact)
{
setName(p_contact->getContactName());
@ -71,31 +71,14 @@ Shared::Message Chat::createMessage() const
return msg;
}
void Chat::addMessage(const Shared::Message& data)
void Chat::onMessage(const Shared::Message& data)
{
Conversation::addMessage(data);
Conversation::onMessage(data);
if (!data.getOutgoing()) { //TODO need to check if that was the last message
if (!data.getOutgoing()) {
const QString& res = data.getPenPalResource();
if (res.size() > 0) {
setPalResource(res);
}
}
}
void Chat::setName(const QString& name)
{
Conversation::setName(name);
line->setPalName(getJid(), name);
}
void Chat::setAvatar(const QString& path)
{
Conversation::setAvatar(path);
if (path.size() == 0) {
line->dropPalAvatar(contact->getJid());
} else {
line->setPalAvatar(contact->getJid(), path);
}
}

View File

@ -35,15 +35,12 @@ public:
Chat(Models::Account* acc, Models::Contact* p_contact, QWidget* parent = 0);
~Chat();
void addMessage(const Shared::Message & data) override;
void setAvatar(const QString& path) override;
protected slots:
void onContactChanged(Models::Item* item, int row, int col);
protected:
void setName(const QString & name) override;
Shared::Message createMessage() const override;
void onMessage(const Shared::Message& msg) override;
private:
void updateState();

View File

@ -18,7 +18,6 @@
#include "conversation.h"
#include "ui_conversation.h"
#include "ui/utils/dropshadoweffect.h"
#include <QDebug>
#include <QScrollBar>
@ -29,31 +28,44 @@
#include <QAbstractTextDocumentLayout>
#include <QCoreApplication>
Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, const QString pRes, QWidget* parent):
Conversation::Conversation(bool muc, Models::Account* acc, Models::Element* el, const QString pJid, const QString pRes, QWidget* parent):
QWidget(parent),
isMuc(muc),
account(acc),
element(el),
palJid(pJid),
activePalResource(pRes),
line(new MessageLine(muc)),
m_ui(new Ui::Conversation()),
ker(),
scrollResizeCatcher(),
vis(),
thread(),
statusIcon(0),
statusLabel(0),
filesLayout(0),
overlay(new QWidget()),
filesToAttach(),
scroll(down),
feed(new FeedView()),
delegate(new MessageDelegate(this)),
manualSliderChange(false),
requestingHistory(false),
everShown(false),
tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1)
tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1),
shadow(10, 1, Qt::black, this),
contextMenu(new QMenu())
{
m_ui->setupUi(this);
shadow.setFrames(true, false, true, false);
feed->setItemDelegate(delegate);
feed->setFrameShape(QFrame::NoFrame);
feed->setContextMenuPolicy(Qt::CustomContextMenu);
delegate->initializeFonts(feed->getFont());
feed->setModel(el->feed);
el->feed->incrementObservers();
m_ui->widget->layout()->addWidget(feed);
connect(el->feed, &Models::MessageFeed::newMessage, this, &Conversation::onFeedMessage);
connect(feed, &FeedView::resized, this, &Conversation::positionShadow);
connect(feed, &FeedView::customContextMenuRequested, this, &Conversation::onFeedContext);
connect(acc, &Models::Account::childChanged, this, &Conversation::onAccountChanged);
filesLayout = new FlowLayout(m_ui->filesPanel, 0);
@ -63,14 +75,7 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c
statusLabel = m_ui->statusLabel;
connect(&ker, &KeyEnterReceiver::enterPressed, this, &Conversation::onEnterPressed);
connect(&scrollResizeCatcher, &Resizer::resized, this, &Conversation::onScrollResize);
connect(&vis, &VisibilityCatcher::shown, this, &Conversation::onScrollResize);
connect(&vis, &VisibilityCatcher::hidden, this, &Conversation::onScrollResize);
connect(m_ui->sendButton, &QPushButton::clicked, this, &Conversation::onEnterPressed);
connect(line, &MessageLine::resize, this, &Conversation::onMessagesResize);
connect(line, &MessageLine::downloadFile, this, &Conversation::downloadFile);
connect(line, &MessageLine::uploadFile, this, qOverload<const Shared::Message&, const QString&>(&Conversation::sendMessage));
connect(line, &MessageLine::requestLocalFile, this, &Conversation::requestLocalFile);
connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach);
connect(m_ui->clearButton, &QPushButton::clicked, this, &Conversation::onClearButton);
connect(m_ui->messageEditor->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged,
@ -78,23 +83,43 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c
m_ui->messageEditor->installEventFilter(&ker);
QScrollBar* vs = m_ui->scrollArea->verticalScrollBar();
m_ui->scrollArea->setWidget(line);
vs->installEventFilter(&vis);
line->setAutoFillBackground(false);
if (testAttribute(Qt::WA_TranslucentBackground)) {
m_ui->scrollArea->setAutoFillBackground(false);
} else {
m_ui->scrollArea->setBackgroundRole(QPalette::Base);
//line->setAutoFillBackground(false);
//if (testAttribute(Qt::WA_TranslucentBackground)) {
//m_ui->scrollArea->setAutoFillBackground(false);
//} else {
//m_ui->scrollArea->setBackgroundRole(QPalette::Base);
//}
//line->setMyAvatarPath(acc->getAvatarPath());
//line->setMyName(acc->getName());
initializeOverlay();
}
Conversation::~Conversation()
{
delete contextMenu;
element->feed->decrementObservers();
}
void Conversation::onAccountChanged(Models::Item* item, int row, int col)
{
if (item == account) {
if (col == 2 && account->getState() == Shared::ConnectionState::connected) { //to request the history when we're back online after reconnect
//if (!requestingHistory) {
//requestingHistory = true;
//line->showBusyIndicator();
//emit requestArchive("");
//scroll = down;
//}
}
}
}
connect(vs, &QScrollBar::valueChanged, this, &Conversation::onSliderValueChanged);
m_ui->scrollArea->installEventFilter(&scrollResizeCatcher);
line->setMyAvatarPath(acc->getAvatarPath());
line->setMyName(acc->getName());
void Conversation::initializeOverlay()
{
QGridLayout* gr = static_cast<QGridLayout*>(layout());
QLabel* progressLabel = new QLabel(tr("Drop files here to attach them to your message"));
gr->addWidget(overlay, 0, 0, 2, 1);
@ -115,36 +140,6 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c
nl->addWidget(progressLabel);
nl->addStretch();
overlay->hide();
applyVisualEffects();
}
Conversation::~Conversation()
{
}
void Conversation::onAccountChanged(Models::Item* item, int row, int col)
{
if (item == account) {
if (col == 2 && account->getState() == Shared::ConnectionState::connected) {
if (!requestingHistory) {
requestingHistory = true;
line->showBusyIndicator();
emit requestArchive("");
scroll = down;
}
}
}
}
void Conversation::applyVisualEffects()
{
DropShadowEffect *e1 = new DropShadowEffect;
e1->setBlurRadius(10);
e1->setColor(Qt::black);
e1->setThickness(1);
e1->setFrame(true, false, true, false);
m_ui->scrollArea->setGraphicsEffect(e1);
}
void Conversation::setName(const QString& name)
@ -163,22 +158,6 @@ QString Conversation::getJid() const
return palJid;
}
void Conversation::addMessage(const Shared::Message& data)
{
int pos = m_ui->scrollArea->verticalScrollBar()->sliderPosition();
int max = m_ui->scrollArea->verticalScrollBar()->maximum();
MessageLine::Position place = line->message(data);
if (place == MessageLine::invalid) {
return;
}
}
void Conversation::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
line->changeMessage(id, data);
}
KeyEnterReceiver::KeyEnterReceiver(QObject* parent): QObject(parent), ownEvent(false) {}
bool KeyEnterReceiver::eventFilter(QObject* obj, QEvent* event)
@ -226,93 +205,19 @@ void Conversation::onEnterPressed()
m_ui->messageEditor->clear();
Shared::Message msg = createMessage();
msg.setBody(body);
addMessage(msg);
emit sendMessage(msg);
}
if (filesToAttach.size() > 0) {
for (Badge* badge : filesToAttach) {
Shared::Message msg = createMessage();
line->appendMessageWithUpload(msg, badge->id);
usleep(1000); //this is required for the messages not to have equal time when appending into messageline
msg.setAttachPath(badge->id);
element->feed->registerUpload(msg.getId());
emit sendMessage(msg);
}
clearAttachedFiles();
}
}
void Conversation::appendMessageWithUpload(const Shared::Message& data, const QString& path)
{
line->appendMessageWithUploadNoSiganl(data, path);
}
void Conversation::onMessagesResize(int amount)
{
manualSliderChange = true;
switch (scroll) {
case down:
m_ui->scrollArea->verticalScrollBar()->setValue(m_ui->scrollArea->verticalScrollBar()->maximum());
break;
case keep: {
int max = m_ui->scrollArea->verticalScrollBar()->maximum();
int value = m_ui->scrollArea->verticalScrollBar()->value() + amount;
m_ui->scrollArea->verticalScrollBar()->setValue(value);
if (value == max) {
scroll = down;
} else {
scroll = nothing;
}
}
break;
default:
break;
}
manualSliderChange = false;
}
void Conversation::onSliderValueChanged(int value)
{
if (!manualSliderChange) {
if (value == m_ui->scrollArea->verticalScrollBar()->maximum()) {
scroll = down;
} else {
if (!requestingHistory && value == 0) {
requestingHistory = true;
line->showBusyIndicator();
emit requestArchive(line->firstMessageId());
scroll = keep;
} else {
scroll = nothing;
}
}
}
}
void Conversation::responseArchive(const std::list<Shared::Message> list)
{
requestingHistory = false;
scroll = keep;
line->hideBusyIndicator();
for (std::list<Shared::Message>::const_iterator itr = list.begin(), end = list.end(); itr != end; ++itr) {
addMessage(*itr);
}
}
void Conversation::showEvent(QShowEvent* event)
{
if (!everShown) {
everShown = true;
line->showBusyIndicator();
requestingHistory = true;
scroll = keep;
emit requestArchive(line->firstMessageId());
}
emit shown();
QWidget::showEvent(event);
}
void Conversation::onAttach()
{
QFileDialog* d = new QFileDialog(this, tr("Chose a file to send"));
@ -340,34 +245,6 @@ void Conversation::setStatus(const QString& status)
statusLabel->setText(Shared::processMessageBody(status));
}
void Conversation::onScrollResize()
{
if (everShown) {
int size = m_ui->scrollArea->width();
QScrollBar* bar = m_ui->scrollArea->verticalScrollBar();
if (bar->isVisible() && !tsb) {
size -= bar->width();
}
line->setMaximumWidth(size);
}
}
void Conversation::responseFileProgress(const QString& messageId, qreal progress)
{
line->fileProgress(messageId, progress);
}
void Conversation::fileError(const QString& messageId, const QString& error)
{
line->fileError(messageId, error);
}
void Conversation::responseLocalFile(const QString& messageId, const QString& path)
{
line->responseLocalFile(messageId, path);
}
Models::Roster::ElId Conversation::getId() const
{
return {getAccount(), getJid()};
@ -444,7 +321,7 @@ void Conversation::onTextEditDocSizeChanged(const QSizeF& size)
void Conversation::setFeedFrames(bool top, bool right, bool bottom, bool left)
{
static_cast<DropShadowEffect*>(m_ui->scrollArea->graphicsEffect())->setFrame(top, right, bottom, left);
shadow.setFrames(top, right, bottom, left);
}
void Conversation::dragEnterEvent(QDragEnterEvent* event)
@ -504,21 +381,55 @@ Shared::Message Conversation::createMessage() const
return msg;
}
bool VisibilityCatcher::eventFilter(QObject* obj, QEvent* event)
void Conversation::onFeedMessage(const Shared::Message& msg)
{
if (event->type() == QEvent::Show) {
emit shown();
}
if (event->type() == QEvent::Hide) {
emit hidden();
}
return false;
this->onMessage(msg);
}
VisibilityCatcher::VisibilityCatcher(QWidget* parent):
QObject(parent)
void Conversation::onMessage(const Shared::Message& msg)
{
if (!msg.getForwarded()) {
QApplication::alert(this);
if (window()->windowState().testFlag(Qt::WindowMinimized)) {
emit notifyableMessage(getAccount(), msg);
}
}
}
void Conversation::positionShadow()
{
int w = width();
int h = feed->height();
shadow.resize(w, h);
shadow.move(feed->pos());
shadow.raise();
}
void Conversation::onFeedContext(const QPoint& pos)
{
QModelIndex index = feed->indexAt(pos);
if (index.isValid()) {
Shared::Message* item = static_cast<Shared::Message*>(index.internalPointer());
contextMenu->clear();
bool showMenu = false;
QString path = item->getAttachPath();
if (path.size() > 0) {
showMenu = true;
QAction* open = contextMenu->addAction(Shared::icon("document-preview"), tr("Open"));
connect(open, &QAction::triggered, [path]() {
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
QAction* show = contextMenu->addAction(Shared::icon("folder"), tr("Show in folder"));
connect(show, &QAction::triggered, [path]() {
Shared::Global::highlightInFileManager(path);
});
}
if (showMenu) {
contextMenu->popup(feed->viewport()->mapToGlobal(pos));
}
}
}

View File

@ -24,15 +24,20 @@
#include <QMap>
#include <QMimeData>
#include <QFileInfo>
#include <QGraphicsOpacityEffect>
#include <QMenu>
#include <QAction>
#include <QDesktopServices>
#include "shared/message.h"
#include "order.h"
#include "ui/models/account.h"
#include "ui/models/roster.h"
#include "ui/utils/messageline.h"
#include "ui/utils/resizer.h"
#include "ui/utils/flowlayout.h"
#include "ui/utils/badge.h"
#include "ui/utils/feedview.h"
#include "ui/utils/messagedelegate.h"
#include "ui/utils/shadowoverlay.h"
#include "shared/icons.h"
#include "shared/utils.h"
@ -54,54 +59,32 @@ signals:
void enterPressed();
};
class VisibilityCatcher : public QObject {
Q_OBJECT
public:
VisibilityCatcher(QWidget* parent = nullptr);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
signals:
void hidden();
void shown();
};
class Conversation : public QWidget
{
Q_OBJECT
public:
Conversation(bool muc, Models::Account* acc, const QString pJid, const QString pRes, QWidget* parent = 0);
Conversation(bool muc, Models::Account* acc, Models::Element* el, const QString pJid, const QString pRes, QWidget* parent = 0);
~Conversation();
QString getJid() const;
QString getAccount() const;
QString getPalResource() const;
Models::Roster::ElId getId() const;
virtual void addMessage(const Shared::Message& data);
void setPalResource(const QString& res);
void responseArchive(const std::list<Shared::Message> list);
void showEvent(QShowEvent * event) override;
void responseLocalFile(const QString& messageId, const QString& path);
void fileError(const QString& messageId, const QString& error);
void responseFileProgress(const QString& messageId, qreal progress);
virtual void setAvatar(const QString& path);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void setFeedFrames(bool top, bool right, bool bottom, bool left);
virtual void appendMessageWithUpload(const Shared::Message& data, const QString& path);
signals:
void sendMessage(const Shared::Message& message);
void sendMessage(const Shared::Message& message, const QString& path);
void requestArchive(const QString& before);
void shown();
void requestLocalFile(const QString& messageId, const QString& url);
void downloadFile(const QString& messageId, const QString& url);
void notifyableMessage(const QString& account, const Shared::Message& msg);
protected:
virtual void setName(const QString& name);
void applyVisualEffects();
virtual Shared::Message createMessage() const;
void setStatus(const QString& status);
void addAttachedFile(const QString& path);
@ -110,47 +93,44 @@ protected:
void dragEnterEvent(QDragEnterEvent* event) override;
void dragLeaveEvent(QDragLeaveEvent* event) override;
void dropEvent(QDropEvent* event) override;
void initializeOverlay();
virtual void onMessage(const Shared::Message& msg);
protected slots:
void onEnterPressed();
void onMessagesResize(int amount);
void onSliderValueChanged(int value);
void onAttach();
void onFileSelected();
void onScrollResize();
void onBadgeClose();
void onClearButton();
void onTextEditDocSizeChanged(const QSizeF& size);
void onAccountChanged(Models::Item* item, int row, int col);
void onFeedMessage(const Shared::Message& msg);
void positionShadow();
void onFeedContext(const QPoint &pos);
public:
const bool isMuc;
protected:
enum Scroll {
nothing,
keep,
down
};
Models::Account* account;
Models::Element* element;
QString palJid;
QString activePalResource;
MessageLine* line;
QScopedPointer<Ui::Conversation> m_ui;
KeyEnterReceiver ker;
Resizer scrollResizeCatcher;
VisibilityCatcher vis;
QString thread;
QLabel* statusIcon;
QLabel* statusLabel;
FlowLayout* filesLayout;
QWidget* overlay;
W::Order<Badge*, Badge::Comparator> filesToAttach;
Scroll scroll;
FeedView* feed;
MessageDelegate* delegate;
bool manualSliderChange;
bool requestingHistory;
bool everShown;
bool tsb; //transient scroll bars
ShadowOverlay shadow;
QMenu* contextMenu;
};
#endif // CONVERSATION_H

View File

@ -214,62 +214,8 @@
</layout>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="midLineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>520</width>
<height>385</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<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>
</layout>
</widget>
</widget>
</item>
</layout>
<zorder>scrollArea</zorder>
<zorder>widget_3</zorder>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="widget_2" native="true">

View File

@ -19,27 +19,16 @@
#include "room.h"
Room::Room(Models::Account* acc, Models::Room* p_room, QWidget* parent):
Conversation(true, acc, p_room->getJid(), "", parent),
Conversation(true, acc, p_room, p_room->getJid(), "", parent),
room(p_room)
{
setName(p_room->getName());
line->setMyName(room->getNick());
setStatus(room->getSubject());
setAvatar(room->getAvatarPath());
connect(room, &Models::Room::childChanged, this, &Room::onRoomChanged);
connect(room, &Models::Room::participantJoined, this, &Room::onParticipantJoined);
connect(room, &Models::Room::participantLeft, this, &Room::onParticipantLeft);
std::map<QString, const Models::Participant&> members = room->getParticipants();
for (std::pair<QString, const Models::Participant&> pair : members) {
QString aPath = pair.second.getAvatarPath();
if (aPath.size() > 0) {
line->setPalAvatar(pair.first, aPath);
}
}
line->setExPalAvatars(room->getExParticipantAvatars());
}
Room::~Room()
@ -75,30 +64,14 @@ void Room::onRoomChanged(Models::Item* item, int row, int col)
setAvatar(room->getAvatarPath());
break;
}
} else {
switch (col) {
case 7: {
Models::Participant* mem = static_cast<Models::Participant*>(item);
QString aPath = mem->getAvatarPath();
if (aPath.size() > 0) {
line->setPalAvatar(mem->getName(), aPath);
} else {
line->dropPalAvatar(mem->getName());
}
}
}
}
}
void Room::onParticipantJoined(const Models::Participant& participant)
{
QString aPath = participant.getAvatarPath();
if (aPath.size() > 0) {
line->setPalAvatar(participant.getName(), aPath);
}
}
void Room::onParticipantLeft(const QString& name)
{
line->movePalAvatarToEx(name);
}