squawk/main/application.cpp

657 lines
25 KiB
C++

// Squawk messenger.
// Copyright (C) 2019 Yury Gubich <blue@macaw.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "application.h"
Application::Application(Core::Squawk* p_core):
QObject(),
availability(Shared::Availability::offline),
core(p_core),
squawk(nullptr),
notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"),
roster(),
conversations(),
dialogueQueue(roster),
nowQuitting(false),
destroyingSquawk(false),
storage(),
trayIcon(nullptr),
actionQuit(Shared::icon("application-exit"), tr("Quit")),
actionToggle(QApplication::windowIcon(), tr("Minimize to tray")),
expandedPaths()
{
connect(&actionQuit, &QAction::triggered, this, &Application::quit);
connect(&actionToggle, &QAction::triggered, this, &Application::toggleSquawk);
connect(&roster, &Models::Roster::unnoticedMessage, this, &Application::notify);
connect(&roster, &Models::Roster::unreadMessagesCountChanged, this, &Application::unreadMessagesCountChanged);
connect(&roster, &Models::Roster::addedElement, this, &Application::onAddedElement);
//connecting myself to the backend
connect(this, &Application::changeState, core, &Core::Squawk::changeState);
connect(this, &Application::setRoomJoined, core, &Core::Squawk::setRoomJoined);
connect(this, &Application::setRoomAutoJoin, core, &Core::Squawk::setRoomAutoJoin);
connect(this, &Application::subscribeContact, core, &Core::Squawk::subscribeContact);
connect(this, &Application::unsubscribeContact, core, &Core::Squawk::unsubscribeContact);
connect(this, &Application::replaceMessage, core, &Core::Squawk::replaceMessage);
connect(this, &Application::sendMessage, core, &Core::Squawk::sendMessage);
connect(this, &Application::resendMessage, core, &Core::Squawk::resendMessage);
connect(&roster, &Models::Roster::requestArchive,
std::bind(&Core::Squawk::requestArchive, core, std::placeholders::_1, std::placeholders::_2, 20, std::placeholders::_3));
connect(&dialogueQueue, &DialogQueue::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
connect(&dialogueQueue, &DialogQueue::responsePassword, core, &Core::Squawk::responsePassword);
connect(&dialogueQueue, &DialogQueue::disconnectAccount, core, &Core::Squawk::disconnectAccount);
connect(&roster, &Models::Roster::fileDownloadRequest, core, &Core::Squawk::fileDownloadRequest);
connect(&roster, &Models::Roster::localPathInvalid, core, &Core::Squawk::onLocalPathInvalid);
//coonecting backend to myself
connect(core, &Core::Squawk::stateChanged, this, &Application::stateChanged);
connect(core, &Core::Squawk::accountMessage, &roster, &Models::Roster::addMessage);
connect(core, &Core::Squawk::responseArchive, &roster, &Models::Roster::responseArchive);
connect(core, &Core::Squawk::changeMessage, &roster, &Models::Roster::changeMessage);
connect(core, &Core::Squawk::newAccount, &roster, &Models::Roster::addAccount);
connect(core, &Core::Squawk::changeAccount, this, &Application::changeAccount);
connect(core, &Core::Squawk::removeAccount, this, &Application::removeAccount);
connect(core, &Core::Squawk::addContact, &roster, &Models::Roster::addContact);
connect(core, &Core::Squawk::addGroup, this, &Application::addGroup);
connect(core, &Core::Squawk::removeGroup, &roster, &Models::Roster::removeGroup);
connect(core, qOverload<const QString&, const QString&>(&Core::Squawk::removeContact),
&roster, qOverload<const QString&, const QString&>(&Models::Roster::removeContact));
connect(core, qOverload<const QString&, const QString&, const QString&>(&Core::Squawk::removeContact),
&roster, qOverload<const QString&, const QString&, const QString&>(&Models::Roster::removeContact));
connect(core, &Core::Squawk::changeContact, &roster, &Models::Roster::changeContact);
connect(core, &Core::Squawk::addPresence, &roster, &Models::Roster::addPresence);
connect(core, &Core::Squawk::removePresence, &roster, &Models::Roster::removePresence);
connect(core, &Core::Squawk::addRoom, &roster, &Models::Roster::addRoom);
connect(core, &Core::Squawk::changeRoom, &roster, &Models::Roster::changeRoom);
connect(core, &Core::Squawk::removeRoom, &roster, &Models::Roster::removeRoom);
connect(core, &Core::Squawk::addRoomParticipant, &roster, &Models::Roster::addRoomParticipant);
connect(core, &Core::Squawk::changeRoomParticipant, &roster, &Models::Roster::changeRoomParticipant);
connect(core, &Core::Squawk::removeRoomParticipant, &roster, &Models::Roster::removeRoomParticipant);
connect(core, &Core::Squawk::fileDownloadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, false));
connect(core, &Core::Squawk::fileUploadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, true));
connect(core, &Core::Squawk::fileProgress, &roster, &Models::Roster::fileProgress);
connect(core, &Core::Squawk::fileError, &roster, &Models::Roster::fileError);
connect(core, &Core::Squawk::requestPassword, this, &Application::requestPassword);
connect(core, &Core::Squawk::ready, this, &Application::readSettings);
QDBusConnection sys = QDBusConnection::sessionBus();
sys.connect(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"NotificationClosed",
this,
SLOT(onNotificationClosed(quint32, quint32))
);
sys.connect(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"ActionInvoked",
this,
SLOT(onNotificationInvoked(quint32, const QString&))
);
}
Application::~Application() {}
void Application::quit() {
if (!nowQuitting) {
nowQuitting = true;
emit quitting();
writeSettings();
unreadMessagesCountChanged(0); //this notification persist in the desktop, for now I'll zero it on quit not to confuse people
for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) {
disconnect(itr->second, &Conversation::destroyed, this, &Application::onConversationClosed);
itr->second->close();
}
conversations.clear();
dialogueQueue.quit();
if (squawk != nullptr)
squawk->close();
if (trayIcon != nullptr) {
trayIcon->deleteLater();
trayIcon = nullptr;
}
if (!destroyingSquawk)
checkForTheLastWindow();
}
}
void Application::checkForTheLastWindow() {
if (QApplication::topLevelWidgets().size() > 0)
emit readyToQuit();
else
connect(qApp, &QApplication::lastWindowClosed, this, &Application::readyToQuit);
}
void Application::createMainWindow() {
if (squawk == nullptr) {
squawk = new Squawk(roster);
connect(squawk, &Squawk::notify, this, &Application::notify);
connect(squawk, &Squawk::changeSubscription, this, &Application::changeSubscription);
connect(squawk, &Squawk::openedConversation, this, &Application::onSquawkOpenedConversation);
connect(squawk, &Squawk::openConversation, this, &Application::openConversation);
connect(squawk, &Squawk::changeState, this, &Application::setState);
connect(squawk, &Squawk::changeTray, this, &Application::onChangeTray);
connect(squawk, &Squawk::itemExpanded, this, &Application::onItemExpanded);
connect(squawk, &Squawk::itemCollapsed, this, &Application::onItemCollapsed);
connect(squawk, &Squawk::quit, this, &Application::quit);
connect(squawk, &Squawk::closing, this, &Application::onSquawkClosing);
connect(squawk, &Squawk::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
connect(squawk, &Squawk::newAccountRequest, core, &Core::Squawk::newAccountRequest);
connect(squawk, &Squawk::removeAccountRequest, core, &Core::Squawk::removeAccountRequest);
connect(squawk, &Squawk::connectAccount, core, &Core::Squawk::connectAccount);
connect(squawk, &Squawk::disconnectAccount, core, &Core::Squawk::disconnectAccount);
connect(squawk, &Squawk::addContactRequest, core, &Core::Squawk::addContactRequest);
connect(squawk, &Squawk::removeContactRequest, core, &Core::Squawk::removeContactRequest);
connect(squawk, &Squawk::removeRoomRequest, core, &Core::Squawk::removeRoomRequest);
connect(squawk, &Squawk::addRoomRequest, core, &Core::Squawk::addRoomRequest);
connect(squawk, &Squawk::addContactToGroupRequest, core, &Core::Squawk::addContactToGroupRequest);
connect(squawk, &Squawk::removeContactFromGroupRequest, core, &Core::Squawk::removeContactFromGroupRequest);
connect(squawk, &Squawk::renameContactRequest, core, &Core::Squawk::renameContactRequest);
connect(squawk, &Squawk::requestInfo, core, &Core::Squawk::requestInfo);
connect(squawk, &Squawk::updateInfo, core, &Core::Squawk::updateInfo);
connect(squawk, &Squawk::changeDownloadsPath, core, &Core::Squawk::changeDownloadsPath);
connect(core, &Core::Squawk::responseInfo, squawk, &Squawk::responseInfo);
dialogueQueue.setParentWidnow(squawk);
squawk->stateChanged(availability);
squawk->raise();
squawk->show();
squawk->activateWindow();
for (const std::list<QString>& entry : expandedPaths) {
QModelIndex ind = roster.getIndexByPath(entry);
if (ind.isValid())
squawk->expand(ind);
}
connect(squawk, &Squawk::itemExpanded, this, &Application::onItemExpanded);
connect(squawk, &Squawk::itemCollapsed, this, &Application::onItemCollapsed);
}
}
void Application::onSquawkClosing() {
dialogueQueue.setParentWidnow(nullptr);
if (!nowQuitting) {
disconnect(core, &Core::Squawk::responseInfo, squawk, &Squawk::responseInfo);
}
destroyingSquawk = true;
squawk->deleteLater();
squawk = nullptr;
QSettings settings;
if (!nowQuitting && QSystemTrayIcon::isSystemTrayAvailable() && settings.value("tray", false).toBool()) {
if (settings.value("hideTray", false).toBool()) {
createTrayIcon();
}
actionToggle.setText(tr("Show Squawk"));
} else {
quit();
}
}
void Application::onSquawkDestroyed() {
destroyingSquawk = false;
if (nowQuitting)
checkForTheLastWindow();
}
void Application::onChangeTray(bool enabled, bool hide) {
if (enabled) {
if (trayIcon == nullptr) {
if (!hide || squawk == nullptr)
createTrayIcon();
} else {
if (hide && squawk != nullptr) {
trayIcon->deleteLater();
trayIcon = nullptr;
}
}
} else if (trayIcon == nullptr) {
trayIcon->deleteLater();
trayIcon = nullptr;
}
}
void Application::createTrayIcon() {
trayIcon = new QSystemTrayIcon();
QMenu* trayIconMenu = new QMenu();
trayIconMenu->addAction(&actionToggle);
trayIconMenu->addAction(&actionQuit);
trayIcon->setContextMenu(trayIconMenu);
trayIcon->setIcon(QApplication::windowIcon().pixmap(32, 32));
trayIcon->setToolTip(QApplication::applicationDisplayName());
connect(trayIcon, &QSystemTrayIcon::activated, this, &Application::trayClicked);
connect(trayIcon, &QSystemTrayIcon::destroyed, trayIconMenu, &QMenu::deleteLater);
trayIcon->show();
}
void Application::trayClicked(QSystemTrayIcon::ActivationReason reason) {
switch (reason) {
case QSystemTrayIcon::Trigger:
case QSystemTrayIcon::DoubleClick:
case QSystemTrayIcon::MiddleClick:
toggleSquawk();
break;
default:
break;
}
}
void Application::toggleSquawk() {
QSettings settings;
if (squawk == nullptr) {
createMainWindow();
if (settings.value("hideTray", false).toBool()) {
trayIcon->deleteLater();
trayIcon = nullptr;
}
actionToggle.setText(tr("Minimize to tray"));
} else {
squawk->close();
}
}
void Application::onItemCollapsed(const QModelIndex& index) {
std::list<QString> address = roster.getItemPath(index);
if (address.size() > 0)
expandedPaths.erase(address);
}
void Application::onItemExpanded(const QModelIndex& index) {
std::list<QString> address = roster.getItemPath(index);
if (address.size() > 0)
expandedPaths.insert(address);
}
void Application::onAddedElement(const std::list<QString>& path) {
if (squawk != nullptr && expandedPaths.count(path) > 0) {
QModelIndex index = roster.getIndexByPath(path);
if (index.isValid())
squawk->expand(index);
}
}
void Application::notify(const QString& account, const Shared::Message& msg) {
QString jid = msg.getPenPalJid();
QString name = QString(roster.getContactName(account, jid));
QString path = QString(roster.getContactIconPath(account, jid, msg.getPenPalResource()));
QVariantList args;
args << QString();
uint32_t notificationId = qHash(msg.getId());
args << notificationId;
if (path.size() > 0)
args << path;
else
args << QString("mail-message"); //TODO should here better be unknown user icon?
if (msg.getType() == Shared::Message::groupChat)
args << msg.getFromResource() + tr(" from ") + name;
else
args << name;
QString body(msg.getBody());
QString oob(msg.getOutOfBandUrl());
if (body == oob) {
body = tr("Attached file");
}
args << body;
args << QStringList({
"markAsRead", tr("Mark as Read"),
"openConversation", tr("Open conversation")
});
args << QVariantMap({
{"desktop-entry", qApp->desktopFileName()},
{"category", QString("message")},
{"urgency", 1},
// {"sound-file", "/path/to/macaw/squawk"},
{"sound-name", QString("message-new-instant")}
});
args << -1;
notifications.callWithArgumentList(QDBus::AutoDetect, "Notify", args);
storage.insert(std::make_pair(notificationId, std::make_pair(Models::Roster::ElId(account, name), msg.getId())));
if (squawk != nullptr)
QApplication::alert(squawk);
}
void Application::onNotificationClosed(quint32 id, quint32 reason) {
Notifications::const_iterator itr = storage.find(id);
if (itr != storage.end()) {
if (reason == 2) { //dissmissed by user (https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html)
//TODO may ba also mark as read?
}
if (reason != 1) { //just expired, can be activated again from history, so no removing for now
storage.erase(id);
qDebug() << "Notification" << id << "was closed";
}
}
}
void Application::onNotificationInvoked(quint32 id, const QString& action) {
qDebug() << "Notification" << id << action << "request";
Notifications::const_iterator itr = storage.find(id);
if (itr != storage.end()) {
if (action == "markAsRead")
roster.markMessageAsRead(itr->second.first, itr->second.second);
else if (action == "openConversation")
focusConversation(itr->second.first, "", itr->second.second);
}
}
void Application::unreadMessagesCountChanged(int count) {
QDBusMessage signal = QDBusMessage::createSignal("/", "com.canonical.Unity.LauncherEntry", "Update");
signal << qApp->desktopFileName() + QLatin1String(".desktop");
signal << QVariantMap ({
{"count-visible", count != 0},
{"count", count}
});
QDBusConnection::sessionBus().send(signal);
}
void Application::focusConversation(const Models::Roster::ElId& id, const QString& resource, const QString& messageId) {
if (squawk != nullptr) {
if (squawk->currentConversationId() != id) {
QModelIndex index = roster.getContactIndex(id.account, id.name, resource);
squawk->select(index);
}
if (squawk->isMinimized())
squawk->showNormal();
else
squawk->show();
squawk->raise();
squawk->activateWindow();
} else {
openConversation(id, resource);
}
SHARED_UNUSED(messageId);
//TODO focus messageId;
}
void Application::setState(Shared::Availability p_availability) {
if (availability != p_availability) {
availability = p_availability;
emit changeState(availability);
}
}
void Application::stateChanged(Shared::Availability state) {
availability = state;
if (squawk != nullptr)
squawk->stateChanged(state);
}
void Application::readSettings() {
QSettings settings;
settings.beginGroup("ui");
int avail;
if (settings.contains("availability"))
avail = settings.value("availability").toInt();
else
avail = static_cast<int>(Shared::Availability::online);
settings.beginGroup("roster");
QStringList entries = settings.allKeys();
for (const QString& entry : entries) {
QStringList p = entry.split("/");
if (p.last() == "expanded" && settings.value(entry, false).toBool()) {
p.pop_back();
expandedPaths.emplace(p.begin(), p.end());
}
}
settings.endGroup();
settings.endGroup();
setState(Shared::Global::fromInt<Shared::Availability>(avail));
createMainWindow();
if (settings.value("tray", false).toBool() && !settings.value("hideTray", false).toBool())
createTrayIcon();
}
void Application::writeSettings() {
QSettings settings;
settings.beginGroup("ui");
settings.setValue("availability", static_cast<int>(availability));
settings.remove("roster");
settings.beginGroup("roster");
for (const std::list<QString>& address : expandedPaths) {
QString path = "";
for (const QString& hop : address)
path += hop + "/";
path += "expanded";
settings.setValue(path, true);
}
settings.endGroup();
settings.endGroup();
}
void Application::requestPassword(const QString& account, bool authenticationError) {
if (authenticationError)
dialogueQueue.addAction(account, DialogQueue::askCredentials);
else
dialogueQueue.addAction(account, DialogQueue::askPassword);
}
void Application::onConversationClosed() {
Conversation* conv = static_cast<Conversation*>(sender());
Models::Roster::ElId id(conv->getAccount(), conv->getJid());
Conversations::const_iterator itr = conversations.find(id);
if (itr != conversations.end())
conversations.erase(itr);
if (conv->isMuc) {
Room* room = static_cast<Room*>(conv);
if (!room->autoJoined())
emit setRoomJoined(id.account, id.name, false);
}
}
void Application::changeSubscription(const Models::Roster::ElId& id, bool subscribe) {
Models::Item::Type type = roster.getContactType(id);
switch (type) {
case Models::Item::contact:
if (subscribe)
emit subscribeContact(id.account, id.name, "");
else
emit unsubscribeContact(id.account, id.name, "");
break;
case Models::Item::room:
setRoomAutoJoin(id.account, id.name, subscribe);
if (!isConverstationOpened(id))
emit setRoomJoined(id.account, id.name, subscribe);
break;
default:
break;
}
}
void Application::subscribeConversation(Conversation* conv) {
connect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
connect(conv, &Conversation::sendMessage, this, &Application::onConversationMessage);
connect(conv, &Conversation::replaceMessage, this, &Application::onConversationReplaceMessage);
connect(conv, &Conversation::resendMessage, this, &Application::onConversationResend);
connect(conv, &Conversation::notifyableMessage, this, &Application::notify);
}
void Application::openConversation(const Models::Roster::ElId& id, const QString& resource) {
Conversations::const_iterator itr = conversations.find(id);
Models::Account* acc = roster.getAccount(id.account);
Conversation* conv = nullptr;
bool created = false;
if (itr != conversations.end()) {
conv = itr->second;
} else {
Models::Element* el = roster.getElement(id);
if (el != nullptr) {
if (el->type == Models::Item::room) {
created = true;
Models::Room* room = static_cast<Models::Room*>(el);
conv = new Room(acc, room);
if (!room->getJoined())
emit setRoomJoined(id.account, id.name, true);
} else if (el->type == Models::Item::contact) {
created = true;
conv = new Chat(acc, static_cast<Models::Contact*>(el));
}
}
}
if (conv != nullptr) {
if (created) {
conv->setAttribute(Qt::WA_DeleteOnClose);
subscribeConversation(conv);
conversations.insert(std::make_pair(id, conv));
}
conv->show();
conv->raise();
conv->activateWindow();
if (resource.size() > 0)
conv->setPalResource(resource);
}
}
void Application::onConversationMessage(const Shared::Message& msg) {
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
roster.addMessage(acc, msg);
emit sendMessage(acc, msg);
}
void Application::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg) {
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
roster.changeMessage(acc, msg.getPenPalJid(), originalId, {
{"state", static_cast<uint>(Shared::Message::State::pending)}
});
emit replaceMessage(acc, originalId, msg);
}
void Application::onConversationResend(const QString& id) {
Conversation* conv = static_cast<Conversation*>(sender());
QString acc = conv->getAccount();
QString jid = conv->getJid();
emit resendMessage(acc, jid, id);
}
void Application::onSquawkOpenedConversation() {
subscribeConversation(squawk->currentConversation);
Models::Roster::ElId id = squawk->currentConversationId();
const Models::Element* el = roster.getElementConst(id);
if (el != nullptr && el->isRoom() && !static_cast<const Models::Room*>(el)->getJoined()) {
emit setRoomJoined(id.account, id.name, true);
}
}
void Application::removeAccount(const QString& account) {
Conversations::const_iterator itr = conversations.begin();
while (itr != conversations.end()) {
if (itr->first.account == account) {
Conversations::const_iterator lItr = itr;
++itr;
Conversation* conv = lItr->second;
disconnect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
conv->close();
conversations.erase(lItr);
} else {
++itr;
}
}
if (squawk != nullptr && squawk->currentConversationId().account == account)
squawk->closeCurrentConversation();
roster.removeAccount(account);
}
void Application::changeAccount(const QString& account, const QMap<QString, QVariant>& data) {
for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
QString attr = itr.key();
roster.updateAccount(account, attr, *itr);
}
}
void Application::addGroup(const QString& account, const QString& name) {
roster.addGroup(account, name);
if (squawk != nullptr) {
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("roster");
settings.beginGroup(account);
if (settings.value("expanded", false).toBool()) {
QModelIndex ind = roster.getAccountIndex(account);
squawk->expand(ind);
if (settings.value(name + "/expanded", false).toBool())
squawk->expand(roster.getGroupIndex(account, name));
}
settings.endGroup();
settings.endGroup();
settings.endGroup();
}
}
bool Application::isConverstationOpened(const Models::Roster::ElId& id) const {
return (conversations.count(id) > 0) || (squawk != nullptr && squawk->currentConversationId() == id);}