linphone-desktop/Linphone/core/notifier/Notifier.cpp
Gaelle Braud b17bc8cc27 Fixes:
fix get size with screen ratio function

fix chat sending area ui #LINQT-2068

print debug logs in linphone files for futur debugging

fix call history details ui when no video conference factory set

use remote name of each call if in local conference #LINQT-2058
2025-10-21 11:25:17 +02:00

504 lines
20 KiB
C++

/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <QFileInfo>
#include <QMutex>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQuickItem>
#include <QQuickView>
#include <QQuickWindow>
#include <QScreen>
#include <QTimer>
#include "Notifier.hpp"
#include "core/App.hpp"
#include "core/call/CallGui.hpp"
#include "core/chat/ChatGui.hpp"
#include "model/tool/ToolModel.hpp"
#include "tool/LinphoneEnums.hpp"
#include "tool/accessibility/AccessibilityHelper.hpp"
#include "tool/providers/AvatarProvider.hpp"
#include "tool/providers/ImageProvider.hpp"
DEFINE_ABSTRACT_OBJECT(Notifier)
// =============================================================================
using namespace std;
namespace {
constexpr char NotificationsPath[] = "qrc:/qt/qml/Linphone/view/Control/Popup/Notification/";
// ---------------------------------------------------------------------------
// Notifications QML properties/methods.
// ---------------------------------------------------------------------------
constexpr char NotificationShowMethodName[] = "open";
constexpr char NotificationPropertyData[] = "notificationData";
constexpr char NotificationPropertyX[] = "popupX";
constexpr char NotificationPropertyY[] = "popupY";
constexpr char NotificationPropertyWindow[] = "__internalWindow";
constexpr char NotificationPropertyTimer[] = "__timer";
// ---------------------------------------------------------------------------
// Arbitrary hardcoded values.
// ---------------------------------------------------------------------------
constexpr int NotificationSpacing = 10;
constexpr int MaxNotificationsNumber = 5;
} // namespace
// =============================================================================
template <class T>
void setProperty(QObject &object, const char *property, const T &value) {
if (!object.setProperty(property, QVariant(value))) {
qWarning() << QStringLiteral("Unable to set property: `%1`.").arg(property);
abort();
}
}
// =============================================================================
// Available notifications.
// =============================================================================
const QHash<int, Notifier::Notification> Notifier::Notifications = {
{Notifier::ReceivedMessage, {Notifier::ReceivedMessage, "NotificationReceivedMessage.qml", 10}},
//{Notifier::ReceivedFileMessage, {Notifier::ReceivedFileMessage, "NotificationReceivedFileMessage.qml", 10}},
{Notifier::ReceivedCall, {Notifier::ReceivedCall, "NotificationReceivedCall.qml", 30}}
//{Notifier::NewVersionAvailable, {Notifier::NewVersionAvailable, "NotificationNewVersionAvailable.qml", 30}},
//{Notifier::SnapshotWasTaken, {Notifier::SnapshotWasTaken, "NotificationSnapshotWasTaken.qml", 10}},
//{Notifier::RecordingCompleted, {Notifier::RecordingCompleted, "NotificationRecordingCompleted.qml", 10}}
};
// -----------------------------------------------------------------------------
Notifier::Notifier(QObject *parent) : QObject(parent) {
mustBeInMainThread(getClassName());
const int nComponents = Notifications.size();
mComponents.resize(nComponents);
QQmlEngine *engine = App::getInstance()->mEngine;
for (const auto &key : Notifications.keys()) {
QQmlComponent *component =
new QQmlComponent(engine, QUrl(NotificationsPath + Notifier::Notifications[key].filename));
if (Q_UNLIKELY(component->isError())) {
qWarning() << QStringLiteral("Errors found in `Notification` component %1:").arg(key)
<< component->errors();
abort();
}
mComponents[key] = component;
}
mMutex = new QMutex();
}
Notifier::~Notifier() {
mustBeInMainThread("~" + getClassName());
delete mMutex;
const int nComponents = Notifications.size();
mComponents.clear();
}
// -----------------------------------------------------------------------------
bool Notifier::createNotification(Notifier::NotificationType type, QVariantMap data) {
mMutex->lock();
// Q_ASSERT(mInstancesNumber <= MaxNotificationsNumber);
if (mInstancesNumber == MaxNotificationsNumber) { // Check existing instances.
qWarning() << QStringLiteral("Unable to create another notification.");
mMutex->unlock();
return false;
}
QList<QScreen *> allScreens = QGuiApplication::screens();
if (allScreens.size() > 0) { // Ensure to have a screen to avoid errors
QQuickItem *previousWrapper = nullptr;
bool showAsTool = false;
#ifdef Q_OS_MACOS
for (auto w : QGuiApplication::topLevelWindows()) {
if ((w->windowState() & Qt::WindowFullScreen) == Qt::WindowFullScreen) {
showAsTool = true;
w->raise(); // Used to get focus on Mac (On Mac, A Tool is hidden if the app has not focus and the only
// way to rid it is to use Widget Attributes(Qt::WA_MacAlwaysShowToolWindow) that is not
// available)
}
}
#endif
for (int i = 0; i < allScreens.size(); ++i) {
++mInstancesNumber;
// Use QQuickView to create a visual root object that is
// independant from current application Window
QScreen *screen = allScreens[i];
auto engine = App::getInstance()->mEngine;
const QUrl url(QString(NotificationsPath) + Notifier::Notifications[type].filename);
QObject::connect(
engine, &QQmlApplicationEngine::objectCreated, this,
[this, url, screen, engine, type, data](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) {
lCritical() << "[App] Notifier.qml couldn't be load.";
engine->deleteLater();
exit(-1);
} else {
auto window = qobject_cast<QQuickWindow *>(obj);
if (window) {
window->setProperty(NotificationPropertyData, data);
// for (auto it = data.begin(); it != data.end(); ++it)
// window->setProperty(it.key().toLatin1(), it.value());
const int timeout = Notifications[type].getTimeout() * 1000;
auto updateNotificationCoordinates = [this, window, screen](int width, int height) {
auto screenHeightOffset =
screen ? mScreenHeightOffset.value(screen->name()) : 0; // Access optimization
QRect availableGeometry = screen->availableGeometry();
window->setX(availableGeometry.x() +
(availableGeometry.width() -
width)); //*screen->devicePixelRatio()); when using manual scaler
window->setY(availableGeometry.y() + availableGeometry.height() - screenHeightOffset -
height);
};
updateNotificationCoordinates(window->width(), window->height());
auto screenHeightOffset =
screen ? mScreenHeightOffset.take(screen->name()) : 0; // Access optimization
mScreenHeightOffset.insert(screen->name(), screenHeightOffset + window->height());
QObject::connect(window, &QQuickWindow::closing, window, [this, window] {
qDebug() << "closing notification";
deleteNotification(QVariant::fromValue(window));
});
showNotification(window, timeout);
lInfo() << QStringLiteral("Create notification:") << QVariant::fromValue(window);
}
}
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
lDebug() << log().arg("Engine loading notification");
engine->load(url);
}
}
mMutex->unlock();
return true;
}
// -----------------------------------------------------------------------------
void Notifier::showNotification(QObject *notification, int timeout) {
// Display notification.
QTimer *timer = new QTimer(notification);
timer->setInterval(timeout);
timer->setSingleShot(true);
notification->setProperty(NotificationPropertyTimer, QVariant::fromValue(timer));
// Destroy it after timeout.
QObject::connect(timer, &QTimer::timeout, this,
[this, notification]() { deleteNotificationOnTimeout(QVariant::fromValue(notification)); });
// Called explicitly (by a click on notification for example)
QObject::connect(notification, SIGNAL(deleteNotification(QVariant)), this, SLOT(deleteNotification(QVariant)));
timer->start();
}
// -----------------------------------------------------------------------------
void Notifier::deleteNotificationOnTimeout(QVariant notification) {
#ifdef Q_OS_MACOS
for (auto w : QGuiApplication::topLevelWindows()) {
if ((w->windowState() & Qt::WindowFullScreen) == Qt::WindowFullScreen) {
w->requestActivate(); // Used to get focus on fullscreens on Mac in order to avoid screen switching.
}
}
#endif
deleteNotification(notification);
}
void Notifier::deleteNotification(QVariant notification) {
mMutex->lock();
QObject *instance = notification.value<QObject *>();
// Notification marked destroyed.
if (instance->property("__valid").isValid()) {
mMutex->unlock();
return;
}
lInfo() << QStringLiteral("Delete notification:") << instance << --mInstancesNumber;
instance->setProperty("__valid", true);
auto timerProperty = instance->property(NotificationPropertyTimer).value<QTimer *>();
if (timerProperty) timerProperty->stop();
Q_ASSERT(mInstancesNumber >= 0);
if (mInstancesNumber == 0) mScreenHeightOffset.clear();
mMutex->unlock();
instance->deleteLater();
}
// =============================================================================
#define CREATE_NOTIFICATION(TYPE, DATA) \
auto settings = App::getInstance()->getSettings(); \
if (settings && settings->dndEnabled()) return; \
createNotification(TYPE, DATA);
// -----------------------------------------------------------------------------
// Notification functions.
// -----------------------------------------------------------------------------
void Notifier::notifyReceivedCall(const shared_ptr<linphone::Call> &call) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto remoteAddress = call->getRemoteAddress();
auto accountSender = ToolModel::findAccount(remoteAddress);
auto account = ToolModel::findAccount(call->getToAddress());
if (account) {
auto accountModel = Utils::makeQObject_ptr<AccountModel>(account);
accountModel->setSelf(accountModel);
if (!accountModel->getNotificationsAllowed()) {
lInfo() << log().arg(
"Notifications have been disabled for this account - not creating a notification for incoming call");
if (accountModel->forwardToVoiceMailInDndPresence()) {
lInfo() << log().arg("Transferring call to voicemail");
auto voicemailAddress = linphone::Factory::get()->createAddress(
Utils::appStringToCoreString(accountModel->getVoicemailAddress()));
if (voicemailAddress) call->transferTo(voicemailAddress);
} else {
lInfo() << log().arg("Declining call.");
call->decline(linphone::Reason::Busy);
}
return;
}
}
auto model = CallCore::create(call);
auto gui = new CallGui(model);
gui->moveToThread(App::getInstance()->thread());
auto callLog = call->getCallLog();
auto displayName = callLog && callLog->getConferenceInfo()
? Utils::coreStringToAppString(callLog->getConferenceInfo()->getSubject())
: ToolModel::getDisplayName(call->getRemoteAddress());
// Accessibility alert
//: New call from %1
AccessibilityHelper::announceMessage(tr("new_call_alert_accessible_name").arg(displayName));
App::postCoreAsync([this, gui, displayName]() {
mustBeInMainThread(getClassName());
QVariantMap map;
map["displayName"].setValue(displayName);
map["call"].setValue(gui);
CREATE_NOTIFICATION(Notifier::ReceivedCall, map)
});
}
void Notifier::notifyReceivedMessages(const std::shared_ptr<linphone::ChatRoom> &room,
const list<shared_ptr<linphone::ChatMessage>> &messages) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
if (room->getMuted()) return;
QString txt;
QString remoteAddress;
if (messages.size() > 0) {
shared_ptr<linphone::ChatMessage> message = messages.front();
auto receiverAccount = ToolModel::findAccount(message->getToAddress());
if (receiverAccount) {
auto senderAccount = ToolModel::findAccount(message->getFromAddress());
auto currentAccount = CoreModel::getInstance()->getCore()->getDefaultAccount();
if (senderAccount && senderAccount->getContactAddress()->weakEqual(currentAccount->getContactAddress())) {
qDebug() << "sender is current account, return";
return;
}
auto accountModel = Utils::makeQObject_ptr<AccountModel>(receiverAccount);
accountModel->setSelf(accountModel);
if (!accountModel->getNotificationsAllowed()) {
lInfo() << log().arg(
"Notifications have been disabled for this account - not creating a notification for "
"incoming message");
return;
}
}
auto getMessage = [this, &remoteAddress, &txt](const shared_ptr<linphone::ChatMessage> &message) {
if (message->isRead()) return;
auto remoteAddr = message->getFromAddress();
// remoteAddr->clean();
remoteAddress = Utils::coreStringToAppString(remoteAddr->asStringUriOnly());
auto fileContent = message->getFileTransferInformation();
if (!fileContent) {
for (auto content : message->getContents()) {
if (content->isText()) txt += content->getUtf8Text().c_str();
}
} else if (fileContent->isVoiceRecording())
//: 'Voice message received!' : message to warn the user in a notofication for voice messages.
txt = tr("new_voice_message");
else txt = tr("new_file_message");
if (txt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation received!' : Notification about receiving an invitation to a conference.
txt = tr("new_conference_invitation");
};
if (messages.size() == 1) { // Display only sender on mono message.
getMessage(message);
if (txt.isEmpty()) { // Do not notify message without content
qDebug() << "empty notif, return";
return;
}
} else {
int unreadCount = 0;
for (auto &message : messages) {
if (!message->isRead()) {
++unreadCount;
if (unreadCount == 1) getMessage(message);
}
}
if (unreadCount == 0) return;
if (unreadCount > 1)
//: 'New messages received!' Notification that warn the user of new messages.
txt = tr("new_chat_room_messages");
}
auto chatCore = ChatCore::create(room);
// Accessibility alert
//: New message on chatroom %1
AccessibilityHelper::announceMessage(tr("new_message_alert_accessible_name").arg(chatCore->getTitle()));
App::postCoreAsync([this, txt, chatCore, remoteAddress]() {
mustBeInMainThread(getClassName());
QVariantMap map;
map["message"] = txt;
map["remoteAddress"] = remoteAddress;
map["chatRoomName"] = chatCore->getTitle();
map["chatRoomAddress"] = chatCore->getChatRoomAddress();
map["avatarUri"] = chatCore->getAvatarUri();
map["isGroupChat"] = chatCore->isGroupChat();
map["chat"] = QVariant::fromValue(chatCore ? new ChatGui(chatCore) : nullptr);
CREATE_NOTIFICATION(Notifier::ReceivedMessage, map)
});
auto settings = SettingsModel::getInstance();
if (settings && !settings->dndEnabled()) {
CoreModel::getInstance()->getCore()->playLocal(
Utils::appStringToCoreString(settings->getChatNotificationSoundPath()));
}
}
}
/*
void Notifier::notifyReceivedReactions(
const QList<QPair<std::shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>>>
&reactions) {
QVariantMap map;
QString txt;
if (reactions.size() > 0) {
ChatMessageModel *redirection = nullptr;
QPair<shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>> reaction =
reactions.front();
shared_ptr<linphone::ChatMessage> message = reaction.first;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());
auto timelineModel = CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true);
map["messageId"] = Utils::coreStringToAppString(message->getMessageId());
if (reactions.size() == 1) {
QString messageTxt;
auto fileContent = message->getFileTransferInformation();
if (!fileContent) {
foreach (auto content, message->getContents()) {
if (content->isText()) messageTxt += content->getUtf8Text().c_str();
}
} else if (fileContent->isVoiceRecording())
//: 'Voice message' : Voice message type that has been reacted.
messageTxt += tr("voice_message_react");
else {
QFileInfo file(Utils::coreStringToAppString(fileContent->getFilePath()));
messageTxt += file.fileName();
}
if (messageTxt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation' : Conference invitation message type that has been reacted.
messageTxt += tr("conference_invitation_react");
//: ''Has reacted by %1 to: %2' : Reaction message. %1=Reaction(emoji), %2=type of message(Voice
//: Message/Conference invitation/ Message text)
txt = tr("reaction_message").arg(Utils::coreStringToAppString(reaction.second->getBody())).arg(messageTxt);
} else
//: 'New reactions received!' : Notification that warn the user of new reactions.
txt = tr("new_reactions_messages");
map["message"] = txt;
map["timelineModel"].setValue(timelineModel.get());
if (reactions.size() == 1) { // Display only sender on mono message.
map["remoteAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asStringUriOnly());
map["fullremoteAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asString());
}
map["localAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asStringUriOnly());
map["fullLocalAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asString());
map["window"].setValue(App::getInstance()->getMainWindow());
CREATE_NOTIFICATION(Notifier::ReceivedMessage, map)
}
}
void Notifier::notifyReceivedFileMessage(const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content) {
QVariantMap map;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());
map["timelineModel"].setValue(
CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true).get());
map["fileUri"] = Utils::coreStringToAppString(content->getFilePath());
if (Utils::getImage(map["fileUri"].toString()).isNull()) map["imageUri"] = "";
else map["imageUri"] = map["fileUri"];
map["fileSize"] = quint64(content->getSize() + content->getFileSize());
CREATE_NOTIFICATION(Notifier::ReceivedFileMessage, map)
}
void Notifier::notifyNewVersionAvailable(const QString &version, const QString &url) {
QVariantMap map;
map["message"] = tr("new_version_available").arg(version);
map["url"] = url;
CREATE_NOTIFICATION(Notifier::NewVersionAvailable, map)
}
void Notifier::notifySnapshotWasTaken(const QString &filePath) {
QVariantMap map;
map["filePath"] = filePath;
CREATE_NOTIFICATION(Notifier::SnapshotWasTaken, map)
}
void Notifier::notifyRecordingCompleted(const QString &filePath) {
QVariantMap map;
map["filePath"] = filePath;
CREATE_NOTIFICATION(Notifier::RecordingCompleted, map)
}
*/
#undef SHOW_NOTIFICATION
#undef CREATE_NOTIFICATION