mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-26 19:14:02 +02:00
Desktop App Toolkit uses GLib as the D-Bus library for quite long time, but GLib is not only a D-Bus library, it's more a basic library providing native Linux APIs implementing various specs. The situation right now is that DESKTOP_APP_DISABLE_DBUS_INTEGRATION disables not only D-Bus code but all the native API integration such as MIME handling or .desktop file parsing. In other words, the option disables native Linux APIs on Linux what is absurd and doesn't have any sense.
1202 lines
33 KiB
C++
1202 lines
33 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "window/notifications_manager.h"
|
|
|
|
#include "base/options.h"
|
|
#include "platform/platform_notifications_manager.h"
|
|
#include "window/notifications_manager_default.h"
|
|
#include "media/audio/media_audio_track.h"
|
|
#include "media/audio/media_audio.h"
|
|
#include "mtproto/mtproto_config.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/view/history_view_replies_section.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "data/notify/data_notify_settings.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_forum_topic.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_poll.h"
|
|
#include "base/unixtime.h"
|
|
#include "window/window_controller.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "core/application.h"
|
|
#include "mainwindow.h"
|
|
#include "api/api_updates.h"
|
|
#include "apiwrap.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_domain.h"
|
|
#include "ui/text/text_utilities.h"
|
|
|
|
#include <QtGui/QWindow>
|
|
|
|
#if __has_include(<giomm.h>)
|
|
#include <giomm.h>
|
|
#endif // __has_include(<giomm.h>)
|
|
|
|
namespace Window {
|
|
namespace Notifications {
|
|
namespace {
|
|
|
|
// not more than one sound in 500ms from one peer - grouping
|
|
constexpr auto kMinimalDelay = crl::time(100);
|
|
constexpr auto kMinimalForwardDelay = crl::time(500);
|
|
constexpr auto kMinimalAlertDelay = crl::time(500);
|
|
constexpr auto kWaitingForAllGroupedDelay = crl::time(1000);
|
|
constexpr auto kReactionNotificationEach = 60 * 60 * crl::time(1000);
|
|
|
|
#ifdef Q_OS_MAC
|
|
constexpr auto kSystemAlertDuration = crl::time(1000);
|
|
#else // !Q_OS_MAC
|
|
constexpr auto kSystemAlertDuration = crl::time(0);
|
|
#endif // Q_OS_MAC
|
|
|
|
[[nodiscard]] QString PlaceholderReactionText() {
|
|
static const auto result = QString::fromUtf8("\xf0\x9f\x92\xad");
|
|
return result;
|
|
}
|
|
|
|
QString TextWithPermanentSpoiler(const TextWithEntities &textWithEntities) {
|
|
auto text = textWithEntities.text;
|
|
for (const auto &e : textWithEntities.entities) {
|
|
if (e.type() == EntityType::Spoiler) {
|
|
auto replacement = QString().fill(QChar(0x259A), e.length());
|
|
text = text.replace(
|
|
e.offset(),
|
|
e.length(),
|
|
std::move(replacement));
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char kOptionGNotification[] = "gnotification";
|
|
|
|
base::options::toggle OptionGNotification({
|
|
.id = kOptionGNotification,
|
|
.name = "GNotification",
|
|
.description = "Force enable GLib's GNotification."
|
|
" When disabled, autodetect is used.",
|
|
.scope = [] {
|
|
#if __has_include(<giomm.h>)
|
|
return bool(Gio::Application::get_default());
|
|
#else // __has_include(<giomm.h>)
|
|
return false;
|
|
#endif // __has_include(<giomm.h>)
|
|
},
|
|
.restartRequired = true,
|
|
});
|
|
|
|
struct System::Waiter {
|
|
NotificationInHistoryKey key;
|
|
UserData *reactionSender = nullptr;
|
|
Data::ItemNotificationType type = Data::ItemNotificationType::Message;
|
|
crl::time when = 0;
|
|
};
|
|
|
|
System::NotificationInHistoryKey::NotificationInHistoryKey(
|
|
Data::ItemNotification notification)
|
|
: NotificationInHistoryKey(notification.item->id, notification.type) {
|
|
}
|
|
|
|
System::NotificationInHistoryKey::NotificationInHistoryKey(
|
|
MsgId messageId,
|
|
Data::ItemNotificationType type)
|
|
: messageId(messageId)
|
|
, type(type) {
|
|
}
|
|
|
|
System::System()
|
|
: _waitTimer([=] { showNext(); })
|
|
, _waitForAllGroupedTimer([=] { showGrouped(); })
|
|
, _manager(std::make_unique<DummyManager>(this)) {
|
|
settingsChanged(
|
|
) | rpl::start_with_next([=](ChangeType type) {
|
|
if (type == ChangeType::DesktopEnabled) {
|
|
clearAll();
|
|
} else if (type == ChangeType::ViewParams) {
|
|
updateAll();
|
|
} else if (type == ChangeType::IncludeMuted
|
|
|| type == ChangeType::CountMessages) {
|
|
Core::App().domain().notifyUnreadBadgeChanged();
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
void System::createManager() {
|
|
Platform::Notifications::Create(this);
|
|
}
|
|
|
|
void System::setManager(std::unique_ptr<Manager> manager) {
|
|
_manager = std::move(manager);
|
|
if (!_manager) {
|
|
_manager = std::make_unique<Default::Manager>(this);
|
|
}
|
|
}
|
|
|
|
Manager &System::manager() const {
|
|
Expects(_manager != nullptr);
|
|
return *_manager;
|
|
}
|
|
|
|
Main::Session *System::findSession(uint64 sessionId) const {
|
|
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
|
if (const auto session = account->maybeSession()) {
|
|
if (session->uniqueId() == sessionId) {
|
|
return session;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool System::skipReactionNotification(not_null<HistoryItem*> item) const {
|
|
const auto id = ReactionNotificationId{
|
|
.itemId = item->fullId(),
|
|
.sessionId = item->history()->session().uniqueId(),
|
|
};
|
|
const auto now = crl::now();
|
|
const auto clearBefore = now - kReactionNotificationEach;
|
|
for (auto i = begin(_sentReactionNotifications)
|
|
; i != end(_sentReactionNotifications)
|
|
;) {
|
|
if (i->second <= clearBefore) {
|
|
i = _sentReactionNotifications.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
return !_sentReactionNotifications.emplace(id, now).second;
|
|
}
|
|
|
|
System::SkipState System::skipNotification(
|
|
Data::ItemNotification notification) const {
|
|
const auto item = notification.item;
|
|
const auto type = notification.type;
|
|
const auto messageType = (type == Data::ItemNotificationType::Message);
|
|
if (!item->notificationThread()->currentNotification()
|
|
|| (messageType && item->skipNotification())
|
|
|| (type == Data::ItemNotificationType::Reaction
|
|
&& skipReactionNotification(item))) {
|
|
return { SkipState::Skip };
|
|
}
|
|
return computeSkipState(notification);
|
|
}
|
|
|
|
System::SkipState System::computeSkipState(
|
|
Data::ItemNotification notification) const {
|
|
const auto type = notification.type;
|
|
const auto item = notification.item;
|
|
const auto thread = item->notificationThread();
|
|
const auto notifySettings = &thread->owner().notifySettings();
|
|
const auto messageType = (type == Data::ItemNotificationType::Message);
|
|
const auto withSilent = [&](
|
|
SkipState::Value value,
|
|
bool forceSilent = false) {
|
|
return SkipState{
|
|
.value = value,
|
|
.silent = (forceSilent
|
|
|| !messageType
|
|
|| item->isSilent()
|
|
|| notifySettings->sound(thread).none),
|
|
};
|
|
};
|
|
const auto showForMuted = messageType
|
|
&& item->out()
|
|
&& item->isFromScheduled();
|
|
const auto notifyBy = messageType
|
|
? item->specialNotificationPeer()
|
|
: notification.reactionSender;
|
|
if (Core::Quitting()) {
|
|
return { SkipState::Skip };
|
|
} else if (!Core::App().settings().notifyFromAll()
|
|
&& &thread->session().account() != &Core::App().domain().active()) {
|
|
return { SkipState::Skip };
|
|
}
|
|
|
|
if (messageType) {
|
|
notifySettings->request(thread);
|
|
} else if (notifyBy->blockStatus() == PeerData::BlockStatus::Unknown) {
|
|
notifyBy->updateFull();
|
|
}
|
|
if (notifyBy) {
|
|
notifySettings->request(notifyBy);
|
|
}
|
|
|
|
if (messageType && notifySettings->muteUnknown(thread)) {
|
|
return { SkipState::Unknown };
|
|
} else if (messageType && !notifySettings->isMuted(thread)) {
|
|
return withSilent(SkipState::DontSkip);
|
|
} else if (!notifyBy) {
|
|
return withSilent(
|
|
showForMuted ? SkipState::DontSkip : SkipState::Skip,
|
|
showForMuted);
|
|
} else if (notifySettings->muteUnknown(notifyBy)
|
|
|| (!messageType
|
|
&& notifyBy->blockStatus() == PeerData::BlockStatus::Unknown)) {
|
|
return withSilent(SkipState::Unknown);
|
|
} else if (!notifySettings->isMuted(notifyBy)
|
|
&& (messageType || !notifyBy->isBlocked())) {
|
|
return withSilent(SkipState::DontSkip);
|
|
} else {
|
|
return withSilent(
|
|
showForMuted ? SkipState::DontSkip : SkipState::Skip,
|
|
showForMuted);
|
|
}
|
|
}
|
|
|
|
System::Timing System::countTiming(
|
|
not_null<Data::Thread*> thread,
|
|
crl::time minimalDelay) const {
|
|
auto delay = minimalDelay;
|
|
const auto t = base::unixtime::now();
|
|
const auto ms = crl::now();
|
|
const auto &updates = thread->session().updates();
|
|
const auto &config = thread->session().serverConfig();
|
|
const bool isOnline = updates.lastWasOnline();
|
|
const auto otherNotOld = ((cOtherOnline() * 1000LL) + config.onlineCloudTimeout > t * 1000LL);
|
|
const bool otherLaterThanMe = (cOtherOnline() * 1000LL + (ms - updates.lastSetOnline()) > t * 1000LL);
|
|
if (!isOnline && otherNotOld && otherLaterThanMe) {
|
|
delay = config.notifyCloudDelay;
|
|
} else if (cOtherOnline() >= t) {
|
|
delay = config.notifyDefaultDelay;
|
|
}
|
|
return {
|
|
.delay = delay,
|
|
.when = ms + delay,
|
|
};
|
|
}
|
|
|
|
void System::registerThread(not_null<Data::Thread*> thread) {
|
|
if (const auto topic = thread->asTopic()) {
|
|
const auto [i, ok] = _watchedTopics.emplace(topic, rpl::lifetime());
|
|
if (ok) {
|
|
topic->destroyed() | rpl::start_with_next([=] {
|
|
clearFromTopic(topic);
|
|
}, i->second);
|
|
}
|
|
}
|
|
}
|
|
|
|
void System::schedule(Data::ItemNotification notification) {
|
|
Expects(_manager != nullptr);
|
|
|
|
const auto item = notification.item;
|
|
const auto type = notification.type;
|
|
const auto thread = item->notificationThread();
|
|
const auto skip = skipNotification(notification);
|
|
if (skip.value == SkipState::Skip) {
|
|
thread->popNotification(notification);
|
|
return;
|
|
}
|
|
const auto ready = (skip.value != SkipState::Unknown)
|
|
&& item->notificationReady();
|
|
|
|
const auto minimalDelay = (type == Data::ItemNotificationType::Reaction)
|
|
? kMinimalDelay
|
|
: item->Has<HistoryMessageForwarded>()
|
|
? kMinimalForwardDelay
|
|
: kMinimalDelay;
|
|
const auto timing = countTiming(thread, minimalDelay);
|
|
const auto notifyBy = (type == Data::ItemNotificationType::Message)
|
|
? item->specialNotificationPeer()
|
|
: notification.reactionSender;
|
|
if (!skip.silent) {
|
|
registerThread(thread);
|
|
_whenAlerts[thread].emplace(timing.when, notifyBy);
|
|
}
|
|
if (Core::App().settings().desktopNotify()
|
|
&& !_manager->skipToast()) {
|
|
registerThread(thread);
|
|
const auto key = NotificationInHistoryKey(notification);
|
|
auto &whenMap = _whenMaps[thread];
|
|
if (whenMap.find(key) == whenMap.end()) {
|
|
whenMap.emplace(key, timing.when);
|
|
}
|
|
|
|
auto &addTo = ready ? _waiters : _settingWaiters;
|
|
const auto it = addTo.find(thread);
|
|
if (it == addTo.end() || it->second.when > timing.when) {
|
|
addTo.emplace(thread, Waiter{
|
|
.key = key,
|
|
.reactionSender = notification.reactionSender,
|
|
.type = notification.type,
|
|
.when = timing.when,
|
|
});
|
|
}
|
|
}
|
|
if (ready) {
|
|
if (!_waitTimer.isActive()
|
|
|| _waitTimer.remainingTime() > timing.delay) {
|
|
_waitTimer.callOnce(timing.delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
void System::clearAll() {
|
|
if (_manager) {
|
|
_manager->clearAll();
|
|
}
|
|
|
|
for (const auto &[thread, _] : _whenMaps) {
|
|
thread->clearNotifications();
|
|
}
|
|
_whenMaps.clear();
|
|
_whenAlerts.clear();
|
|
_waiters.clear();
|
|
_settingWaiters.clear();
|
|
_watchedTopics.clear();
|
|
}
|
|
|
|
void System::clearFromTopic(not_null<Data::ForumTopic*> topic) {
|
|
if (_manager) {
|
|
_manager->clearFromTopic(topic);
|
|
}
|
|
|
|
topic->clearNotifications();
|
|
_whenMaps.remove(topic);
|
|
_whenAlerts.remove(topic);
|
|
_waiters.remove(topic);
|
|
_settingWaiters.remove(topic);
|
|
|
|
_watchedTopics.remove(topic);
|
|
|
|
_waitTimer.cancel();
|
|
showNext();
|
|
}
|
|
|
|
void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) {
|
|
for (auto i = _whenMaps.begin(); i != _whenMaps.end();) {
|
|
const auto thread = i->first;
|
|
if (!predicate(thread)) {
|
|
++i;
|
|
continue;
|
|
}
|
|
i = _whenMaps.erase(i);
|
|
|
|
thread->clearNotifications();
|
|
_whenAlerts.remove(thread);
|
|
_waiters.remove(thread);
|
|
_settingWaiters.remove(thread);
|
|
if (const auto topic = thread->asTopic()) {
|
|
_watchedTopics.remove(topic);
|
|
}
|
|
}
|
|
const auto clearFrom = [&](auto &map) {
|
|
for (auto i = map.begin(); i != map.end();) {
|
|
const auto thread = i->first;
|
|
if (predicate(thread)) {
|
|
if (const auto topic = thread->asTopic()) {
|
|
_watchedTopics.remove(topic);
|
|
}
|
|
i = map.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
};
|
|
clearFrom(_whenAlerts);
|
|
clearFrom(_waiters);
|
|
clearFrom(_settingWaiters);
|
|
|
|
_waitTimer.cancel();
|
|
showNext();
|
|
}
|
|
|
|
void System::clearFromHistory(not_null<History*> history) {
|
|
if (_manager) {
|
|
_manager->clearFromHistory(history);
|
|
}
|
|
clearForThreadIf([&](not_null<Data::Thread*> thread) {
|
|
return (thread->owningHistory() == history);
|
|
});
|
|
}
|
|
|
|
void System::clearFromSession(not_null<Main::Session*> session) {
|
|
if (_manager) {
|
|
_manager->clearFromSession(session);
|
|
}
|
|
clearForThreadIf([&](not_null<Data::Thread*> thread) {
|
|
return (&thread->session() == session);
|
|
});
|
|
}
|
|
|
|
void System::clearIncomingFromHistory(not_null<History*> history) {
|
|
if (_manager) {
|
|
_manager->clearFromHistory(history);
|
|
}
|
|
history->clearIncomingNotifications();
|
|
_whenAlerts.remove(history);
|
|
}
|
|
|
|
void System::clearIncomingFromTopic(not_null<Data::ForumTopic*> topic) {
|
|
if (_manager) {
|
|
_manager->clearFromTopic(topic);
|
|
}
|
|
topic->clearIncomingNotifications();
|
|
_whenAlerts.remove(topic);
|
|
}
|
|
|
|
void System::clearFromItem(not_null<HistoryItem*> item) {
|
|
if (_manager) {
|
|
_manager->clearFromItem(item);
|
|
}
|
|
}
|
|
|
|
void System::clearAllFast() {
|
|
if (_manager) {
|
|
_manager->clearAllFast();
|
|
}
|
|
|
|
_whenMaps.clear();
|
|
_whenAlerts.clear();
|
|
_waiters.clear();
|
|
_settingWaiters.clear();
|
|
_watchedTopics.clear();
|
|
}
|
|
|
|
void System::checkDelayed() {
|
|
for (auto i = _settingWaiters.begin(); i != _settingWaiters.end();) {
|
|
const auto remove = [&] {
|
|
const auto thread = i->first;
|
|
const auto peer = thread->peer();
|
|
const auto fullId = FullMsgId(peer->id, i->second.key.messageId);
|
|
const auto item = thread->owner().message(fullId);
|
|
if (!item) {
|
|
return true;
|
|
}
|
|
const auto state = computeSkipState({
|
|
.item = item,
|
|
.reactionSender = i->second.reactionSender,
|
|
.type = i->second.type,
|
|
});
|
|
if (state.value == SkipState::Skip) {
|
|
return true;
|
|
} else if (state.value == SkipState::Unknown
|
|
|| !item->notificationReady()) {
|
|
return false;
|
|
}
|
|
_waiters.emplace(i->first, i->second);
|
|
return true;
|
|
}();
|
|
if (remove) {
|
|
i = _settingWaiters.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
_waitTimer.cancel();
|
|
showNext();
|
|
}
|
|
|
|
void System::showGrouped() {
|
|
Expects(_manager != nullptr);
|
|
|
|
if (const auto session = findSession(_lastHistorySessionId)) {
|
|
if (const auto lastItem = session->data().message(_lastHistoryItemId)) {
|
|
_waitForAllGroupedTimer.cancel();
|
|
_manager->showNotification({
|
|
.item = lastItem,
|
|
.forwardedCount = _lastForwardedCount,
|
|
});
|
|
_lastForwardedCount = 0;
|
|
_lastHistoryItemId = FullMsgId();
|
|
_lastHistorySessionId = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void System::showNext() {
|
|
Expects(_manager != nullptr);
|
|
|
|
if (Core::Quitting()) {
|
|
return;
|
|
}
|
|
|
|
const auto isSameGroup = [=](HistoryItem *item) {
|
|
if (!_lastHistorySessionId || !_lastHistoryItemId || !item) {
|
|
return false;
|
|
} else if (item->history()->session().uniqueId()
|
|
!= _lastHistorySessionId) {
|
|
return false;
|
|
}
|
|
const auto lastItem = item->history()->owner().message(
|
|
_lastHistoryItemId);
|
|
if (lastItem) {
|
|
return (lastItem->groupId() == item->groupId())
|
|
|| (lastItem->author() == item->author());
|
|
}
|
|
return false;
|
|
};
|
|
|
|
auto ms = crl::now(), nextAlert = crl::time(0);
|
|
auto alertThread = (Data::Thread*)nullptr;
|
|
for (auto i = _whenAlerts.begin(); i != _whenAlerts.end();) {
|
|
while (!i->second.empty() && i->second.begin()->first <= ms) {
|
|
const auto thread = i->first;
|
|
const auto notifySettings = &thread->owner().notifySettings();
|
|
const auto threadUnknown = notifySettings->muteUnknown(thread);
|
|
const auto threadAlert = !threadUnknown
|
|
&& !notifySettings->isMuted(thread);
|
|
const auto from = i->second.begin()->second;
|
|
const auto fromUnknown = (!from
|
|
|| notifySettings->muteUnknown(from));
|
|
const auto fromAlert = !fromUnknown
|
|
&& !notifySettings->isMuted(from);
|
|
if (threadAlert || fromAlert) {
|
|
alertThread = thread;
|
|
}
|
|
while (!i->second.empty()
|
|
&& i->second.begin()->first <= ms + kMinimalAlertDelay) {
|
|
i->second.erase(i->second.begin());
|
|
}
|
|
}
|
|
if (i->second.empty()) {
|
|
i = _whenAlerts.erase(i);
|
|
} else {
|
|
if (!nextAlert || nextAlert > i->second.begin()->first) {
|
|
nextAlert = i->second.begin()->first;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
const auto &settings = Core::App().settings();
|
|
if (alertThread) {
|
|
if (settings.flashBounceNotify()) {
|
|
const auto peer = alertThread->peer();
|
|
if (const auto window = Core::App().windowFor(peer)) {
|
|
if (const auto controller = window->sessionController()) {
|
|
_manager->maybeFlashBounce(crl::guard(controller, [=] {
|
|
if (const auto handle = window->widget()->windowHandle()) {
|
|
handle->alert(kSystemAlertDuration);
|
|
// (handle, SLOT(_q_clearAlert())); in the future.
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
if (settings.soundNotify()) {
|
|
const auto owner = &alertThread->owner();
|
|
const auto id = owner->notifySettings().sound(alertThread).id;
|
|
_manager->maybePlaySound(crl::guard(&owner->session(), [=] {
|
|
const auto track = lookupSound(owner, id);
|
|
track->playOnce();
|
|
Media::Player::mixer()->suppressAll(track->getLengthMs());
|
|
Media::Player::mixer()->scheduleFaderCallback();
|
|
}));
|
|
}
|
|
}
|
|
|
|
if (_waiters.empty() || !settings.desktopNotify() || _manager->skipToast()) {
|
|
if (nextAlert) {
|
|
_waitTimer.callOnce(nextAlert - ms);
|
|
}
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
auto next = 0LL;
|
|
auto notify = std::optional<Data::ItemNotification>();
|
|
auto notifyThread = (Data::Thread*)nullptr;
|
|
for (auto i = _waiters.begin(); i != _waiters.end();) {
|
|
const auto thread = i->first;
|
|
auto current = thread->currentNotification();
|
|
if (current && current->item->id != i->second.key.messageId) {
|
|
auto j = _whenMaps.find(thread);
|
|
if (j == _whenMaps.end()) {
|
|
thread->clearNotifications();
|
|
i = _waiters.erase(i);
|
|
continue;
|
|
}
|
|
do {
|
|
auto k = j->second.find(*current);
|
|
if (k != j->second.cend()) {
|
|
i->second.key = k->first;
|
|
i->second.when = k->second;
|
|
break;
|
|
}
|
|
thread->skipNotification();
|
|
current = thread->currentNotification();
|
|
} while (current);
|
|
}
|
|
if (!current) {
|
|
_whenMaps.remove(thread);
|
|
i = _waiters.erase(i);
|
|
continue;
|
|
}
|
|
auto when = i->second.when;
|
|
if (!notify || next > when) {
|
|
next = when;
|
|
notify = current,
|
|
notifyThread = thread;
|
|
}
|
|
++i;
|
|
}
|
|
if (!notify) {
|
|
break;
|
|
} else if (next > ms) {
|
|
if (nextAlert && nextAlert < next) {
|
|
next = nextAlert;
|
|
nextAlert = 0;
|
|
}
|
|
_waitTimer.callOnce(next - ms);
|
|
break;
|
|
}
|
|
const auto notifyItem = notify->item;
|
|
const auto messageType = (notify->type
|
|
== Data::ItemNotificationType::Message);
|
|
const auto isForwarded = messageType
|
|
&& notifyItem->Has<HistoryMessageForwarded>();
|
|
const auto isAlbum = messageType
|
|
&& notifyItem->groupId();
|
|
|
|
// Forwarded and album notify grouping.
|
|
auto groupedItem = (isForwarded || isAlbum)
|
|
? notifyItem.get()
|
|
: nullptr;
|
|
auto forwardedCount = isForwarded ? 1 : 0;
|
|
|
|
const auto thread = notifyItem->notificationThread();
|
|
const auto j = _whenMaps.find(thread);
|
|
if (j == _whenMaps.cend()) {
|
|
thread->clearNotifications();
|
|
} else {
|
|
while (true) {
|
|
auto nextNotify = std::optional<Data::ItemNotification>();
|
|
thread->skipNotification();
|
|
if (!thread->hasNotification()) {
|
|
break;
|
|
}
|
|
|
|
j->second.remove({
|
|
(groupedItem ? groupedItem : notifyItem.get())->id,
|
|
notify->type,
|
|
});
|
|
do {
|
|
const auto k = j->second.find(
|
|
thread->currentNotification());
|
|
if (k != j->second.cend()) {
|
|
nextNotify = thread->currentNotification();
|
|
_waiters.emplace(notifyThread, Waiter{
|
|
.key = k->first,
|
|
.when = k->second
|
|
});
|
|
break;
|
|
}
|
|
thread->skipNotification();
|
|
} while (thread->hasNotification());
|
|
if (!nextNotify || !groupedItem) {
|
|
break;
|
|
}
|
|
const auto nextMessageNotification
|
|
= (nextNotify->type
|
|
== Data::ItemNotificationType::Message);
|
|
const auto canNextBeGrouped = nextMessageNotification
|
|
&& ((isForwarded
|
|
&& nextNotify->item->Has<HistoryMessageForwarded>())
|
|
|| (isAlbum && nextNotify->item->groupId()));
|
|
const auto nextItem = canNextBeGrouped
|
|
? nextNotify->item.get()
|
|
: nullptr;
|
|
if (nextItem
|
|
&& qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) {
|
|
if (isForwarded
|
|
&& groupedItem->author() == nextItem->author()) {
|
|
++forwardedCount;
|
|
groupedItem = nextItem;
|
|
continue;
|
|
}
|
|
if (isAlbum
|
|
&& groupedItem->groupId() == nextItem->groupId()) {
|
|
groupedItem = nextItem;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!_lastHistoryItemId && groupedItem) {
|
|
_lastHistorySessionId = groupedItem->history()->session().uniqueId();
|
|
_lastHistoryItemId = groupedItem->fullId();
|
|
}
|
|
|
|
// If the current notification is grouped.
|
|
if (isAlbum || isForwarded) {
|
|
// If the previous notification is grouped
|
|
// then reset the timer.
|
|
if (_waitForAllGroupedTimer.isActive()) {
|
|
_waitForAllGroupedTimer.cancel();
|
|
// If this is not the same group
|
|
// then show the previous group immediately.
|
|
if (!isSameGroup(groupedItem)) {
|
|
showGrouped();
|
|
}
|
|
}
|
|
// We have to wait until all the messages in this group are loaded.
|
|
_lastForwardedCount += forwardedCount;
|
|
_lastHistorySessionId = groupedItem->history()->session().uniqueId();
|
|
_lastHistoryItemId = groupedItem->fullId();
|
|
_waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay);
|
|
} else {
|
|
// If the current notification is not grouped
|
|
// then there is no reason to wait for the timer
|
|
// to show the previous notification.
|
|
showGrouped();
|
|
const auto reactionNotification
|
|
= (notify->type == Data::ItemNotificationType::Reaction);
|
|
const auto reaction = reactionNotification
|
|
? notify->item->lookupUnreadReaction(notify->reactionSender)
|
|
: Data::ReactionId();
|
|
if (!reactionNotification || !reaction.empty()) {
|
|
_manager->showNotification({
|
|
.item = notify->item,
|
|
.forwardedCount = forwardedCount,
|
|
.reactionFrom = notify->reactionSender,
|
|
.reactionId = reaction,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!thread->hasNotification()) {
|
|
_waiters.remove(thread);
|
|
_whenMaps.remove(thread);
|
|
}
|
|
}
|
|
if (nextAlert) {
|
|
_waitTimer.callOnce(nextAlert - ms);
|
|
}
|
|
}
|
|
|
|
not_null<Media::Audio::Track*> System::lookupSound(
|
|
not_null<Data::Session*> owner,
|
|
DocumentId id) {
|
|
if (!id) {
|
|
ensureSoundCreated();
|
|
return _soundTrack.get();
|
|
}
|
|
const auto i = _customSoundTracks.find(id);
|
|
if (i != end(_customSoundTracks)) {
|
|
return i->second.get();
|
|
}
|
|
const auto ¬ifySettings = owner->notifySettings();
|
|
const auto custom = notifySettings.lookupRingtone(id);
|
|
if (custom && !custom->bytes().isEmpty()) {
|
|
const auto j = _customSoundTracks.emplace(
|
|
id,
|
|
Media::Audio::Current().createTrack()
|
|
).first;
|
|
j->second->fillFromData(bytes::make_vector(custom->bytes()));
|
|
return j->second.get();
|
|
}
|
|
ensureSoundCreated();
|
|
return _soundTrack.get();
|
|
}
|
|
|
|
void System::ensureSoundCreated() {
|
|
if (_soundTrack) {
|
|
return;
|
|
}
|
|
|
|
_soundTrack = Media::Audio::Current().createTrack();
|
|
_soundTrack->fillFromFile(
|
|
Core::App().settings().getSoundPath(u"msg_incoming"_q));
|
|
}
|
|
|
|
void System::updateAll() {
|
|
if (_manager) {
|
|
_manager->updateAll();
|
|
}
|
|
}
|
|
|
|
rpl::producer<ChangeType> System::settingsChanged() const {
|
|
return _settingsChanged.events();
|
|
}
|
|
|
|
void System::notifySettingsChanged(ChangeType type) {
|
|
return _settingsChanged.fire(std::move(type));
|
|
}
|
|
|
|
void System::playSound(not_null<Main::Session*> session, DocumentId id) {
|
|
lookupSound(&session->data(), id)->playOnce();
|
|
}
|
|
|
|
Manager::DisplayOptions Manager::getNotificationOptions(
|
|
HistoryItem *item,
|
|
Data::ItemNotificationType type) const {
|
|
const auto hideEverything = Core::App().passcodeLocked()
|
|
|| forceHideDetails();
|
|
const auto view = Core::App().settings().notifyView();
|
|
const auto peer = item ? item->history()->peer.get() : nullptr;
|
|
const auto topic = item ? item->topic() : nullptr;
|
|
|
|
auto result = DisplayOptions();
|
|
result.hideNameAndPhoto = hideEverything
|
|
|| (view > Core::Settings::NotifyView::ShowName);
|
|
result.hideMessageText = hideEverything
|
|
|| (view > Core::Settings::NotifyView::ShowPreview);
|
|
result.hideMarkAsRead = result.hideMessageText
|
|
|| (type != Data::ItemNotificationType::Message)
|
|
|| !item
|
|
|| ((item->out() || peer->isSelf()) && item->isFromScheduled());
|
|
result.hideReplyButton = result.hideMarkAsRead
|
|
|| (!Data::CanSendTexts(peer)
|
|
&& (!topic || !Data::CanSendTexts(topic)))
|
|
|| peer->isBroadcast()
|
|
|| (peer->slowmodeSecondsLeft() > 0);
|
|
result.spoilerLoginCode = item
|
|
&& !item->out()
|
|
&& peer->isNotificationsUser()
|
|
&& Core::App().isSharingScreen();
|
|
return result;
|
|
}
|
|
|
|
TextWithEntities Manager::ComposeReactionEmoji(
|
|
not_null<Main::Session*> session,
|
|
const Data::ReactionId &reaction) {
|
|
if (const auto emoji = std::get_if<QString>(&reaction.data)) {
|
|
return TextWithEntities{ *emoji };
|
|
}
|
|
const auto id = v::get<DocumentId>(reaction.data);
|
|
auto entities = EntitiesInText();
|
|
const auto document = session->data().document(id);
|
|
const auto sticker = document->sticker();
|
|
const auto text = sticker ? sticker->alt : PlaceholderReactionText();
|
|
return TextWithEntities{
|
|
text,
|
|
{
|
|
EntityInText(
|
|
EntityType::CustomEmoji,
|
|
0,
|
|
text.size(),
|
|
Data::SerializeCustomEmojiId(id))
|
|
}
|
|
};
|
|
}
|
|
|
|
TextWithEntities Manager::ComposeReactionNotification(
|
|
not_null<HistoryItem*> item,
|
|
const Data::ReactionId &reaction,
|
|
bool hideContent) {
|
|
const auto reactionWithEntities = ComposeReactionEmoji(
|
|
&item->history()->session(),
|
|
reaction);
|
|
const auto simple = [&](const auto &phrase) {
|
|
return phrase(
|
|
tr::now,
|
|
lt_reaction,
|
|
reactionWithEntities,
|
|
Ui::Text::WithEntities);
|
|
};
|
|
if (hideContent) {
|
|
return simple(tr::lng_reaction_notext);
|
|
}
|
|
const auto media = item->media();
|
|
const auto text = [&] {
|
|
return tr::lng_reaction_text(
|
|
tr::now,
|
|
lt_reaction,
|
|
reactionWithEntities,
|
|
lt_text,
|
|
item->notificationText(),
|
|
Ui::Text::WithEntities);
|
|
};
|
|
if (!media || media->webpage()) {
|
|
return text();
|
|
} else if (media->photo()) {
|
|
return simple(tr::lng_reaction_photo);
|
|
} else if (const auto document = media->document()) {
|
|
if (document->isVoiceMessage()) {
|
|
return simple(tr::lng_reaction_voice_message);
|
|
} else if (document->isVideoMessage()) {
|
|
return simple(tr::lng_reaction_video_message);
|
|
} else if (document->isAnimation()) {
|
|
return simple(tr::lng_reaction_gif);
|
|
} else if (document->isVideoFile()) {
|
|
return simple(tr::lng_reaction_video);
|
|
} else if (const auto sticker = document->sticker()) {
|
|
return tr::lng_reaction_sticker(
|
|
tr::now,
|
|
lt_reaction,
|
|
reactionWithEntities,
|
|
lt_emoji,
|
|
Ui::Text::WithEntities(sticker->alt),
|
|
Ui::Text::WithEntities);
|
|
}
|
|
return simple(tr::lng_reaction_document);
|
|
} else if (const auto contact = media->sharedContact()) {
|
|
const auto name = contact->firstName.isEmpty()
|
|
? contact->lastName
|
|
: contact->lastName.isEmpty()
|
|
? contact->firstName
|
|
: tr::lng_full_name(
|
|
tr::now,
|
|
lt_first_name,
|
|
contact->firstName,
|
|
lt_last_name,
|
|
contact->lastName);
|
|
return tr::lng_reaction_contact(
|
|
tr::now,
|
|
lt_reaction,
|
|
reactionWithEntities,
|
|
lt_name,
|
|
Ui::Text::WithEntities(name),
|
|
Ui::Text::WithEntities);
|
|
} else if (media->location()) {
|
|
return simple(tr::lng_reaction_location);
|
|
// lng_reaction_live_location not used right now :(
|
|
} else if (const auto poll = media->poll()) {
|
|
return (poll->quiz()
|
|
? tr::lng_reaction_quiz
|
|
: tr::lng_reaction_poll)(
|
|
tr::now,
|
|
lt_reaction,
|
|
reactionWithEntities,
|
|
lt_title,
|
|
Ui::Text::WithEntities(poll->question),
|
|
Ui::Text::WithEntities);
|
|
} else if (media->game()) {
|
|
return simple(tr::lng_reaction_game);
|
|
} else if (media->invoice()) {
|
|
return simple(tr::lng_reaction_invoice);
|
|
}
|
|
return text();
|
|
}
|
|
|
|
TextWithEntities Manager::addTargetAccountName(
|
|
TextWithEntities title,
|
|
not_null<Main::Session*> session) {
|
|
const auto add = [&] {
|
|
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
|
if (const auto other = account->maybeSession()) {
|
|
if (other != session) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}();
|
|
if (!add) {
|
|
return title;
|
|
}
|
|
return title.append(accountNameSeparator()).append(
|
|
(session->user()->username().isEmpty()
|
|
? session->user()->name()
|
|
: session->user()->username()));
|
|
}
|
|
|
|
QString Manager::addTargetAccountName(
|
|
const QString &title,
|
|
not_null<Main::Session*> session) {
|
|
return addTargetAccountName(TextWithEntities{ title }, session).text;
|
|
}
|
|
|
|
QString Manager::accountNameSeparator() {
|
|
return QString::fromUtf8(" \xE2\x9E\x9C ");
|
|
}
|
|
|
|
void Manager::notificationActivated(
|
|
NotificationId id,
|
|
const TextWithTags &reply) {
|
|
onBeforeNotificationActivated(id);
|
|
if (const auto session = system()->findSession(id.contextId.sessionId)) {
|
|
if (session->windows().empty()) {
|
|
Core::App().domain().activate(&session->account());
|
|
}
|
|
if (!session->windows().empty()) {
|
|
const auto window = session->windows().front();
|
|
const auto history = session->data().history(
|
|
id.contextId.peerId);
|
|
const auto item = history->owner().message(
|
|
history->peer,
|
|
id.msgId);
|
|
const auto topic = item ? item->topic() : nullptr;
|
|
if (!reply.text.isEmpty()) {
|
|
const auto topicRootId = topic
|
|
? topic->rootId()
|
|
: id.contextId.topicRootId;
|
|
const auto replyToId = (id.msgId > 0
|
|
&& !history->peer->isUser()
|
|
&& id.msgId != topicRootId)
|
|
? id.msgId
|
|
: 0;
|
|
auto draft = std::make_unique<Data::Draft>(
|
|
reply,
|
|
replyToId,
|
|
topicRootId,
|
|
MessageCursor{
|
|
int(reply.text.size()),
|
|
int(reply.text.size()),
|
|
QFIXED_MAX,
|
|
},
|
|
Data::PreviewState::Allowed);
|
|
history->setLocalDraft(std::move(draft));
|
|
}
|
|
window->widget()->showFromTray();
|
|
window->widget()->reActivateWindow();
|
|
if (Core::App().passcodeLocked()) {
|
|
window->widget()->setInnerFocus();
|
|
system()->clearAll();
|
|
} else {
|
|
openNotificationMessage(history, id.msgId);
|
|
}
|
|
onAfterNotificationActivated(id, window);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Manager::openNotificationMessage(
|
|
not_null<History*> history,
|
|
MsgId messageId) {
|
|
const auto item = history->owner().message(history->peer, messageId);
|
|
const auto openExactlyMessage = !history->peer->isBroadcast()
|
|
&& item
|
|
&& item->isRegular()
|
|
&& (item->out() || (item->mentionsMe() && !history->peer->isUser()));
|
|
const auto topic = item ? item->topic() : nullptr;
|
|
const auto separate = Core::App().separateWindowForPeer(history->peer);
|
|
const auto window = separate
|
|
? separate->sessionController()
|
|
: history->session().tryResolveWindow();
|
|
const auto itemId = openExactlyMessage ? messageId : ShowAtUnreadMsgId;
|
|
if (window) {
|
|
if (topic) {
|
|
window->showSection(
|
|
std::make_shared<HistoryView::RepliesMemento>(
|
|
history,
|
|
topic->rootId(),
|
|
itemId),
|
|
SectionShow::Way::Forward);
|
|
} else {
|
|
window->showPeerHistory(
|
|
history->peer->id,
|
|
SectionShow::Way::Forward,
|
|
itemId);
|
|
}
|
|
}
|
|
if (topic) {
|
|
system()->clearFromTopic(topic);
|
|
} else {
|
|
system()->clearFromHistory(history);
|
|
}
|
|
}
|
|
|
|
void Manager::notificationReplied(
|
|
NotificationId id,
|
|
const TextWithTags &reply) {
|
|
if (!id.contextId.sessionId || !id.contextId.peerId) {
|
|
return;
|
|
}
|
|
|
|
const auto session = system()->findSession(id.contextId.sessionId);
|
|
if (!session) {
|
|
return;
|
|
}
|
|
const auto history = session->data().history(id.contextId.peerId);
|
|
const auto item = history->owner().message(history->peer, id.msgId);
|
|
const auto topic = item ? item->topic() : nullptr;
|
|
const auto topicRootId = topic
|
|
? topic->rootId()
|
|
: id.contextId.topicRootId;
|
|
|
|
auto message = Api::MessageToSend(Api::SendAction(history));
|
|
message.textWithTags = reply;
|
|
message.action.replyTo = (id.msgId > 0 && !history->peer->isUser()
|
|
&& id.msgId != topicRootId)
|
|
? id.msgId
|
|
: history->peer->isForum()
|
|
? topicRootId
|
|
: MsgId(0);
|
|
message.action.topicRootId = topic ? topic->rootId() : 0;
|
|
message.action.clearDraft = false;
|
|
history->session().api().sendMessage(std::move(message));
|
|
|
|
if (item && item->isUnreadMention() && !item->isIncomingUnreadMedia()) {
|
|
history->session().api().markContentsRead(item);
|
|
}
|
|
}
|
|
|
|
void NativeManager::doShowNotification(NotificationFields &&fields) {
|
|
const auto options = getNotificationOptions(
|
|
fields.item,
|
|
(fields.reactionFrom
|
|
? Data::ItemNotificationType::Reaction
|
|
: Data::ItemNotificationType::Message));
|
|
const auto item = fields.item;
|
|
const auto peer = item->history()->peer;
|
|
const auto reactionFrom = fields.reactionFrom;
|
|
if (reactionFrom && options.hideNameAndPhoto) {
|
|
return;
|
|
}
|
|
const auto scheduled = !options.hideNameAndPhoto
|
|
&& !reactionFrom
|
|
&& (item->out() || peer->isSelf())
|
|
&& item->isFromScheduled();
|
|
const auto topicWithChat = [&] {
|
|
const auto name = peer->name();
|
|
const auto topic = item->topic();
|
|
return topic ? (topic->title() + u" ("_q + name + ')') : name;
|
|
};
|
|
const auto title = options.hideNameAndPhoto
|
|
? AppName.utf16()
|
|
: (scheduled && peer->isSelf())
|
|
? tr::lng_notification_reminder(tr::now)
|
|
: topicWithChat();
|
|
const auto fullTitle = addTargetAccountName(title, &peer->session());
|
|
const auto subtitle = reactionFrom
|
|
? (reactionFrom != peer ? reactionFrom->name() : QString())
|
|
: options.hideNameAndPhoto
|
|
? QString()
|
|
: item->notificationHeader();
|
|
const auto text = reactionFrom
|
|
? TextWithPermanentSpoiler(ComposeReactionNotification(
|
|
item,
|
|
fields.reactionId,
|
|
options.hideMessageText))
|
|
: options.hideMessageText
|
|
? tr::lng_notification_preview(tr::now)
|
|
: (fields.forwardedCount > 1)
|
|
? tr::lng_forward_messages(tr::now, lt_count, fields.forwardedCount)
|
|
: item->groupId()
|
|
? tr::lng_in_dlg_album(tr::now)
|
|
: TextWithPermanentSpoiler(item->notificationText({
|
|
.spoilerLoginCode = options.spoilerLoginCode,
|
|
}));
|
|
|
|
// #TODO optimize
|
|
auto userpicView = item->history()->peer->createUserpicView();
|
|
doShowNativeNotification(
|
|
item->history()->peer,
|
|
item->topicRootId(),
|
|
userpicView,
|
|
item->id,
|
|
scheduled ? WrapFromScheduled(fullTitle) : fullTitle,
|
|
subtitle,
|
|
text,
|
|
options);
|
|
}
|
|
|
|
bool NativeManager::forceHideDetails() const {
|
|
return Core::App().screenIsLocked();
|
|
}
|
|
|
|
System::~System() = default;
|
|
|
|
QString WrapFromScheduled(const QString &text) {
|
|
return QString::fromUtf8("\xF0\x9F\x93\x85 ") + text;
|
|
}
|
|
|
|
} // namespace Notifications
|
|
} // namespace Window
|