From ed895ace66786e706f6f99ee721d3abc9458fe07 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 24 Oct 2022 11:22:26 +0400
Subject: [PATCH] Track dialog row offline status by timer.

Fixes #6410.
---
 Telegram/SourceFiles/api/api_updates.cpp      |  2 +
 Telegram/SourceFiles/apiwrap.cpp              |  6 +-
 .../SourceFiles/data/data_peer_values.cpp     |  7 ++-
 Telegram/SourceFiles/data/data_peer_values.h  |  2 +-
 Telegram/SourceFiles/data/data_session.cpp    | 60 +++++++++++++++++++
 Telegram/SourceFiles/data/data_session.h      |  7 +++
 .../dialogs/dialogs_inner_widget.cpp          | 23 ++++---
 .../dialogs/dialogs_inner_widget.h            |  8 +--
 Telegram/SourceFiles/dialogs/dialogs_row.cpp  | 11 +++-
 .../view/history_view_top_bar_widget.cpp      |  4 +-
 10 files changed, 104 insertions(+), 26 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 51fdd65d3..27804c4cf 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -945,6 +945,7 @@ void Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) {
 			Data::PeerUpdate::Flag::OnlineStatus);
 		if (!isOnline) { // Went offline, so we need to save message draft to the cloud.
 			api().saveCurrentDraftToCloud();
+			session().data().maybeStopWatchForOffline(self);
 		}
 
 		_lastSetOnline = ms;
@@ -1856,6 +1857,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
 			session().changes().peerUpdated(
 				user,
 				Data::PeerUpdate::Flag::OnlineStatus);
+			session().data().maybeStopWatchForOffline(user);
 		}
 		if (UserId(d.vuser_id()) == session().userId()) {
 			if (d.vstatus().type() == mtpc_userStatusOffline
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index b7c397be0..e27fe368d 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -1843,6 +1843,7 @@ void ApiWrap::updatePrivacyLastSeens() {
 		session().changes().peerUpdated(
 			user,
 			Data::PeerUpdate::Flag::OnlineStatus);
+		session().data().maybeStopWatchForOffline(user);
 	});
 
 	if (_contactsStatusesRequestId) {
@@ -1856,12 +1857,15 @@ void ApiWrap::updatePrivacyLastSeens() {
 			auto &data = item.c_contactStatus();
 			if (auto user = _session->data().userLoaded(data.vuser_id())) {
 				auto oldOnlineTill = user->onlineTill;
-				auto newOnlineTill = OnlineTillFromStatus(data.vstatus(), oldOnlineTill);
+				auto newOnlineTill = OnlineTillFromStatus(
+					data.vstatus(),
+					oldOnlineTill);
 				if (oldOnlineTill != newOnlineTill) {
 					user->onlineTill = newOnlineTill;
 					session().changes().peerUpdated(
 						user,
 						Data::PeerUpdate::Flag::OnlineStatus);
+					session().data().maybeStopWatchForOffline(user);
 				}
 			}
 		}
diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index 8f8cf779e..08feba690 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -460,8 +460,11 @@ bool OnlineTextActive(not_null<UserData*> user, TimeId now) {
 	return OnlineTextActive(user->onlineTill, now);
 }
 
-bool IsUserOnline(not_null<UserData*> user) {
-	return OnlineTextActive(user, base::unixtime::now());
+bool IsUserOnline(not_null<UserData*> user, TimeId now) {
+	if (!now) {
+		now = base::unixtime::now();
+	}
+	return OnlineTextActive(user, now);
 }
 
 bool ChannelHasActiveCall(not_null<ChannelData*> channel) {
diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h
index e4d0b25f0..7a5911775 100644
--- a/Telegram/SourceFiles/data/data_peer_values.h
+++ b/Telegram/SourceFiles/data/data_peer_values.h
@@ -122,7 +122,7 @@ inline auto PeerFullFlagValue(
 [[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now);
 [[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now);
 [[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now);
-[[nodiscard]] bool IsUserOnline(not_null<UserData*> user);
+[[nodiscard]] bool IsUserOnline(not_null<UserData*> user, TimeId now = 0);
 [[nodiscard]] bool ChannelHasActiveCall(not_null<ChannelData*> channel);
 
 [[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue(
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 7b7ffe9f5..e0bc63e44 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -246,6 +246,7 @@ Session::Session(not_null<Main::Session*> session)
 , _ttlCheckTimer([=] { checkTTLs(); })
 , _selfDestructTimer([=] { checkSelfDestructItems(); })
 , _pollsClosingTimer([=] { checkPollsClosings(); })
+, _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); })
 , _groups(this)
 , _chatsFilters(std::make_unique<ChatFilters>(this))
 , _scheduledMessages(std::make_unique<ScheduledMessages>(this))
@@ -644,6 +645,7 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
 		if (oldOnlineTill != newOnlineTill) {
 			result->onlineTill = newOnlineTill;
 			flags |= UpdateFlag::OnlineStatus;
+			session().data().maybeStopWatchForOffline(result);
 		}
 	}
 
@@ -968,6 +970,64 @@ GroupCall *Session::groupCall(CallId callId) const {
 	return (i != end(_groupCalls)) ? i->second.get() : nullptr;
 }
 
+void Session::watchForOffline(not_null<UserData*> user, TimeId now) {
+	if (!now) {
+		now = base::unixtime::now();
+	}
+	if (!Data::IsUserOnline(user, now)) {
+		return;
+	}
+	const auto till = user->onlineTill;
+	const auto [i, ok] = _watchingForOffline.emplace(user, till);
+	if (!ok) {
+		if (i->second == till) {
+			return;
+		}
+		i->second = till;
+	}
+	const auto timeout = Data::OnlineChangeTimeout(till, now);
+	const auto fires = _watchForOfflineTimer.isActive()
+		? _watchForOfflineTimer.remainingTime()
+		: -1;
+	if (fires >= 0 && fires <= timeout) {
+		return;
+	}
+	_watchForOfflineTimer.callOnce(std::max(timeout, crl::time(1)));
+}
+
+void Session::maybeStopWatchForOffline(not_null<UserData*> user) {
+	if (Data::IsUserOnline(user)) {
+		return;
+	} else if (_watchingForOffline.remove(user)
+		&& _watchingForOffline.empty()) {
+		_watchForOfflineTimer.cancel();
+	}
+}
+
+void Session::checkLocalUsersWentOffline() {
+	_watchForOfflineTimer.cancel();
+
+	auto minimal = 86400 * crl::time(1000);
+	const auto now = base::unixtime::now();
+	for (auto i = begin(_watchingForOffline)
+		; i != end(_watchingForOffline);) {
+		const auto user = i->first;
+		if (!Data::IsUserOnline(user, now)) {
+			i = _watchingForOffline.erase(i);
+			session().changes().peerUpdated(
+				user,
+				PeerUpdate::Flag::OnlineStatus);
+		} else {
+			const auto timeout = Data::OnlineChangeTimeout(user, now);
+			accumulate_min(minimal, timeout);
+			++i;
+		}
+	}
+	if (!_watchingForOffline.empty()) {
+		_watchForOfflineTimer.callOnce(std::max(minimal, crl::time(1)));
+	}
+}
+
 auto Session::invitedToCallUsers(CallId callId) const
 -> const base::flat_set<not_null<UserData*>> & {
 	static const base::flat_set<not_null<UserData*>> kEmpty;
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index d491e897b..85afbc2a5 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -192,6 +192,9 @@ public:
 	void unregisterGroupCall(not_null<GroupCall*> call);
 	GroupCall *groupCall(CallId callId) const;
 
+	void watchForOffline(not_null<UserData*> user, TimeId now = 0);
+	void maybeStopWatchForOffline(not_null<UserData*> user);
+
 	[[nodiscard]] auto invitedToCallUsers(CallId callId) const
 		-> const base::flat_set<not_null<UserData*>> &;
 	void registerInvitedToCallUser(
@@ -717,6 +720,7 @@ private:
 	void setupUserIsContactViewer();
 
 	void checkSelfDestructItems();
+	void checkLocalUsersWentOffline();
 
 	void scheduleNextTTLs();
 	void checkTTLs();
@@ -976,6 +980,9 @@ private:
 	std::vector<WallPaper> _wallpapers;
 	uint64 _wallpapersHash = 0;
 
+	base::flat_map<not_null<UserData*>, TimeId> _watchingForOffline;
+	base::Timer _watchForOfflineTimer;
+
 	rpl::event_stream<WebViewResultSent> _webViewResultSent;
 
 	Groups _groups;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 08b613c5d..784baa107 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -193,7 +193,7 @@ InnerWidget::InnerWidget(
 
 	session().data().sendActionManager().speakingAnimationUpdated(
 	) | rpl::start_with_next([=](not_null<History*> history) {
-		updateDialogRowCornerStatus(history);
+		repaintDialogRowCornerStatus(history);
 	}, lifetime());
 
 	setupOnlineStatusCheck();
@@ -3181,15 +3181,15 @@ void InnerWidget::setupOnlineStatusCheck() {
 		Data::PeerUpdate::Flag::OnlineStatus
 		| Data::PeerUpdate::Flag::GroupCall
 	) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
-		if (update.peer->isUser()) {
-			userOnlineUpdated(update.peer);
+		if (const auto user = update.peer->asUser()) {
+			userOnlineUpdated(user);
 		} else {
 			groupHasCallUpdated(update.peer);
 		}
 	}, lifetime());
 }
 
-void InnerWidget::updateDialogRowCornerStatus(not_null<History*> history) {
+void InnerWidget::repaintDialogRowCornerStatus(not_null<History*> history) {
 	const auto user = history->peer->isUser();
 	const auto size = user
 		? st::dialogsOnlineBadgeSize
@@ -3217,16 +3217,15 @@ void InnerWidget::updateDialogRowCornerStatus(not_null<History*> history) {
 		UpdateRowSection::Default | UpdateRowSection::Filtered);
 }
 
-void InnerWidget::userOnlineUpdated(not_null<PeerData*> peer) {
-	const auto user = peer->isSelf() ? nullptr : peer->asUser();
-	if (!user) {
+void InnerWidget::userOnlineUpdated(not_null<UserData*> user) {
+	if (!user->isSelf()) {
 		return;
 	}
 	const auto history = session().data().historyLoaded(user);
 	if (!history) {
 		return;
 	}
-	updateRowCornerStatusShown(history, Data::IsUserOnline(user));
+	updateRowCornerStatusShown(history);
 }
 
 void InnerWidget::groupHasCallUpdated(not_null<PeerData*> peer) {
@@ -3238,14 +3237,12 @@ void InnerWidget::groupHasCallUpdated(not_null<PeerData*> peer) {
 	if (!history) {
 		return;
 	}
-	updateRowCornerStatusShown(history, Data::ChannelHasActiveCall(group));
+	updateRowCornerStatusShown(history);
 }
 
-void InnerWidget::updateRowCornerStatusShown(
-		not_null<History*> history,
-		bool shown) {
+void InnerWidget::updateRowCornerStatusShown(not_null<History*> history) {
 	const auto repaint = [=] {
-		updateDialogRowCornerStatus(history);
+		repaintDialogRowCornerStatus(history);
 	};
 	repaint();
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index dc842c77a..a0a80c931 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -248,13 +248,11 @@ private:
 
 	int defaultRowTop(not_null<Row*> row) const;
 	void setupOnlineStatusCheck();
-	void userOnlineUpdated(not_null<PeerData*> peer);
+	void userOnlineUpdated(not_null<UserData*> user);
 	void groupHasCallUpdated(not_null<PeerData*> peer);
 
-	void updateRowCornerStatusShown(
-		not_null<History*> history,
-		bool shown);
-	void updateDialogRowCornerStatus(not_null<History*> history);
+	void updateRowCornerStatusShown(not_null<History*> history);
+	void repaintDialogRowCornerStatus(not_null<History*> history);
 
 	void setupShortcuts();
 	RowDescriptor computeJump(
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index dd6da06d6..5af607cf1 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -15,10 +15,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/ui/dialogs_video_userpic.h"
 #include "dialogs/ui/dialogs_layout.h"
 #include "data/data_folder.h"
+#include "data/data_session.h"
 #include "data/data_peer_values.h"
 #include "history/history.h"
 #include "history/history_item.h"
 #include "lang/lang_keys.h"
+#include "base/unixtime.h"
 #include "mainwidget.h"
 #include "styles/style_dialogs.h"
 
@@ -196,15 +198,20 @@ void Row::setCornerBadgeShown(
 void Row::updateCornerBadgeShown(
 		not_null<PeerData*> peer,
 		Fn<void()> updateCallback) const {
+	const auto user = peer->asUser();
+	const auto now = user ? base::unixtime::now() : TimeId();
 	const auto shown = [&] {
-		if (const auto user = peer->asUser()) {
-			return Data::IsUserOnline(user);
+		if (user) {
+			return Data::IsUserOnline(user, now);
 		} else if (const auto channel = peer->asChannel()) {
 			return Data::ChannelHasActiveCall(channel);
 		}
 		return false;
 	}();
 	setCornerBadgeShown(shown, std::move(updateCallback));
+	if (shown && user) {
+		peer->owner().watchForOffline(user, now);
+	}
 }
 
 void Row::ensureCornerBadgeUserpic() const {
diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
index 2b29389b8..62a3044eb 100644
--- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
@@ -1338,9 +1338,9 @@ void TopBarWidget::updateOnlineDisplayTimer() {
 	}
 
 	const auto now = base::unixtime::now();
-	auto minTimeout = crl::time(86400);
+	auto minTimeout = 86400 * crl::time(1000);
 	const auto handleUser = [&](not_null<UserData*> user) {
-		auto hisTimeout = Data::OnlineChangeTimeout(user, now);
+		const auto hisTimeout = Data::OnlineChangeTimeout(user, now);
 		accumulate_min(minTimeout, hisTimeout);
 	};
 	if (const auto user = peer->asUser()) {