1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "window/notifications_manager.h"
9 
10 #include "platform/platform_notifications_manager.h"
11 #include "window/notifications_manager_default.h"
12 #include "media/audio/media_audio_track.h"
13 #include "media/audio/media_audio.h"
14 #include "mtproto/mtproto_config.h"
15 #include "history/history.h"
16 #include "history/history_item_components.h"
17 #include "lang/lang_keys.h"
18 #include "data/data_session.h"
19 #include "data/data_channel.h"
20 #include "data/data_user.h"
21 #include "base/unixtime.h"
22 #include "window/window_controller.h"
23 #include "window/window_session_controller.h"
24 #include "core/application.h"
25 #include "mainwindow.h"
26 #include "api/api_updates.h"
27 #include "apiwrap.h"
28 #include "main/main_account.h"
29 #include "main/main_session.h"
30 #include "main/main_domain.h"
31 #include "facades.h"
32 #include "app.h"
33 
34 #include <QtGui/QWindow>
35 
36 namespace Window {
37 namespace Notifications {
38 namespace {
39 
40 // not more than one sound in 500ms from one peer - grouping
41 constexpr auto kMinimalAlertDelay = crl::time(500);
42 constexpr auto kWaitingForAllGroupedDelay = crl::time(1000);
43 
44 #ifdef Q_OS_MAC
45 constexpr auto kSystemAlertDuration = crl::time(1000);
46 #else // !Q_OS_MAC
47 constexpr auto kSystemAlertDuration = crl::time(0);
48 #endif // Q_OS_MAC
49 
50 } // namespace
51 
System()52 System::System()
53 : _waitTimer([=] { showNext(); })
__anoncbf258090302null54 , _waitForAllGroupedTimer([=] { showGrouped(); }) {
55 	settingsChanged(
__anoncbf258090402(ChangeType type) 56 	) | rpl::start_with_next([=](ChangeType type) {
57 		if (type == ChangeType::DesktopEnabled) {
58 			clearAll();
59 		} else if (type == ChangeType::ViewParams) {
60 			updateAll();
61 		} else if (type == ChangeType::IncludeMuted
62 			|| type == ChangeType::CountMessages) {
63 			Core::App().domain().notifyUnreadBadgeChanged();
64 		}
65 	}, lifetime());
66 }
67 
createManager()68 void System::createManager() {
69 	Platform::Notifications::Create(this);
70 }
71 
setManager(std::unique_ptr<Manager> manager)72 void System::setManager(std::unique_ptr<Manager> manager) {
73 	_manager = std::move(manager);
74 	if (!_manager) {
75 		_manager = std::make_unique<Default::Manager>(this);
76 	}
77 }
78 
managerType() const79 std::optional<ManagerType> System::managerType() const {
80 	if (_manager) {
81 		return _manager->type();
82 	}
83 	return std::nullopt;
84 }
85 
findSession(uint64 sessionId) const86 Main::Session *System::findSession(uint64 sessionId) const {
87 	for (const auto &[index, account] : Core::App().domain().accounts()) {
88 		if (const auto session = account->maybeSession()) {
89 			if (session->uniqueId() == sessionId) {
90 				return session;
91 			}
92 		}
93 	}
94 	return nullptr;
95 }
96 
skipNotification(not_null<HistoryItem * > item) const97 System::SkipState System::skipNotification(
98 		not_null<HistoryItem*> item) const {
99 	const auto history = item->history();
100 	const auto notifyBy = item->specialNotificationPeer();
101 	if (App::quitting()
102 		|| !history->currentNotification()
103 		|| item->skipNotification()) {
104 		return { SkipState::Skip };
105 	} else if (!Core::App().settings().notifyFromAll()
106 		&& &history->session().account() != &Core::App().domain().active()) {
107 		return { SkipState::Skip };
108 	}
109 
110 	history->owner().requestNotifySettings(history->peer);
111 	if (notifyBy) {
112 		history->owner().requestNotifySettings(notifyBy);
113 	}
114 
115 	const auto scheduled = item->out() && item->isFromScheduled();
116 	if (history->owner().notifyMuteUnknown(history->peer)) {
117 		return { SkipState::Unknown, item->isSilent() };
118 	} else if (!history->owner().notifyIsMuted(history->peer)) {
119 		return { SkipState::DontSkip, item->isSilent() };
120 	} else if (!notifyBy) {
121 		return {
122 			scheduled ? SkipState::DontSkip : SkipState::Skip,
123 			item->isSilent() || scheduled
124 		};
125 	} else if (history->owner().notifyMuteUnknown(notifyBy)) {
126 		return { SkipState::Unknown, item->isSilent() };
127 	} else if (!history->owner().notifyIsMuted(notifyBy)) {
128 		return { SkipState::DontSkip, item->isSilent() };
129 	} else {
130 		return {
131 			scheduled ? SkipState::DontSkip : SkipState::Skip,
132 			item->isSilent() || scheduled
133 		};
134 	}
135 }
136 
schedule(not_null<HistoryItem * > item)137 void System::schedule(not_null<HistoryItem*> item) {
138 	Expects(_manager != nullptr);
139 
140 	const auto history = item->history();
141 	const auto skip = skipNotification(item);
142 	if (skip.value == SkipState::Skip) {
143 		history->popNotification(item);
144 		return;
145 	}
146 	const auto notifyBy = item->specialNotificationPeer();
147 	const auto ready = (skip.value != SkipState::Unknown)
148 		&& item->notificationReady();
149 
150 	auto delay = item->Has<HistoryMessageForwarded>() ? 500 : 100;
151 	const auto t = base::unixtime::now();
152 	const auto ms = crl::now();
153 	const auto &updates = history->session().updates();
154 	const auto &config = history->session().serverConfig();
155 	const bool isOnline = updates.lastWasOnline();
156 	const auto otherNotOld = ((cOtherOnline() * 1000LL) + config.onlineCloudTimeout > t * 1000LL);
157 	const bool otherLaterThanMe = (cOtherOnline() * 1000LL + (ms - updates.lastSetOnline()) > t * 1000LL);
158 	if (!isOnline && otherNotOld && otherLaterThanMe) {
159 		delay = config.notifyCloudDelay;
160 	} else if (cOtherOnline() >= t) {
161 		delay = config.notifyDefaultDelay;
162 	}
163 
164 	auto when = ms + delay;
165 	if (!skip.silent) {
166 		_whenAlerts[history].emplace(when, notifyBy);
167 	}
168 	if (Core::App().settings().desktopNotify()
169 		&& !_manager->skipToast()) {
170 		auto &whenMap = _whenMaps[history];
171 		if (whenMap.find(item->id) == whenMap.end()) {
172 			whenMap.emplace(item->id, when);
173 		}
174 
175 		auto &addTo = ready ? _waiters : _settingWaiters;
176 		const auto it = addTo.find(history);
177 		if (it == addTo.end() || it->second.when > when) {
178 			addTo.emplace(history, Waiter{
179 				.msg = item->id,
180 				.when = when,
181 				.notifyBy = notifyBy
182 			});
183 		}
184 	}
185 	if (ready) {
186 		if (!_waitTimer.isActive() || _waitTimer.remainingTime() > delay) {
187 			_waitTimer.callOnce(delay);
188 		}
189 	}
190 }
191 
clearAll()192 void System::clearAll() {
193 	if (_manager) {
194 		_manager->clearAll();
195 	}
196 
197 	for (auto i = _whenMaps.cbegin(), e = _whenMaps.cend(); i != e; ++i) {
198 		i->first->clearNotifications();
199 	}
200 	_whenMaps.clear();
201 	_whenAlerts.clear();
202 	_waiters.clear();
203 	_settingWaiters.clear();
204 }
205 
clearFromHistory(not_null<History * > history)206 void System::clearFromHistory(not_null<History*> history) {
207 	if (_manager) {
208 		_manager->clearFromHistory(history);
209 	}
210 
211 	history->clearNotifications();
212 	_whenMaps.remove(history);
213 	_whenAlerts.remove(history);
214 	_waiters.remove(history);
215 	_settingWaiters.remove(history);
216 
217 	_waitTimer.cancel();
218 	showNext();
219 }
220 
clearFromSession(not_null<Main::Session * > session)221 void System::clearFromSession(not_null<Main::Session*> session) {
222 	if (_manager) {
223 		_manager->clearFromSession(session);
224 	}
225 
226 	for (auto i = _whenMaps.begin(); i != _whenMaps.end();) {
227 		const auto history = i->first;
228 		if (&history->session() != session) {
229 			++i;
230 			continue;
231 		}
232 		history->clearNotifications();
233 		i = _whenMaps.erase(i);
234 		_whenAlerts.remove(history);
235 		_waiters.remove(history);
236 		_settingWaiters.remove(history);
237 	}
238 	const auto clearFrom = [&](auto &map) {
239 		for (auto i = map.begin(); i != map.end();) {
240 			if (&i->first->session() == session) {
241 				i = map.erase(i);
242 			} else {
243 				++i;
244 			}
245 		}
246 	};
247 	clearFrom(_whenAlerts);
248 	clearFrom(_waiters);
249 	clearFrom(_settingWaiters);
250 }
251 
clearIncomingFromHistory(not_null<History * > history)252 void System::clearIncomingFromHistory(not_null<History*> history) {
253 	if (_manager) {
254 		_manager->clearFromHistory(history);
255 	}
256 	history->clearIncomingNotifications();
257 	_whenAlerts.remove(history);
258 }
259 
clearFromItem(not_null<HistoryItem * > item)260 void System::clearFromItem(not_null<HistoryItem*> item) {
261 	if (_manager) {
262 		_manager->clearFromItem(item);
263 	}
264 }
265 
clearAllFast()266 void System::clearAllFast() {
267 	if (_manager) {
268 		_manager->clearAllFast();
269 	}
270 
271 	_whenMaps.clear();
272 	_whenAlerts.clear();
273 	_waiters.clear();
274 	_settingWaiters.clear();
275 }
276 
checkDelayed()277 void System::checkDelayed() {
278 	for (auto i = _settingWaiters.begin(); i != _settingWaiters.end();) {
279 		const auto history = i->first;
280 		const auto peer = history->peer;
281 		auto loaded = false;
282 		auto muted = false;
283 		if (!peer->owner().notifyMuteUnknown(peer)) {
284 			if (!peer->owner().notifyIsMuted(peer)) {
285 				loaded = true;
286 			} else if (const auto from = i->second.notifyBy) {
287 				if (!peer->owner().notifyMuteUnknown(from)) {
288 					if (!peer->owner().notifyIsMuted(from)) {
289 						loaded = true;
290 					} else {
291 						loaded = muted = true;
292 					}
293 				}
294 			} else {
295 				loaded = muted = true;
296 			}
297 		}
298 		if (loaded) {
299 			const auto fullId = FullMsgId(
300 				history->channelId(),
301 				i->second.msg);
302 			if (const auto item = peer->owner().message(fullId)) {
303 				if (!item->notificationReady()) {
304 					loaded = false;
305 				}
306 			} else {
307 				muted = true;
308 			}
309 		}
310 		if (loaded) {
311 			if (!muted) {
312 				_waiters.emplace(i->first, i->second);
313 			}
314 			i = _settingWaiters.erase(i);
315 		} else {
316 			++i;
317 		}
318 	}
319 	_waitTimer.cancel();
320 	showNext();
321 }
322 
showGrouped()323 void System::showGrouped() {
324 	Expects(_manager != nullptr);
325 
326 	if (const auto session = findSession(_lastHistorySessionId)) {
327 		if (const auto lastItem = session->data().message(_lastHistoryItemId)) {
328 			_waitForAllGroupedTimer.cancel();
329 			_manager->showNotification(lastItem, _lastForwardedCount);
330 			_lastForwardedCount = 0;
331 			_lastHistoryItemId = FullMsgId();
332 			_lastHistorySessionId = 0;
333 		}
334 	}
335 }
336 
showNext()337 void System::showNext() {
338 	Expects(_manager != nullptr);
339 
340 	if (App::quitting()) {
341 		return;
342 	}
343 
344 	const auto isSameGroup = [=](HistoryItem *item) {
345 		if (!_lastHistorySessionId || !_lastHistoryItemId || !item) {
346 			return false;
347 		} else if (item->history()->session().uniqueId()
348 			!= _lastHistorySessionId) {
349 			return false;
350 		}
351 		const auto lastItem = item->history()->owner().message(
352 			_lastHistoryItemId);
353 		if (lastItem) {
354 			return (lastItem->groupId() == item->groupId())
355 				|| (lastItem->author() == item->author());
356 		}
357 		return false;
358 	};
359 
360 	auto ms = crl::now(), nextAlert = crl::time(0);
361 	bool alert = false;
362 	for (auto i = _whenAlerts.begin(); i != _whenAlerts.end();) {
363 		while (!i->second.empty() && i->second.begin()->first <= ms) {
364 			const auto peer = i->first->peer;
365 			const auto peerUnknown = peer->owner().notifyMuteUnknown(peer);
366 			const auto peerAlert = !peerUnknown
367 				&& !peer->owner().notifyIsMuted(peer);
368 			const auto from = i->second.begin()->second;
369 			const auto fromUnknown = (!from
370 				|| peer->owner().notifyMuteUnknown(from));
371 			const auto fromAlert = !fromUnknown
372 				&& !peer->owner().notifyIsMuted(from);
373 			if (peerAlert || fromAlert) {
374 				alert = true;
375 			}
376 			while (!i->second.empty()
377 				&& i->second.begin()->first <= ms + kMinimalAlertDelay) {
378 				i->second.erase(i->second.begin());
379 			}
380 		}
381 		if (i->second.empty()) {
382 			i = _whenAlerts.erase(i);
383 		} else {
384 			if (!nextAlert || nextAlert > i->second.begin()->first) {
385 				nextAlert = i->second.begin()->first;
386 			}
387 			++i;
388 		}
389 	}
390 	const auto &settings = Core::App().settings();
391 	if (alert) {
392 		if (settings.flashBounceNotify() && !_manager->skipFlashBounce()) {
393 			if (const auto window = Core::App().activeWindow()) {
394 				if (const auto handle = window->widget()->windowHandle()) {
395 					handle->alert(kSystemAlertDuration);
396 					// (handle, SLOT(_q_clearAlert())); in the future.
397 				}
398 			}
399 		}
400 		if (settings.soundNotify() && !_manager->skipAudio()) {
401 			ensureSoundCreated();
402 			_soundTrack->playOnce();
403 			Media::Player::mixer()->suppressAll(_soundTrack->getLengthMs());
404 			Media::Player::mixer()->faderOnTimer();
405 		}
406 	}
407 
408 	if (_waiters.empty() || !settings.desktopNotify() || _manager->skipToast()) {
409 		if (nextAlert) {
410 			_waitTimer.callOnce(nextAlert - ms);
411 		}
412 		return;
413 	}
414 
415 	while (true) {
416 		auto next = 0LL;
417 		HistoryItem *notifyItem = nullptr;
418 		History *notifyHistory = nullptr;
419 		for (auto i = _waiters.begin(); i != _waiters.end();) {
420 			const auto history = i->first;
421 			if (history->currentNotification() && history->currentNotification()->id != i->second.msg) {
422 				auto j = _whenMaps.find(history);
423 				if (j == _whenMaps.end()) {
424 					history->clearNotifications();
425 					i = _waiters.erase(i);
426 					continue;
427 				}
428 				do {
429 					auto k = j->second.find(history->currentNotification()->id);
430 					if (k != j->second.cend()) {
431 						i->second.msg = k->first;
432 						i->second.when = k->second;
433 						break;
434 					}
435 					history->skipNotification();
436 				} while (history->currentNotification());
437 			}
438 			if (!history->currentNotification()) {
439 				_whenMaps.remove(history);
440 				i = _waiters.erase(i);
441 				continue;
442 			}
443 			auto when = i->second.when;
444 			if (!notifyItem || next > when) {
445 				next = when;
446 				notifyItem = history->currentNotification();
447 				notifyHistory = history;
448 			}
449 			++i;
450 		}
451 		if (notifyItem) {
452 			if (next > ms) {
453 				if (nextAlert && nextAlert < next) {
454 					next = nextAlert;
455 					nextAlert = 0;
456 				}
457 				_waitTimer.callOnce(next - ms);
458 				break;
459 			} else {
460 				const auto isForwarded = notifyItem->Has<HistoryMessageForwarded>();
461 				const auto isAlbum = notifyItem->groupId();
462 
463 				auto groupedItem = (isForwarded || isAlbum) ? notifyItem : nullptr; // forwarded and album notify grouping
464 				auto forwardedCount = isForwarded ? 1 : 0;
465 
466 				const auto history = notifyItem->history();
467 				const auto j = _whenMaps.find(history);
468 				if (j == _whenMaps.cend()) {
469 					history->clearNotifications();
470 				} else {
471 					auto nextNotify = (HistoryItem*)nullptr;
472 					do {
473 						history->skipNotification();
474 						if (!history->hasNotification()) {
475 							break;
476 						}
477 
478 						j->second.remove((groupedItem ? groupedItem : notifyItem)->id);
479 						do {
480 							const auto k = j->second.find(history->currentNotification()->id);
481 							if (k != j->second.cend()) {
482 								nextNotify = history->currentNotification();
483 								_waiters.emplace(notifyHistory, Waiter{
484 									.msg = k->first,
485 									.when = k->second
486 								});
487 								break;
488 							}
489 							history->skipNotification();
490 						} while (history->hasNotification());
491 						if (nextNotify) {
492 							if (groupedItem) {
493 								const auto canNextBeGrouped = (isForwarded && nextNotify->Has<HistoryMessageForwarded>())
494 									|| (isAlbum && nextNotify->groupId());
495 								const auto nextItem = canNextBeGrouped ? nextNotify : nullptr;
496 								if (nextItem
497 									&& qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) {
498 									if (isForwarded
499 										&& groupedItem->author() == nextItem->author()) {
500 										++forwardedCount;
501 										groupedItem = nextItem;
502 										continue;
503 									}
504 									if (isAlbum
505 										&& groupedItem->groupId() == nextItem->groupId()) {
506 										groupedItem = nextItem;
507 										continue;
508 									}
509 								}
510 							}
511 							nextNotify = nullptr;
512 						}
513 					} while (nextNotify);
514 				}
515 
516 				if (!_lastHistoryItemId && groupedItem) {
517 					_lastHistorySessionId = groupedItem->history()->session().uniqueId();
518 					_lastHistoryItemId = groupedItem->fullId();
519 				}
520 
521 				// If the current notification is grouped.
522 				if (isAlbum || isForwarded) {
523 					// If the previous notification is grouped
524 					// then reset the timer.
525 					if (_waitForAllGroupedTimer.isActive()) {
526 						_waitForAllGroupedTimer.cancel();
527 						// If this is not the same group
528 						// then show the previous group immediately.
529 						if (!isSameGroup(groupedItem)) {
530 							showGrouped();
531 						}
532 					}
533 					// We have to wait until all the messages in this group are loaded.
534 					_lastForwardedCount += forwardedCount;
535 					_lastHistorySessionId = groupedItem->history()->session().uniqueId();
536 					_lastHistoryItemId = groupedItem->fullId();
537 					_waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay);
538 				} else {
539 					// If the current notification is not grouped
540 					// then there is no reason to wait for the timer
541 					// to show the previous notification.
542 					showGrouped();
543 					_manager->showNotification(notifyItem, forwardedCount);
544 				}
545 
546 				if (!history->hasNotification()) {
547 					_waiters.remove(history);
548 					_whenMaps.remove(history);
549 					continue;
550 				}
551 			}
552 		} else {
553 			break;
554 		}
555 	}
556 	if (nextAlert) {
557 		_waitTimer.callOnce(nextAlert - ms);
558 	}
559 }
560 
ensureSoundCreated()561 void System::ensureSoundCreated() {
562 	if (_soundTrack) {
563 		return;
564 	}
565 
566 	_soundTrack = Media::Audio::Current().createTrack();
567 	_soundTrack->fillFromFile(
568 		Core::App().settings().getSoundPath(qsl("msg_incoming")));
569 }
570 
updateAll()571 void System::updateAll() {
572 	if (_manager) {
573 		_manager->updateAll();
574 	}
575 }
576 
settingsChanged() const577 rpl::producer<ChangeType> System::settingsChanged() const {
578 	return _settingsChanged.events();
579 }
580 
notifySettingsChanged(ChangeType type)581 void System::notifySettingsChanged(ChangeType type) {
582 	return _settingsChanged.fire(std::move(type));
583 }
584 
getNotificationOptions(HistoryItem * item) const585 Manager::DisplayOptions Manager::getNotificationOptions(
586 		HistoryItem *item) const {
587 	const auto hideEverything = Core::App().passcodeLocked()
588 		|| forceHideDetails();
589 
590 	const auto view = Core::App().settings().notifyView();
591 	DisplayOptions result;
592 	result.hideNameAndPhoto = hideEverything
593 		|| (view > Core::Settings::NotifyView::ShowName);
594 	result.hideMessageText = hideEverything
595 		|| (view > Core::Settings::NotifyView::ShowPreview);
596 	result.hideMarkAsRead = result.hideMessageText
597 		|| !item
598 		|| ((item->out() || item->history()->peer->isSelf())
599 			&& item->isFromScheduled());
600 	result.hideReplyButton = result.hideMarkAsRead
601 		|| !item->history()->peer->canWrite()
602 		|| item->history()->peer->isBroadcast()
603 		|| (item->history()->peer->slowmodeSecondsLeft() > 0);
604 	return result;
605 }
606 
addTargetAccountName(const QString & title,not_null<Main::Session * > session)607 QString Manager::addTargetAccountName(
608 		const QString &title,
609 		not_null<Main::Session*> session) {
610 	const auto add = [&] {
611 		for (const auto &[index, account] : Core::App().domain().accounts()) {
612 			if (const auto other = account->maybeSession()) {
613 				if (other != session) {
614 					return true;
615 				}
616 			}
617 		}
618 		return false;
619 	}();
620 	return add
621 		? (title
622 			+ accountNameSeparator()
623 			+ (session->user()->username.isEmpty()
624 				? session->user()->name
625 				: session->user()->username))
626 		: title;
627 }
628 
accountNameSeparator()629 QString Manager::accountNameSeparator() {
630 	return QString::fromUtf8(" \xE2\x9E\x9C ");
631 }
632 
notificationActivated(NotificationId id,const TextWithTags & reply)633 void Manager::notificationActivated(
634 		NotificationId id,
635 		const TextWithTags &reply) {
636 	onBeforeNotificationActivated(id);
637 	if (const auto session = system()->findSession(id.full.sessionId)) {
638 		if (session->windows().empty()) {
639 			Core::App().domain().activate(&session->account());
640 		}
641 		if (!session->windows().empty()) {
642 			const auto window = session->windows().front();
643 			const auto history = session->data().history(id.full.peerId);
644 			if (!reply.text.isEmpty()) {
645 				const auto replyToId = (id.msgId > 0
646 					&& !history->peer->isUser())
647 					? id.msgId
648 					: 0;
649 				auto draft = std::make_unique<Data::Draft>(
650 					reply,
651 					replyToId,
652 					MessageCursor{
653 						int(reply.text.size()),
654 						int(reply.text.size()),
655 						QFIXED_MAX,
656 					},
657 					Data::PreviewState::Allowed);
658 				history->setLocalDraft(std::move(draft));
659 			}
660 			window->widget()->showFromTray();
661 			window->widget()->reActivateWindow();
662 			if (Core::App().passcodeLocked()) {
663 				window->widget()->setInnerFocus();
664 				system()->clearAll();
665 			} else {
666 				openNotificationMessage(history, id.msgId);
667 			}
668 			onAfterNotificationActivated(id, window);
669 		}
670 	}
671 }
672 
openNotificationMessage(not_null<History * > history,MsgId messageId)673 void Manager::openNotificationMessage(
674 		not_null<History*> history,
675 		MsgId messageId) {
676 	const auto openExactlyMessage = [&] {
677 		if (history->peer->isUser() || history->peer->isChannel()) {
678 			return false;
679 		}
680 		const auto item = history->owner().message(
681 			history->channelId(),
682 			messageId);
683 		if (!item || !item->isRegular() || !item->mentionsMe()) {
684 			return false;
685 		}
686 		return true;
687 	}();
688 	if (openExactlyMessage) {
689 		Ui::showPeerHistory(history, messageId);
690 	} else {
691 		Ui::showPeerHistory(history, ShowAtUnreadMsgId);
692 	}
693 	system()->clearFromHistory(history);
694 }
695 
notificationReplied(NotificationId id,const TextWithTags & reply)696 void Manager::notificationReplied(
697 		NotificationId id,
698 		const TextWithTags &reply) {
699 	if (!id.full.sessionId || !id.full.peerId) {
700 		return;
701 	}
702 
703 	const auto session = system()->findSession(id.full.sessionId);
704 	if (!session) {
705 		return;
706 	}
707 	const auto history = session->data().history(id.full.peerId);
708 
709 	auto message = Api::MessageToSend(history);
710 	message.textWithTags = reply;
711 	message.action.replyTo = (id.msgId > 0 && !history->peer->isUser())
712 		? id.msgId
713 		: 0;
714 	message.action.clearDraft = false;
715 	history->session().api().sendMessage(std::move(message));
716 
717 	const auto item = history->owner().message(
718 		history->channelId(),
719 		id.msgId);
720 	if (item && item->isUnreadMention() && !item->isUnreadMedia()) {
721 		history->session().api().markMediaRead(item);
722 	}
723 }
724 
doShowNotification(not_null<HistoryItem * > item,int forwardedCount)725 void NativeManager::doShowNotification(
726 		not_null<HistoryItem*> item,
727 		int forwardedCount) {
728 	const auto options = getNotificationOptions(item);
729 
730 	const auto peer = item->history()->peer;
731 	const auto scheduled = !options.hideNameAndPhoto
732 		&& (item->out() || peer->isSelf())
733 		&& item->isFromScheduled();
734 	const auto title = options.hideNameAndPhoto
735 		? qsl("Telegram Desktop")
736 		: (scheduled && peer->isSelf())
737 		? tr::lng_notification_reminder(tr::now)
738 		: peer->name;
739 	const auto fullTitle = addTargetAccountName(title, &peer->session());
740 	const auto subtitle = options.hideNameAndPhoto
741 		? QString()
742 		: item->notificationHeader();
743 	const auto text = options.hideMessageText
744 		? tr::lng_notification_preview(tr::now)
745 		: (forwardedCount < 2
746 			? (item->groupId()
747 				? tr::lng_in_dlg_album(tr::now)
748 				: item->notificationText())
749 			: tr::lng_forward_messages(tr::now, lt_count, forwardedCount));
750 
751 	// #TODO optimize
752 	auto userpicView = item->history()->peer->createUserpicView();
753 	doShowNativeNotification(
754 		item->history()->peer,
755 		userpicView,
756 		item->id,
757 		scheduled ? WrapFromScheduled(fullTitle) : fullTitle,
758 		subtitle,
759 		text,
760 		options);
761 }
762 
forceHideDetails() const763 bool NativeManager::forceHideDetails() const {
764 	return Core::App().screenIsLocked();
765 }
766 
767 System::~System() = default;
768 
WrapFromScheduled(const QString & text)769 QString WrapFromScheduled(const QString &text) {
770 	return QString::fromUtf8("\xF0\x9F\x93\x85 ") + text;
771 }
772 
773 } // namespace Notifications
774 } // namespace Window
775