/* 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_default.h" #include "platform/platform_notifications_manager.h" #include "platform/platform_specific.h" #include "core/application.h" #include "core/ui_integration.h" #include "chat_helpers/message_field.h" #include "lang/lang_keys.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/input_field.h" #include "ui/platform/ui_platform_utility.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/emoji_config.h" #include "ui/empty_userpic.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" #include "data/data_session.h" #include "data/data_forum_topic.h" #include "data/stickers/data_custom_emoji.h" #include "dialogs/ui/dialogs_layout.h" #include "window/window_controller.h" #include "storage/file_download.h" #include "main/main_session.h" #include "main/main_account.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_item_preview.h" #include "base/platform/base_platform_last_input.h" #include "base/call_delayed.h" #include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_window.h" #include #include namespace Window { namespace Notifications { namespace Default { namespace { [[nodiscard]] QPoint notificationStartPosition() { const auto corner = Core::App().settings().notificationsCorner(); const auto window = Core::App().activePrimaryWindow(); const auto r = window ? window->widget()->desktopRect() : QGuiApplication::primaryScreen()->availableGeometry(); const auto isLeft = Core::Settings::IsLeftCorner(corner); const auto isTop = Core::Settings::IsTopCorner(corner); const auto x = (isLeft == rtl()) ? (r.x() + r.width() - st::notifyWidth - st::notifyDeltaX) : (r.x() + st::notifyDeltaX); const auto y = isTop ? r.y() : (r.y() + r.height()); return QPoint(x, y); } internal::Widget::Direction notificationShiftDirection() { auto isTop = Core::Settings::IsTopCorner(Core::App().settings().notificationsCorner()); return isTop ? internal::Widget::Direction::Down : internal::Widget::Direction::Up; } } // namespace std::unique_ptr Create(System *system) { return std::make_unique(system); } Manager::Manager(System *system) : Notifications::Manager(system) , _inputCheckTimer([=] { checkLastInput(); }) { system->settingsChanged( ) | rpl::start_with_next([=](ChangeType change) { settingsChanged(change); }, _lifetime); } Manager::QueuedNotification::QueuedNotification(NotificationFields &&fields) : history(fields.item->history()) , topicRootId(fields.item->topicRootId()) , peer(history->peer) , reaction(fields.reactionId) , author(!fields.reactionFrom ? fields.item->notificationHeader() : (fields.reactionFrom != peer) ? fields.reactionFrom->name() : QString()) , item((fields.forwardedCount < 2) ? fields.item.get() : nullptr) , forwardedCount(fields.forwardedCount) , fromScheduled(reaction.empty() && (fields.item->out() || peer->isSelf()) && fields.item->isFromScheduled()) { } QPixmap Manager::hiddenUserpicPlaceholder() const { if (_hiddenUserpicPlaceholder.isNull()) { const auto ratio = style::DevicePixelRatio(); _hiddenUserpicPlaceholder = Ui::PixmapFromImage( LogoNoMargin().scaled( st::notifyPhotoSize * ratio, st::notifyPhotoSize * ratio, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); _hiddenUserpicPlaceholder.setDevicePixelRatio(ratio); } return _hiddenUserpicPlaceholder; } bool Manager::hasReplyingNotification() const { for (const auto ¬ification : _notifications) { if (notification->isReplying()) { return true; } } return false; } void Manager::settingsChanged(ChangeType change) { if (change == ChangeType::Corner) { auto startPosition = notificationStartPosition(); auto shiftDirection = notificationShiftDirection(); for (const auto ¬ification : _notifications) { notification->updatePosition(startPosition, shiftDirection); } if (_hideAll) { _hideAll->updatePosition(startPosition, shiftDirection); } } else if (change == ChangeType::MaxCount) { int allow = Core::App().settings().notificationsCount(); for (int i = _notifications.size(); i != 0;) { auto ¬ification = _notifications[--i]; if (notification->isUnlinked()) continue; if (--allow < 0) { notification->unlinkHistory(); } } if (allow > 0) { for (int i = 0; i != allow; ++i) { showNextFromQueue(); } } } else if ((change == ChangeType::DemoIsShown) || (change == ChangeType::DemoIsHidden)) { _demoIsShown = (change == ChangeType::DemoIsShown); _demoMasterOpacity.start( [=] { demoMasterOpacityCallback(); }, _demoIsShown ? 1. : 0., _demoIsShown ? 0. : 1., st::notifyFastAnim); } } void Manager::demoMasterOpacityCallback() { for (const auto ¬ification : _notifications) { notification->updateOpacity(); } if (_hideAll) { _hideAll->updateOpacity(); } } float64 Manager::demoMasterOpacity() const { return _demoMasterOpacity.value(_demoIsShown ? 0. : 1.); } void Manager::checkLastInput() { auto replying = hasReplyingNotification(); auto waiting = false; const auto lastInputTime = base::Platform::LastUserInputTimeSupported() ? std::make_optional(Core::App().lastNonIdleTime()) : std::nullopt; for (const auto ¬ification : _notifications) { if (!notification->checkLastInput(replying, lastInputTime)) { waiting = true; } } if (waiting) { _inputCheckTimer.callOnce(300); } } void Manager::startAllHiding() { if (!hasReplyingNotification()) { int notHidingCount = 0; for (const auto ¬ification : _notifications) { if (notification->isShowing()) { ++notHidingCount; } else { notification->startHiding(); } } notHidingCount += _queuedNotifications.size(); if (_hideAll && notHidingCount < 2) { _hideAll->startHiding(); } } } void Manager::stopAllHiding() { for (const auto ¬ification : _notifications) { notification->stopHiding(); } if (_hideAll) { _hideAll->stopHiding(); } } void Manager::showNextFromQueue() { auto guard = gsl::finally([this] { if (_positionsOutdated) { moveWidgets(); } }); if (_queuedNotifications.empty()) { return; } int count = Core::App().settings().notificationsCount(); for (const auto ¬ification : _notifications) { if (notification->isUnlinked()) continue; --count; } if (count <= 0) { return; } auto startPosition = notificationStartPosition(); auto startShift = 0; auto shiftDirection = notificationShiftDirection(); do { auto queued = _queuedNotifications.front(); _queuedNotifications.pop_front(); subscribeToSession(&queued.history->session()); _notifications.push_back(std::make_unique( this, queued.history, queued.topicRootId, queued.peer, queued.author, queued.item, queued.reaction, queued.forwardedCount, queued.fromScheduled, startPosition, startShift, shiftDirection)); --count; } while (count > 0 && !_queuedNotifications.empty()); _positionsOutdated = true; checkLastInput(); } void Manager::subscribeToSession(not_null session) { auto i = _subscriptions.find(session); if (i == _subscriptions.end()) { i = _subscriptions.emplace(session).first; session->account().sessionChanges( ) | rpl::start_with_next([=] { _subscriptions.remove(session); }, i->second.lifetime); } else if (i->second.subscription) { return; } session->downloaderTaskFinished( ) | rpl::start_with_next([=] { auto found = false; for (const auto ¬ification : _notifications) { if (const auto history = notification->maybeHistory()) { if (&history->session() == session) { notification->updatePeerPhoto(); found = true; } } } if (!found) { _subscriptions[session].subscription.destroy(); } }, i->second.subscription); } void Manager::moveWidgets() { auto shift = st::notifyDeltaY; int lastShift = 0, lastShiftCurrent = 0, count = 0; for (int i = _notifications.size(); i != 0;) { auto ¬ification = _notifications[--i]; if (notification->isUnlinked()) continue; notification->changeShift(shift); shift += notification->height() + st::notifyDeltaY; lastShiftCurrent = notification->currentShift(); lastShift = shift; ++count; } if (count > 1 || !_queuedNotifications.empty()) { if (!_hideAll) { _hideAll = std::make_unique(this, notificationStartPosition(), lastShiftCurrent, notificationShiftDirection()); } _hideAll->changeShift(lastShift); _hideAll->stopHiding(); } else if (_hideAll) { _hideAll->startHidingFast(); } } void Manager::changeNotificationHeight(Notification *notification, int newHeight) { auto deltaHeight = newHeight - notification->height(); if (!deltaHeight) return; notification->addToHeight(deltaHeight); auto it = std::find_if(_notifications.cbegin(), _notifications.cend(), [notification](auto &item) { return (item.get() == notification); }); if (it != _notifications.cend()) { for (auto i = _notifications.cbegin(); i != it; ++i) { auto ¬ification = *i; if (notification->isUnlinked()) continue; notification->addToShift(deltaHeight); } } if (_hideAll) { _hideAll->addToShift(deltaHeight); } } void Manager::unlinkFromShown(Notification *remove) { if (remove) { if (remove->unlinkHistory()) { _positionsOutdated = true; } } showNextFromQueue(); } void Manager::removeWidget(internal::Widget *remove) { if (remove == _hideAll.get()) { _hideAll.reset(); } else if (remove) { const auto it = ranges::find( _notifications, remove, &std::unique_ptr::get); if (it != end(_notifications)) { _notifications.erase(it); _positionsOutdated = true; } } showNextFromQueue(); } void Manager::doShowNotification(NotificationFields &&fields) { _queuedNotifications.emplace_back(std::move(fields)); showNextFromQueue(); } void Manager::doClearAll() { _queuedNotifications.clear(); for (const auto ¬ification : _notifications) { notification->unlinkHistory(); } showNextFromQueue(); } void Manager::doClearAllFast() { _queuedNotifications.clear(); base::take(_notifications); base::take(_hideAll); } void Manager::doClearFromTopic(not_null topic) { const auto history = topic->history(); const auto topicRootId = topic->rootId(); for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) { if (i->history == history && i->topicRootId == topicRootId) { i = _queuedNotifications.erase(i); } else { ++i; } } for (const auto ¬ification : _notifications) { if (notification->unlinkHistory(history, topicRootId)) { _positionsOutdated = true; } } showNextFromQueue(); } void Manager::doClearFromHistory(not_null history) { for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) { if (i->history == history) { i = _queuedNotifications.erase(i); } else { ++i; } } for (const auto ¬ification : _notifications) { if (notification->unlinkHistory(history)) { _positionsOutdated = true; } } showNextFromQueue(); } void Manager::doClearFromSession(not_null session) { for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) { if (&i->history->session() == session) { i = _queuedNotifications.erase(i); } else { ++i; } } for (const auto ¬ification : _notifications) { if (notification->unlinkSession(session)) { _positionsOutdated = true; } } showNextFromQueue(); } void Manager::doClearFromItem(not_null item) { _queuedNotifications.erase(std::remove_if(_queuedNotifications.begin(), _queuedNotifications.end(), [&](auto &queued) { return (queued.item == item); }), _queuedNotifications.cend()); auto showNext = false; for (const auto ¬ification : _notifications) { if (notification->unlinkItem(item)) { showNext = true; } } if (showNext) { // This call invalidates _notifications iterators. showNextFromQueue(); } } bool Manager::doSkipToast() const { return Platform::Notifications::SkipToastForCustom(); } void Manager::doMaybePlaySound(Fn playSound) { Platform::Notifications::MaybePlaySoundForCustom(std::move(playSound)); } void Manager::doMaybeFlashBounce(Fn flashBounce) { Platform::Notifications::MaybeFlashBounceForCustom(std::move(flashBounce)); } void Manager::doUpdateAll() { for (const auto ¬ification : _notifications) { notification->updateNotifyDisplay(); } } Manager::~Manager() { clearAllFast(); } namespace internal { Widget::Widget( not_null manager, QPoint startPosition, int shift, Direction shiftDirection) : _manager(manager) , _startPosition(startPosition) , _direction(shiftDirection) , _shift(shift) , _shiftAnimation([=](crl::time now) { return shiftAnimationCallback(now); }) { setWindowOpacity(0.); setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::WindowStaysOnTopHint | Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::Tool); setAttribute(Qt::WA_MacAlwaysShowToolWindow); setAttribute(Qt::WA_OpaquePaintEvent); Ui::Platform::InitOnTopPanel(this); _a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim); } void Widget::destroyDelayed() { hide(); if (_deleted) return; _deleted = true; // Ubuntu has a lag if a fully transparent widget is destroyed immediately. base::call_delayed(1000, this, [this] { manager()->removeWidget(this); }); } void Widget::opacityAnimationCallback() { updateOpacity(); update(); if (!_a_opacity.animating() && _hiding) { destroyDelayed(); } } bool Widget::shiftAnimationCallback(crl::time now) { if (anim::Disabled()) { now += st::notifyFastAnim; } const auto dt = (now - _shiftAnimation.started()) / float64(st::notifyFastAnim); if (dt >= 1.) { _shift.finish(); } else { _shift.update(dt, anim::linear); } moveByShift(); return (dt < 1.); } void Widget::hideSlow() { if (anim::Disabled()) { _hiding = true; base::call_delayed( st::notifySlowHide, this, [=, guard = _hidingDelayed.make_guard()] { if (guard && _hiding) { hideFast(); } }); } else { hideAnimated(st::notifySlowHide, anim::easeInCirc); } } void Widget::hideFast() { hideAnimated(st::notifyFastAnim, anim::linear); } void Widget::hideStop() { if (_hiding) { _hiding = false; _hidingDelayed = {}; _a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim); } } void Widget::hideAnimated(float64 duration, const anim::transition &func) { _hiding = true; _a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., duration, func); } void Widget::updateOpacity() { setWindowOpacity(_a_opacity.value(_hiding ? 0. : 1.) * _manager->demoMasterOpacity()); } void Widget::changeShift(int top) { _shift.start(top); _shiftAnimation.start(); } void Widget::updatePosition(QPoint startPosition, Direction shiftDirection) { _startPosition = startPosition; _direction = shiftDirection; moveByShift(); } void Widget::addToHeight(int add) { auto newHeight = height() + add; auto newPosition = computePosition(newHeight); updateGeometry(newPosition.x(), newPosition.y(), width(), newHeight); Ui::Platform::UpdateOverlayed(this); } void Widget::updateGeometry(int x, int y, int width, int height) { setGeometry(x, y, width, height); setMinimumSize(QSize(width, height)); setMaximumSize(QSize(width, height)); update(); } void Widget::addToShift(int add) { _shift.add(add); moveByShift(); } void Widget::moveByShift() { move(computePosition(height())); } QPoint Widget::computePosition(int height) const { auto realShift = qRound(_shift.current()); if (_direction == Direction::Up) { realShift = -realShift - height; } return QPoint(_startPosition.x(), _startPosition.y() + realShift); } Background::Background(QWidget *parent) : TWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); } void Background::paintEvent(QPaintEvent *e) { Painter p(this); p.fillRect(rect(), st::notificationBg); p.fillRect(0, 0, st::notifyBorderWidth, height(), st::notifyBorder); p.fillRect(width() - st::notifyBorderWidth, 0, st::notifyBorderWidth, height(), st::notifyBorder); p.fillRect(st::notifyBorderWidth, height() - st::notifyBorderWidth, width() - 2 * st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder); } Notification::Notification( not_null manager, not_null history, MsgId topicRootId, not_null peer, const QString &author, HistoryItem *item, const Data::ReactionId &reaction, int forwardedCount, bool fromScheduled, QPoint startPosition, int shift, Direction shiftDirection) : Widget(manager, startPosition, shift, shiftDirection) , _peer(peer) , _started(crl::now()) , _history(history) , _topic(history->peer->forumTopicFor(topicRootId)) , _topicRootId(topicRootId) , _userpicView(_peer->createUserpicView()) , _author(author) , _reaction(reaction) , _item(item) , _forwardedCount(forwardedCount) , _fromScheduled(fromScheduled) , _close(this, st::notifyClose) , _reply(this, tr::lng_notification_reply(), st::defaultBoxButton) { Lang::Updated( ) | rpl::start_with_next([=] { refreshLang(); }, lifetime()); if (_topic) { _topic->destroyed( ) | rpl::start_with_next([=] { unlinkHistory(); }, lifetime()); } auto position = computePosition(st::notifyMinHeight); updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyMinHeight); _userpicLoaded = !Ui::PeerUserpicLoading(_userpicView); updateNotifyDisplay(); _hideTimer.setSingleShot(true); connect(&_hideTimer, &QTimer::timeout, [=] { startHiding(); }); _close->setClickedCallback([this] { unlinkHistoryInManager(); }); _close->setAcceptBoth(true); _close->moveToRight(st::notifyClosePos.x(), st::notifyClosePos.y()); _close->show(); _reply->setClickedCallback([this] { showReplyField(); }); _replyPadding = st::notifyMinHeight - st::notifyPhotoPos.y() - st::notifyPhotoSize; updateReplyGeometry(); _reply->hide(); prepareActionsCache(); style::PaletteChanged( ) | rpl::start_with_next([=] { updateNotifyDisplay(); if (!_buttonsCache.isNull()) { prepareActionsCache(); } update(); if (_background) { _background->update(); } }, lifetime()); show(); } void Notification::updateReplyGeometry() { _reply->moveToRight(_replyPadding, height() - _reply->height() - _replyPadding); } void Notification::refreshLang() { InvokeQueued(this, [this] { updateReplyGeometry(); }); } void Notification::prepareActionsCache() { auto replyCache = Ui::GrabWidget(_reply); auto fadeWidth = st::notifyFadeRight.width(); auto actionsTop = st::notifyTextTop + st::semiboldFont->height; auto replyRight = _replyPadding - st::notifyBorderWidth; auto actionsCacheWidth = _reply->width() + replyRight + fadeWidth; auto actionsCacheHeight = height() - actionsTop - st::notifyBorderWidth; auto actionsCacheImg = QImage(QSize(actionsCacheWidth, actionsCacheHeight) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); actionsCacheImg.setDevicePixelRatio(cRetinaFactor()); actionsCacheImg.fill(Qt::transparent); { Painter p(&actionsCacheImg); st::notifyFadeRight.fill(p, style::rtlrect(0, 0, fadeWidth, actionsCacheHeight, actionsCacheWidth)); p.fillRect(style::rtlrect(fadeWidth, 0, actionsCacheWidth - fadeWidth, actionsCacheHeight, actionsCacheWidth), st::notificationBg); p.drawPixmapRight(replyRight, _reply->y() - actionsTop, actionsCacheWidth, replyCache); } _buttonsCache = Ui::PixmapFromImage(std::move(actionsCacheImg)); } bool Notification::checkLastInput( bool hasReplyingNotifications, std::optional lastInputTime) { if (!_waitingForInput) return true; using namespace Platform::Notifications; const auto waitForUserInput = WaitForInputForCustom() && lastInputTime.has_value() && (*lastInputTime <= _started); if (!waitForUserInput) { _waitingForInput = false; if (!hasReplyingNotifications) { _hideTimer.start(st::notifyWaitLongHide); } return true; } return false; } void Notification::replyResized() { changeHeight(st::notifyMinHeight + _replyArea->height() + st::notifyBorderWidth); } void Notification::replyCancel() { unlinkHistoryInManager(); } void Notification::updateGeometry(int x, int y, int width, int height) { if (height > st::notifyMinHeight) { if (!_background) { _background.create(this); } _background->setGeometry(0, st::notifyMinHeight, width, height - st::notifyMinHeight); } else if (_background) { _background.destroy(); } Widget::updateGeometry(x, y, width, height); } void Notification::paintEvent(QPaintEvent *e) { repaintText(); Painter p(this); p.setClipRect(e->rect()); p.drawImage(0, 0, _cache); auto buttonsTop = st::notifyTextTop + st::semiboldFont->height; if (a_actionsOpacity.animating()) { p.setOpacity(a_actionsOpacity.value(1.)); p.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache); } else if (_actionsVisible) { p.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache); } } void Notification::actionsOpacityCallback() { update(); if (!a_actionsOpacity.animating() && _actionsVisible) { _reply->show(); } } void Notification::customEmojiCallback() { if (_textsRepaintScheduled) { return; } _textsRepaintScheduled = true; crl::on_main(this, [=] { repaintText(); }); } void Notification::repaintText() { if (!_textsRepaintScheduled) { return; } _textsRepaintScheduled = false; if (_cache.isNull()) { return; } Painter p(&_cache); const auto adjusted = Ui::Text::AdjustCustomEmojiSize(st::emojiSize); const auto skip = (adjusted - st::emojiSize + 1) / 2; const auto margin = QMargins{ skip, skip, skip, skip }; p.fillRect(_titleRect.marginsAdded(margin), st::notificationBg); p.fillRect(_textRect.marginsAdded(margin), st::notificationBg); paintTitle(p); paintText(p); update(); } void Notification::paintTitle(Painter &p) { p.setPen(st::dialogsNameFg); p.setFont(st::semiboldFont); _titleCache.draw(p, { .position = _titleRect.topLeft(), .availableWidth = _titleRect.width(), .palette = &st::dialogsTextPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .pausedEmoji = On(PowerSaving::kEmojiChat), .pausedSpoiler = On(PowerSaving::kChatSpoiler), .elisionOneLine = true, }); } void Notification::paintText(Painter &p) { p.setPen(st::dialogsTextFg); p.setFont(st::dialogsTextFont); _textCache.draw(p, { .position = _textRect.topLeft(), .availableWidth = _textRect.width(), .palette = &st::dialogsTextPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .pausedEmoji = On(PowerSaving::kEmojiChat), .pausedSpoiler = On(PowerSaving::kChatSpoiler), .elisionHeight = _textRect.height(), }); } void Notification::updateNotifyDisplay() { if (!_history || (!_item && _forwardedCount < 2)) { return; } const auto options = manager()->getNotificationOptions( _item, (_reaction.empty() ? Data::ItemNotificationType::Message : Data::ItemNotificationType::Reaction)); _hideReplyButton = options.hideReplyButton; int32 w = width(), h = height(); QImage img(w * cIntRetinaFactor(), h * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); img.setDevicePixelRatio(cRetinaFactor()); img.fill(st::notificationBg->c); { Painter p(&img); p.fillRect(0, 0, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder); p.fillRect(w - st::notifyBorderWidth, 0, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder); p.fillRect(st::notifyBorderWidth, h - st::notifyBorderWidth, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder); p.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder); if (!options.hideNameAndPhoto) { if (_fromScheduled && _history->peer->isSelf()) { Ui::EmptyUserpic::PaintSavedMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize); _userpicLoaded = true; } else if (_history->peer->isRepliesChat()) { Ui::EmptyUserpic::PaintRepliesMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize); _userpicLoaded = true; } else { _userpicView = _history->peer->createUserpicView(); _history->peer->loadUserpic(); _history->peer->paintUserpicLeft(p, _userpicView, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize); } } else { p.drawPixmap(st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), manager()->hiddenUserpicPlaceholder()); _userpicLoaded = true; } int32 itemWidth = w - st::notifyPhotoPos.x() - st::notifyPhotoSize - st::notifyTextLeft - st::notifyClosePos.x() - st::notifyClose.width; QRect rectForName(st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyTextTop, itemWidth, st::semiboldFont->height); const auto reminder = _fromScheduled && _history->peer->isSelf(); if (!options.hideNameAndPhoto) { if (_fromScheduled) { static const auto emoji = Ui::Emoji::Find(QString::fromUtf8("\xF0\x9F\x93\x85")); const auto size = Ui::Emoji::GetSizeNormal() / cIntRetinaFactor(); const auto top = rectForName.top() + (st::semiboldFont->height - size) / 2; Ui::Emoji::Draw(p, emoji, Ui::Emoji::GetSizeNormal(), rectForName.left(), top); rectForName.setLeft(rectForName.left() + size + st::semiboldFont->spacew); } const auto chatTypeIcon = _topic ? nullptr : Dialogs::Ui::ChatTypeIcon(_history->peer); if (chatTypeIcon) { chatTypeIcon->paint(p, rectForName.topLeft(), w); rectForName.setLeft(rectForName.left() + chatTypeIcon->width() + st::dialogsChatTypeSkip); } } const auto composeText = !options.hideMessageText || (!_reaction.empty() && !options.hideNameAndPhoto); if (composeText) { auto old = base::take(_textCache); _textCache = Ui::Text::String(itemWidth); auto r = QRect( st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyItemTop + st::semiboldFont->height, itemWidth, 2 * st::dialogsTextFont->height); const auto text = !_reaction.empty() ? (!_author.isEmpty() ? Ui::Text::Colorized(_author).append(' ') : TextWithEntities() ).append(Manager::ComposeReactionNotification( _item, _reaction, options.hideMessageText)) : _item ? _item->toPreview({ .hideSender = reminder, .generateImages = false, .spoilerLoginCode = options.spoilerLoginCode, }).text : ((!_author.isEmpty() ? Ui::Text::Colorized(_author) : TextWithEntities() ).append(_forwardedCount > 1 ? ('\n' + tr::lng_forward_messages( tr::now, lt_count, _forwardedCount)) : QString())); const auto options = TextParseOptions{ (TextParseColorized | TextParseMarkdown | (_forwardedCount > 1 ? TextParseMultiline : 0)), 0, 0, Qt::LayoutDirectionAuto, }; const auto context = Core::MarkedTextContext{ .session = &_history->session(), .customEmojiRepaint = [=] { customEmojiCallback(); }, }; _textCache.setMarkedText( st::dialogsTextStyle, text, options, context); _textRect = r; paintText(p); if (!_textCache.hasPersistentAnimation() && !_topic) { _textCache = Ui::Text::String(); } } else { p.setFont(st::dialogsTextFont); p.setPen(st::dialogsTextFgService); p.drawText( st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyItemTop + st::semiboldFont->height + st::dialogsTextFont->ascent, st::dialogsTextFont->elided( tr::lng_notification_preview(tr::now), itemWidth)); } const auto topicWithChat = [&]() -> TextWithEntities { const auto name = _history->peer->name(); return _topic ? _topic->titleWithIcon().append(u" ("_q + name + ')') : TextWithEntities{ name }; }; auto title = options.hideNameAndPhoto ? TextWithEntities{ u"Telegram Desktop"_q } : reminder ? tr::lng_notification_reminder(tr::now, Ui::Text::WithEntities) : topicWithChat(); const auto fullTitle = manager()->addTargetAccountName( std::move(title), &_history->session()); const auto context = Core::MarkedTextContext{ .session = &_history->session(), .customEmojiRepaint = [=] { customEmojiCallback(); }, }; _titleCache.setMarkedText( st::semiboldTextStyle, fullTitle, Ui::NameTextOptions(), context); _titleRect = rectForName; paintTitle(p); } _cache = std::move(img); if (!canReply()) { toggleActionButtons(false); } update(); } void Notification::updatePeerPhoto() { if (_userpicLoaded) { return; } _userpicView = _peer->createUserpicView(); if (Ui::PeerUserpicLoading(_userpicView)) { return; } _userpicLoaded = true; Painter p(&_cache); p.fillRect( style::rtlrect( QRect( st::notifyPhotoPos, QSize(st::notifyPhotoSize, st::notifyPhotoSize)), width()), st::notificationBg); _peer->paintUserpicLeft( p, _userpicView, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize); _userpicView = {}; update(); } bool Notification::unlinkItem(HistoryItem *deleted) { auto unlink = (_item && _item == deleted); if (unlink) { _item = nullptr; unlinkHistory(); } return unlink; } bool Notification::canReply() const { return !_hideReplyButton && (_item != nullptr) && !Core::App().passcodeLocked() && (Core::App().settings().notifyView() <= Core::Settings::NotifyView::ShowPreview); } void Notification::unlinkHistoryInManager() { manager()->unlinkFromShown(this); } void Notification::toggleActionButtons(bool visible) { if (_actionsVisible != visible) { _actionsVisible = visible; a_actionsOpacity.start([this] { actionsOpacityCallback(); }, _actionsVisible ? 0. : 1., _actionsVisible ? 1. : 0., st::notifyActionsDuration); _reply->clearState(); _reply->hide(); } } void Notification::showReplyField() { if (!_item) { return; } raise(); activateWindow(); if (_replyArea) { _replyArea->setFocus(); return; } stopHiding(); _background.create(this); _background->setGeometry(0, st::notifyMinHeight, width(), st::notifySendReply.height + st::notifyBorderWidth); _background->show(); _replyArea.create( this, st::notifyReplyArea, Ui::InputField::Mode::MultiLine, tr::lng_message_ph()); _replyArea->resize(width() - st::notifySendReply.width - 2 * st::notifyBorderWidth, st::notifySendReply.height); _replyArea->moveToLeft(st::notifyBorderWidth, st::notifyMinHeight); _replyArea->show(); _replyArea->setFocus(); _replyArea->setMaxLength(MaxMessageSize); _replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both); InitMessageFieldHandlers( &_item->history()->session(), nullptr, _replyArea.data(), nullptr); // Catch mouse press event to activate the window. QCoreApplication::instance()->installEventFilter(this); _replyArea->heightChanges( ) | rpl::start_with_next([=] { replyResized(); }, _replyArea->lifetime()); _replyArea->submits( ) | rpl::start_with_next([=] { sendReply(); }, _replyArea->lifetime()); _replyArea->cancelled( ) | rpl::start_with_next([=] { replyCancel(); }, _replyArea->lifetime()); _replySend.create(this, st::notifySendReply); _replySend->moveToRight(st::notifyBorderWidth, st::notifyMinHeight); _replySend->show(); _replySend->setClickedCallback([this] { sendReply(); }); toggleActionButtons(false); replyResized(); update(); } void Notification::sendReply() { if (!_history) return; manager()->notificationReplied( myId(), _replyArea->getTextWithAppliedMarkdown()); manager()->startAllHiding(); } Notifications::Manager::NotificationId Notification::myId() const { if (!_history) { return {}; } return { .contextId = { .sessionId = _history->session().uniqueId(), .peerId = _history->peer->id, .topicRootId = _topicRootId, }, .msgId = _item ? _item->id : ShowAtUnreadMsgId }; } void Notification::changeHeight(int newHeight) { manager()->changeNotificationHeight(this, newHeight); } bool Notification::unlinkHistory(History *history, MsgId topicRootId) { const auto unlink = _history && (history == _history || !history) && (topicRootId == _topicRootId || !topicRootId); if (unlink) { hideFast(); _history = nullptr; _topic = nullptr; _item = nullptr; } return unlink; } bool Notification::unlinkSession(not_null session) { const auto unlink = _history && (&_history->session() == session); if (unlink) { hideFast(); _history = nullptr; _item = nullptr; } return unlink; } void Notification::enterEventHook(QEnterEvent *e) { if (!_history) { return; } manager()->stopAllHiding(); if (!_replyArea && canReply()) { toggleActionButtons(true); } } void Notification::leaveEventHook(QEvent *e) { if (!_history) { return; } manager()->startAllHiding(); toggleActionButtons(false); } void Notification::startHiding() { if (!_history) return; hideSlow(); } void Notification::mousePressEvent(QMouseEvent *e) { if (!_history) return; if (e->button() == Qt::RightButton) { unlinkHistoryInManager(); } else { e->ignore(); manager()->notificationActivated(myId()); } } bool Notification::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::MouseButtonPress) { if (auto receiver = qobject_cast(o)) { if (isAncestorOf(receiver)) { raise(); activateWindow(); } } } return false; } void Notification::stopHiding() { if (!_history) return; _hideTimer.stop(); Widget::hideStop(); } HideAllButton::HideAllButton( not_null manager, QPoint startPosition, int shift, Direction shiftDirection) : Widget(manager, startPosition, shift, shiftDirection) { setCursor(style::cur_pointer); auto position = computePosition(st::notifyHideAllHeight); updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyHideAllHeight); hide(); createWinId(); style::PaletteChanged( ) | rpl::start_with_next([=] { update(); }, lifetime()); show(); } void HideAllButton::startHiding() { hideSlow(); } void HideAllButton::startHidingFast() { hideFast(); } void HideAllButton::stopHiding() { hideStop(); } void HideAllButton::enterEventHook(QEnterEvent *e) { _mouseOver = true; update(); } void HideAllButton::leaveEventHook(QEvent *e) { _mouseOver = false; update(); } void HideAllButton::mousePressEvent(QMouseEvent *e) { _mouseDown = true; } void HideAllButton::mouseReleaseEvent(QMouseEvent *e) { auto mouseDown = base::take(_mouseDown); if (mouseDown && _mouseOver) { manager()->clearAll(); } } void HideAllButton::paintEvent(QPaintEvent *e) { Painter p(this); p.setClipRect(e->rect()); p.fillRect(rect(), _mouseOver ? st::lightButtonBgOver : st::lightButtonBg); p.fillRect(0, 0, width(), st::notifyBorderWidth, st::notifyBorder); p.fillRect(0, height() - st::notifyBorderWidth, width(), st::notifyBorderWidth, st::notifyBorder); p.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder); p.fillRect(width() - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder); p.setFont(st::defaultLinkButton.font); p.setPen(_mouseOver ? st::lightButtonFgOver : st::lightButtonFg); p.drawText(rect(), tr::lng_notification_hide_all(tr::now), style::al_center); } } // namespace internal } // namespace Default } // namespace Notifications } // namespace Window