From ca25ad57b1a98e1673e9cbbf592e6f6d362ba601 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 Jan 2024 13:13:30 +0400 Subject: [PATCH 001/109] Update API scheme to layer 172. --- Telegram/Resources/langs/lang.strings | 26 ++++++++ .../SourceFiles/api/api_global_privacy.cpp | 62 ++++++++++++++++-- Telegram/SourceFiles/api/api_global_privacy.h | 14 +++- .../SourceFiles/api/api_send_progress.cpp | 2 +- Telegram/SourceFiles/api/api_updates.cpp | 28 ++++---- Telegram/SourceFiles/apiwrap.cpp | 38 +++-------- Telegram/SourceFiles/apiwrap.h | 4 -- .../SourceFiles/data/data_peer_values.cpp | 12 ++-- Telegram/SourceFiles/data/data_session.cpp | 23 +++++-- Telegram/SourceFiles/data/data_user.cpp | 64 ++++++++++++++++++- Telegram/SourceFiles/data/data_user.h | 21 ++++++ Telegram/SourceFiles/mtproto/scheme/api.tl | 13 ++-- .../touchbar/items/mac_pinned_chats_item.mm | 2 +- Telegram/SourceFiles/settings/settings.style | 6 +- .../settings/settings_privacy_controllers.cpp | 56 +++++++++++++++- .../settings/settings_privacy_controllers.h | 5 ++ 16 files changed, 295 insertions(+), 81 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 357838992..bf186e1f7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -141,6 +141,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_status_last_week" = "last seen within a week"; "lng_status_last_month" = "last seen within a month"; "lng_status_lastseen_now" = "last seen just now"; +"lng_status_lastseen_hidden" = "last seen hidden"; +"lng_status_lastseen_show" = "show"; "lng_status_lastseen_minutes#one" = "last seen {count} minute ago"; "lng_status_lastseen_minutes#other" = "last seen {count} minutes ago"; "lng_status_lastseen_hours#one" = "last seen {count} hour ago"; @@ -168,6 +170,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_remember" = "Remember this choice"; +"lng_lastseen_show_title" = "Show your last seen"; +"lng_lastseen_show_about" = "To see **{user}'s** Last Seen time, either start showing your own Last Seen time..."; +"lng_lastseen_show_button" = "Show my Last Seen"; +"lng_lastseen_or" = "or"; +"lng_lastseen_premium_title" = "Upgrade to Premium"; +"lng_lastseen_premium_about" = "Subscription will let you see **{user}'s** Last Seen status without showing yours."; +"lng_lastseen_premium_button" = "Subscribe to Telegram Premium"; +"lng_lastseen_shown_toast" = "Your last seen time is now visible."; + +"lng_readtime_show_title" = "Show your read date"; +"lng_readtime_show_about" = "To see when **{user}** read the message, either start showing your own read time..."; +"lng_readtime_show_button" = "Show my Read Time"; +"lng_readtime_or" = "or"; +"lng_readtime_premium_title" = "Upgrade to Premium"; +"lng_readtime_premium_about" = "Subscription will let you see **{user}'s** read time without showing yours."; +"lng_readtime_premium_button" = "Subscribe to Telegram Premium"; +"lng_readtime_shown_toast" = "Your read times are now visible."; + "lng_channels_limit_title" = "Too Many Communities"; "lng_channels_limit1#one" = "You are a member of **{count}** groups and channels."; "lng_channels_limit1#other" = "You are a member of **{count}** groups and channels."; @@ -1086,6 +1106,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_lastseen_exceptions" = "These settings will override the values above."; "lng_edit_privacy_lastseen_always_title" = "Always share with"; "lng_edit_privacy_lastseen_never_title" = "Never share with"; +"lng_edit_lastseen_hide_read_time" = "Hide read time"; +"lng_edit_lastseen_hide_read_time_about" = "Do not show the time when you read a message to people you hid your last seen from. If you turn this on, their read time will also be hidden from you. This does not affect group chats."; +"lng_edit_lastseen_subscribe" = "Subscribe to Telegram Premium"; +"lng_edit_lastseen_subscribe_about" = "If you subscribe to Premium, you will see other users' last seen and read time even if you hid yours from them (unless they specifically restricted it)."; "lng_edit_privacy_groups_title" = "Group invite settings"; "lng_edit_privacy_groups_header" = "Who can invite you to groups and channels"; @@ -2764,6 +2788,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_save_custom_sound" = "Save for notifications"; "lng_context_translate" = "Translate"; "lng_context_translate_selected" = "Translate Selected Text"; +"lng_context_read_hidden" = "read"; +"lng_context_read_show" = "show when"; "lng_context_animated_emoji" = "This message contains emoji from **{name} pack**."; "lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**."; diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 219d335f1..f958bdfe8 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -84,18 +84,61 @@ void GlobalPrivacy::dismissArchiveAndMuteSuggestion() { u"AUTOARCHIVE_POPULAR"_q); } +void GlobalPrivacy::updateHideReadTime(bool hide) { + update( + archiveAndMuteCurrent(), + unarchiveOnNewMessageCurrent(), + hide, + newRequirePremiumCurrent()); +} + +bool GlobalPrivacy::hideReadTimeCurrent() const { + return _hideReadTime.current(); +} + +rpl::producer GlobalPrivacy::hideReadTime() const { + return _hideReadTime.value(); +} + +void GlobalPrivacy::updateNewRequirePremium(bool value) { + update( + archiveAndMuteCurrent(), + unarchiveOnNewMessageCurrent(), + hideReadTimeCurrent(), + value); +} + +bool GlobalPrivacy::newRequirePremiumCurrent() const { + return _newRequirePremium.current(); +} + +rpl::producer GlobalPrivacy::newRequirePremium() const { + return _newRequirePremium.value(); +} + + void GlobalPrivacy::updateArchiveAndMute(bool value) { - update(value, unarchiveOnNewMessageCurrent()); + update( + value, + unarchiveOnNewMessageCurrent(), + hideReadTimeCurrent(), + newRequirePremiumCurrent()); } void GlobalPrivacy::updateUnarchiveOnNewMessage( UnarchiveOnNewMessage value) { - update(archiveAndMuteCurrent(), value); + update( + archiveAndMuteCurrent(), + value, + hideReadTimeCurrent(), + newRequirePremiumCurrent()); } void GlobalPrivacy::update( bool archiveAndMute, - UnarchiveOnNewMessage unarchiveOnNewMessage) { + UnarchiveOnNewMessage unarchiveOnNewMessage, + bool hideReadTime, + bool newRequirePremium) { using Flag = MTPDglobalPrivacySettings::Flag; _api.request(_requestId).cancel(); @@ -108,17 +151,26 @@ void GlobalPrivacy::update( : Flag()) | (unarchiveOnNewMessage != UnarchiveOnNewMessage::AnyUnmuted ? Flag::f_keep_archived_folders + : Flag()) + | (hideReadTime ? Flag::f_hide_read_marks : Flag()) + | ((newRequirePremium && _session->premium()) + ? Flag::f_new_noncontact_peers_require_premium : Flag()); _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( MTP_globalPrivacySettings(MTP_flags(flags)) )).done([=](const MTPGlobalPrivacySettings &result) { _requestId = 0; apply(result); - }).fail([=] { + }).fail([=](const MTP::Error &error) { _requestId = 0; + if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) { + update(archiveAndMute, unarchiveOnNewMessage, hideReadTime, {}); + } }).send(); _archiveAndMute = archiveAndMute; _unarchiveOnNewMessage = unarchiveOnNewMessage; + _hideReadTime = hideReadTime; + _newRequirePremium = newRequirePremium; } void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) { @@ -129,6 +181,8 @@ void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) { : data.is_keep_archived_folders() ? UnarchiveOnNewMessage::NotInFoldersUnmuted : UnarchiveOnNewMessage::AnyUnmuted; + _hideReadTime = data.is_hide_read_marks(); + _newRequirePremium = data.is_new_noncontact_peers_require_premium(); }); } diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index 9e4b8e121..de70ba9b9 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -41,12 +41,22 @@ public: [[nodiscard]] rpl::producer<> suggestArchiveAndMute() const; void dismissArchiveAndMuteSuggestion(); + void updateHideReadTime(bool hide); + [[nodiscard]] bool hideReadTimeCurrent() const; + [[nodiscard]] rpl::producer hideReadTime() const; + + void updateNewRequirePremium(bool value); + [[nodiscard]] bool newRequirePremiumCurrent() const; + [[nodiscard]] rpl::producer newRequirePremium() const; + private: void apply(const MTPGlobalPrivacySettings &data); void update( bool archiveAndMute, - UnarchiveOnNewMessage unarchiveOnNewMessage); + UnarchiveOnNewMessage unarchiveOnNewMessage, + bool hideReadTime, + bool newRequirePremium); const not_null _session; MTP::Sender _api; @@ -55,6 +65,8 @@ private: rpl::variable _unarchiveOnNewMessage = UnarchiveOnNewMessage::None; rpl::variable _showArchiveAndMute = false; + rpl::variable _hideReadTime = false; + rpl::variable _newRequirePremium = false; std::vector> _callbacks; }; diff --git a/Telegram/SourceFiles/api/api_send_progress.cpp b/Telegram/SourceFiles/api/api_send_progress.cpp index 54d3c4e00..d3e277261 100644 --- a/Telegram/SourceFiles/api/api_send_progress.cpp +++ b/Telegram/SourceFiles/api/api_send_progress.cpp @@ -162,7 +162,7 @@ bool SendProgressManager::skipRequest(const Key &key) const { } const auto recently = base::unixtime::now() - kSendTypingsToOfflineFor; const auto online = user->onlineTill; - if (online == -2) { // last seen recently + if (online == kOnlineRecently) { return false; } else if (online < 0) { return (-online < recently); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index dde1a68cd..54e2a37b2 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -944,7 +944,8 @@ void Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) { } const auto self = session().user(); - self->onlineTill = base::unixtime::now() + (isOnline ? (config.onlineUpdatePeriod / 1000) : -1); + self->onlineTill = base::unixtime::now() + + (isOnline ? (config.onlineUpdatePeriod / 1000) : -1); session().changes().peerUpdated( self, Data::PeerUpdate::Flag::OnlineStatus); @@ -1850,23 +1851,16 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateUserStatus: { auto &d = update.c_updateUserStatus(); - if (auto user = session().data().userLoaded(d.vuser_id())) { - switch (d.vstatus().type()) { - case mtpc_userStatusEmpty: user->onlineTill = 0; break; - case mtpc_userStatusRecently: - if (user->onlineTill > -10) { // don't modify pseudo-online - user->onlineTill = -2; - } - break; - case mtpc_userStatusLastWeek: user->onlineTill = -3; break; - case mtpc_userStatusLastMonth: user->onlineTill = -4; break; - case mtpc_userStatusOffline: user->onlineTill = d.vstatus().c_userStatusOffline().vwas_online().v; break; - case mtpc_userStatusOnline: user->onlineTill = d.vstatus().c_userStatusOnline().vexpires().v; break; + if (const auto user = session().data().userLoaded(d.vuser_id())) { + const auto value = OnlineTillFromMTP( + d.vstatus(), + user->onlineTill); + if (user->onlineTill != value) { + session().changes().peerUpdated( + user, + Data::PeerUpdate::Flag::OnlineStatus); + session().data().maybeStopWatchForOffline(user); } - 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 a247ad196..b3940a5b0 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1923,21 +1923,18 @@ void ApiWrap::saveDraftToCloudDelayed(not_null thread) { void ApiWrap::updatePrivacyLastSeens() { const auto now = base::unixtime::now(); _session->data().enumerateUsers([&](UserData *user) { - if (user->isSelf() || !user->isLoaded()) { - return; - } - if (user->onlineTill <= 0) { + if (user->isSelf() || !user->isLoaded() || user->onlineTill <= 0) { return; } if (user->onlineTill + 3 * 86400 >= now) { - user->onlineTill = -2; // recently + user->onlineTill = kOnlineRecently; } else if (user->onlineTill + 7 * 86400 >= now) { - user->onlineTill = -3; // last week + user->onlineTill = kOnlineLastWeek; } else if (user->onlineTill + 30 * 86400 >= now) { - user->onlineTill = -4; // last month + user->onlineTill = kOnlineLastMonth; } else { - user->onlineTill = 0; + user->onlineTill = kOnlineEmpty; } session().changes().peerUpdated( user, @@ -1955,12 +1952,11 @@ void ApiWrap::updatePrivacyLastSeens() { Assert(item.type() == mtpc_contactStatus); auto &data = item.c_contactStatus(); if (auto user = _session->data().userLoaded(data.vuser_id())) { - auto oldOnlineTill = user->onlineTill; - auto newOnlineTill = OnlineTillFromStatus( + auto value = OnlineTillFromMTP( data.vstatus(), - oldOnlineTill); - if (oldOnlineTill != newOnlineTill) { - user->onlineTill = newOnlineTill; + user->onlineTill); + if (user->onlineTill != value) { + user->onlineTill = value; session().changes().peerUpdated( user, Data::PeerUpdate::Flag::OnlineStatus); @@ -1973,22 +1969,6 @@ void ApiWrap::updatePrivacyLastSeens() { }).send(); } -int ApiWrap::OnlineTillFromStatus( - const MTPUserStatus &status, - int currentOnlineTill) { - switch (status.type()) { - case mtpc_userStatusEmpty: return 0; - case mtpc_userStatusRecently: - // Don't modify pseudo-online. - return (currentOnlineTill > -10) ? -2 : currentOnlineTill; - case mtpc_userStatusLastWeek: return -3; - case mtpc_userStatusLastMonth: return -4; - case mtpc_userStatusOffline: return status.c_userStatusOffline().vwas_online().v; - case mtpc_userStatusOnline: return status.c_userStatusOnline().vexpires().v; - } - Unexpected("Bad UserStatus type."); -} - void ApiWrap::clearHistory(not_null peer, bool revoke) { deleteHistory(peer, true, revoke); } diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 7e44d460c..192e14f28 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -258,10 +258,6 @@ public: void updateNotifySettingsDelayed(Data::DefaultNotify type); void saveDraftToCloudDelayed(not_null thread); - static int OnlineTillFromStatus( - const MTPUserStatus &status, - int currentOnlineTill); - void clearHistory(not_null peer, bool revoke); void deleteConversation(not_null peer, bool revoke); diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 1670afa8f..c6458fc1d 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -67,13 +67,13 @@ std::optional OnlineTextSpecial(not_null user) { std::optional OnlineTextCommon(TimeId online, TimeId now) { if (online <= 0) { switch (online) { - case 0: - case -1: return tr::lng_status_offline(tr::now); - case -2: return tr::lng_status_recently(tr::now); - case -3: return tr::lng_status_last_week(tr::now); - case -4: return tr::lng_status_last_month(tr::now); + case kOnlineEmpty: return tr::lng_status_offline(tr::now); + case kOnlineRecently: return tr::lng_status_recently(tr::now); + case kOnlineLastWeek: return tr::lng_status_last_week(tr::now); + case kOnlineLastMonth: return tr::lng_status_last_month(tr::now); + case kOnlineHidden: return tr::lng_status_lastseen_hidden(tr::now); } - return (-online > now) + return IsRecentOnline(online, now) ? tr::lng_status_online(tr::now) : tr::lng_status_recently(tr::now); } else if (online > now) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index de0a3c62d..8d235bc44 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -519,6 +519,8 @@ not_null Session::processUser(const MTPUser &data) { | Flag::BotInlineGeo | Flag::Premium | Flag::Support + | Flag::SomeRequirePremiumToWrite + | Flag::RequirePremiumToWriteKnown | (!minimal ? Flag::Contact | Flag::MutualContact @@ -539,10 +541,20 @@ not_null Session::processUser(const MTPUser &data) { | (data.is_bot_inline_geo() ? Flag::BotInlineGeo : Flag()) | (data.is_premium() ? Flag::Premium : Flag()) | (data.is_support() ? Flag::Support : Flag()) + | (data.is_contact_require_premium() + ? (Flag::SomeRequirePremiumToWrite + | (result->someRequirePremiumToWrite() + ? (result->requirePremiumToWriteKnown() + ? Flag::RequirePremiumToWriteKnown + : Flag()) + : Flag())) + : Flag()) | (!minimal ? (data.is_contact() ? Flag::Contact : Flag()) | (data.is_mutual_contact() ? Flag::MutualContact : Flag()) - | (data.is_apply_min_photo() ? Flag() : Flag::DiscardMinPhoto) + | (data.is_apply_min_photo() + ? Flag() + : Flag::DiscardMinPhoto) | (data.is_stories_hidden() ? Flag::StoriesHidden : Flag()) : Flag()); result->setFlags((result->flags() & ~flagsMask) | flagsSet); @@ -718,12 +730,9 @@ not_null Session::processUser(const MTPUser &data) { } if (status && !minimal) { - const auto oldOnlineTill = result->onlineTill; - const auto newOnlineTill = ApiWrap::OnlineTillFromStatus( - *status, - oldOnlineTill); - if (oldOnlineTill != newOnlineTill) { - result->onlineTill = newOnlineTill; + const auto value = OnlineTillFromMTP(*status, result->onlineTill); + if (result->onlineTill != value) { + result->onlineTill = value; flags |= UpdateFlag::OnlineStatus; session().data().maybeStopWatchForOffline(result); } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 3f83a9e94..b455fba66 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -34,6 +34,40 @@ using UpdateFlag = Data::PeerUpdate::Flag; BotInfo::BotInfo() = default; +int RecentOnlineAfter(TimeId when) { + return (when > 0) ? (-when - kSetOnlineAfterActivity) : 0; +} + +bool IsRecentOnlineValue(int value) { + return (value < -kSetOnlineAfterActivity); +} + +bool IsRecentOnline(int value, TimeId now) { + return IsRecentOnlineValue(value) && (now < -value); +} + +int OnlineTillFromMTP( + const MTPUserStatus &status, + int currentOnlineTill) { + return status.match([](const MTPDuserStatusEmpty &) { + return kOnlineEmpty; + }, [&](const MTPDuserStatusRecently&) { + return IsRecentOnlineValue(currentOnlineTill) + ? currentOnlineTill + : kOnlineRecently; + }, [](const MTPDuserStatusLastWeek &) { + return kOnlineLastWeek; + }, [](const MTPDuserStatusLastMonth &) { + return kOnlineLastMonth; + }, [](const MTPDuserStatusHidden &) { + return kOnlineHidden; + }, [](const MTPDuserStatusOnline& data) { + return data.vexpires().v; + }, [](const MTPDuserStatusOffline &data) { + return data.vwas_online().v; + }); +} + UserData::UserData(not_null owner, PeerId id) : PeerData(owner, id) , _flags((id == owner->session().userPeerId()) ? Flag::Self : Flag(0)) { @@ -351,6 +385,22 @@ bool UserData::hasStoriesHidden() const { return (flags() & UserDataFlag::StoriesHidden); } +bool UserData::someRequirePremiumToWrite() const { + return (flags() & UserDataFlag::SomeRequirePremiumToWrite); +} + +bool UserData::meRequiresPremiumToWrite() const { + return (flags() & UserDataFlag::MeRequiresPremiumToWrite); +} + +bool UserData::requirePremiumToWriteKnown() const { + return (flags() & UserDataFlag::RequirePremiumToWriteKnown); +} + +bool UserData::readDatesPrivate() const { + return (flags() & UserDataFlag::ReadDatesPrivate); +} + bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } @@ -453,15 +503,25 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { | Flag::PhoneCallsPrivate | Flag::CanReceiveGifts | Flag::CanPinMessages - | Flag::VoiceMessagesForbidden; + | Flag::VoiceMessagesForbidden + | Flag::ReadDatesPrivate + | Flag::RequirePremiumToWriteKnown + | Flag::MeRequiresPremiumToWrite; user->setFlags((user->flags() & ~mask) - | (update.is_phone_calls_private() ? Flag::PhoneCallsPrivate : Flag()) + | (update.is_phone_calls_private() + ? Flag::PhoneCallsPrivate + : Flag()) | (update.is_phone_calls_available() ? Flag::HasPhoneCalls : Flag()) | (canReceiveGifts ? Flag::CanReceiveGifts : Flag()) | (update.is_can_pin_message() ? Flag::CanPinMessages : Flag()) | (update.is_blocked() ? Flag::Blocked : Flag()) | (update.is_voice_messages_forbidden() ? Flag::VoiceMessagesForbidden + : Flag()) + | (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag()) + | Flag::RequirePremiumToWriteKnown + | (update.is_contact_require_premium() + ? Flag::MeRequiresPremiumToWrite : Flag())); user->setIsBlocked(update.is_blocked()); user->setCallsStatus(update.is_phone_calls_private() diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 328661260..2951cd9ef 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -65,10 +65,27 @@ enum class UserDataFlag { StoriesHidden = (1 << 18), HasActiveStories = (1 << 19), HasUnreadStories = (1 << 20), + MeRequiresPremiumToWrite = (1 << 21), + SomeRequirePremiumToWrite = (1 << 22), + RequirePremiumToWriteKnown = (1 << 23), + ReadDatesPrivate = (1 << 24), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; +inline constexpr auto kOnlineEmpty = 0; +inline constexpr auto kOnlineRecently = -2; +inline constexpr auto kOnlineLastWeek = -3; +inline constexpr auto kOnlineLastMonth = -4; +inline constexpr auto kOnlineHidden = -5; + +[[nodiscard]] int RecentOnlineAfter(TimeId when); +[[nodiscard]] bool IsRecentOnlineValue(int value); +[[nodiscard]] bool IsRecentOnline(int value, TimeId now); +[[nodiscard]] int OnlineTillFromMTP( + const MTPUserStatus& status, + int currentOnlineTill); + class UserData final : public PeerData { public: using Flag = UserDataFlag; @@ -119,6 +136,10 @@ public: [[nodiscard]] bool applyMinPhoto() const; [[nodiscard]] bool hasPersonalPhoto() const; [[nodiscard]] bool hasStoriesHidden() const; + [[nodiscard]] bool someRequirePremiumToWrite() const; + [[nodiscard]] bool meRequiresPremiumToWrite() const; + [[nodiscard]] bool requirePremiumToWriteKnown() const; + [[nodiscard]] bool readDatesPrivate() const; [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index fee427f9f..74f938682 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -82,7 +82,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; +user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.6?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -93,6 +93,7 @@ userStatusOffline#8c703f was_online:int = UserStatus; userStatusRecently#e26f42f1 = UserStatus; userStatusLastWeek#7bf09fc = UserStatus; userStatusLastMonth#77ebc742 = UserStatus; +userStatusHidden#cf7d64b1 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; @@ -226,7 +227,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; +userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -1259,7 +1260,7 @@ statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInvite stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; -globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true = GlobalPrivacySettings; +globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true = GlobalPrivacySettings; help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; @@ -1649,6 +1650,8 @@ savedReactionTag#cb6ff828 flags:# reaction:Reaction title:flags.0?string count:i messages.savedReactionTagsNotModified#889b59ef = messages.SavedReactionTags; messages.savedReactionTags#3259950a tags:Vector hash:long = messages.SavedReactionTags; +outboxReadDate#3bb842ac date:int = OutboxReadDate; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1778,6 +1781,7 @@ account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; +//users.getIsPremiumRequiredToContact#75c9db9c id:Vector = Vector; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; @@ -2002,6 +2006,7 @@ messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vec messages.getSavedReactionTags#761ddacf hash:long = messages.SavedReactionTags; messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool; messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions; +messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2244,4 +2249,4 @@ premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector peer:InputPeer = p premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList; -// LAYER 171 +// LAYER 172 diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm index 43c37c92e..63ceb76fb 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm @@ -141,7 +141,7 @@ TimeId CalculateOnlineTill(not_null peer) { if (const auto user = peer->asUser()) { if (!user->isServiceUser() && !user->isBot()) { const auto onlineTill = user->onlineTill; - return (onlineTill <= -5) + return IsRecentOnlineValue(onlineTill) ? -onlineTill : (onlineTill <= 0) ? 0 diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 9304a3ebd..616709700 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -15,12 +15,14 @@ settingsButton: SettingsButton(infoProfileButton) { padding: margins(60px, 10px, 22px, 10px); iconLeft: 20px; } +settingsButtonNoIcon: SettingsButton(settingsButton) { + padding: margins(22px, 10px, 22px, 8px); +} settingsButtonLight: SettingsButton(settingsButton) { textFg: lightButtonFg; textFgOver: lightButtonFgOver; } -settingsButtonNoIcon: SettingsButton(settingsButton) { - padding: margins(22px, 10px, 22px, 8px); +settingsButtonLightNoIcon: SettingsButton(settingsButtonLight, settingsButtonNoIcon) { } settingsButtonNoIconLocked : SettingsButton(settingsButtonNoIcon) { toggle: Toggle(infoProfileToggle) { diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index cd4aee433..c38afcd32 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_privacy_controllers.h" +#include "api/api_global_privacy.h" #include "api/api_peer_photo.h" #include "apiwrap.h" #include "base/call_delayed.h" @@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_message.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "ui/boxes/confirm_box.h" #include "ui/cached_round_corners.h" @@ -665,16 +667,64 @@ auto LastSeenPrivacyController::exceptionsDescription() const return tr::lng_edit_privacy_lastseen_exceptions(); } +object_ptr LastSeenPrivacyController::setupBelowWidget( + not_null controller, + not_null parent) { + auto result = object_ptr(parent); + const auto content = result.data(); + + Ui::AddSkip(content); + + const auto privacy = &controller->session().api().globalPrivacy(); + content->add(object_ptr( + content, + tr::lng_edit_lastseen_hide_read_time(), + st::settingsButtonNoIcon + ))->toggleOn(privacy->hideReadTime())->toggledValue( + ) | rpl::start_with_next([=](bool value) { + _hideReadTime = value; + }, content->lifetime()); + + Ui::AddSkip(content); + Ui::AddDividerText( + content, + tr::lng_edit_lastseen_hide_read_time_about()); + if (!controller->session().premium()) { + Ui::AddSkip(content); + content->add(object_ptr( + content, + tr::lng_edit_lastseen_subscribe(), + st::settingsButtonLightNoIcon + ))->setClickedCallback([=] { + Settings::ShowPremium(controller, u"lastseen"_q); + }); + Ui::AddSkip(content); + Ui::AddDividerText( + content, + tr::lng_edit_lastseen_subscribe_about()); + } + + return result; +} + void LastSeenPrivacyController::confirmSave( bool someAreDisallowed, Fn saveCallback) { + const auto privacy = &_session->api().globalPrivacy(); + const auto hideReadTime = _hideReadTime; + const auto save = [=, saveCallback = std::move(saveCallback)] { + if (privacy->hideReadTimeCurrent() != hideReadTime) { + privacy->updateHideReadTime(hideReadTime); + } + saveCallback(); + }; if (someAreDisallowed && !Core::App().settings().lastSeenWarningSeen()) { auto callback = [ =, - saveCallback = std::move(saveCallback) + save = std::move(save) ](Fn &&close) { close(); - saveCallback(); + save(); Core::App().settings().setLastSeenWarningSeen(true); Core::App().saveSettingsDelayed(); }; @@ -685,7 +735,7 @@ void LastSeenPrivacyController::confirmSave( }); Ui::show(std::move(box), Ui::LayerOption::KeepOther); } else { - saveCallback(); + save(); } } diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index e2c8c187f..5211d0731 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -109,12 +109,17 @@ public: Exception exception) const override; rpl::producer exceptionsDescription() const override; + object_ptr setupBelowWidget( + not_null controller, + not_null parent) override; + void confirmSave( bool someAreDisallowed, Fn saveCallback) override; private: const not_null<::Main::Session*> _session; + bool _hideReadTime = false; }; From 33643ff7fcd5343ea8124f06cd59910aabe29f7a Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 11:06:30 +0400 Subject: [PATCH 002/109] Add "show" button for last seen hidden. --- Telegram/SourceFiles/info/info.style | 11 ++++++ .../info/profile/info_profile_cover.cpp | 37 +++++++++++++++++++ .../info/profile/info_profile_cover.h | 3 ++ 3 files changed, 51 insertions(+) diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index c709a7014..fbd6157e3 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -304,6 +304,9 @@ InfoProfileCover { status: FlatLabel; statusLeft: pixels; statusTop: pixels; + showLastSeen: RoundButton; + showLastSeenPosition: point; + showLastSeenVisible: bool; rightSkip: pixels; } infoProfilePhotoInnerSize: 72px; @@ -334,6 +337,14 @@ infoProfileCover: InfoProfileCover { status: infoProfileStatus; statusLeft: 109px; statusTop: 58px; + showLastSeen: RoundButton(defaultActiveButton) { + width: -12px; + height: 20px; + textTop: 2px; + font: font(11px semibold); + } + showLastSeenPosition: point(4px, 56px); + showLastSeenVisible: true; rightSkip: 20px; } infoProfileMegagroupCover: InfoProfileCover(infoProfileCover) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index a15e49ccb..3f9445787 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "ui/controls/userpic_button.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/text/text_utilities.h" #include "base/unixtime.h" @@ -323,6 +324,7 @@ Cover::Cover( : nullptr) , _name(this, _st.name) , _status(this, _st.status) +, _showLastSeen(this, tr::lng_status_lastseen_show(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { _peer->updateFull(); @@ -333,6 +335,8 @@ Cover::Cover( _status->setAttribute(Qt::WA_TransparentForMouseEvents); } + setupShowLastSeen(); + _badge->setPremiumClickCallback([=] { if (const auto panel = _emojiStatusPanel.get()) { panel->show(_controller, _badge->widget(), _badge->sizeTag()); @@ -361,6 +365,34 @@ Cover::Cover( } } +void Cover::setupShowLastSeen() { + const auto user = _peer->asUser(); + if (_st.showLastSeenVisible + && user + && !user->isSelf() + && !user->isBot() + && !user->isServiceUser() + && user->session().premiumPossible()) { + rpl::combine( + user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::OnlineStatus), + Data::AmPremiumValue(&user->session()) + ) | rpl::start_with_next([=] { + _showLastSeen->setVisible( + (user->onlineTill == kOnlineHidden) + && !user->session().premium() + && user->session().premiumPossible()); + }, _showLastSeen->lifetime()); + } else { + _showLastSeen->hide(); + } + + _showLastSeen->setClickedCallback([=] { + ::Settings::ShowPremium(_controller, u"lastseen_hidden"_q); + }); +} + void Cover::setupChildGeometry() { widthValue( ) | rpl::start_with_next([this](int newWidth) { @@ -577,6 +609,11 @@ void Cover::refreshStatusGeometry(int newWidth) { auto statusWidth = newWidth - _st.statusLeft - _st.rightSkip; _status->resizeToWidth(statusWidth); _status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth); + const auto left = _st.statusLeft + _status->textMaxWidth(); + _showLastSeen->moveToLeft( + left + _st.showLastSeenPosition.x(), + _st.showLastSeenPosition.y(), + newWidth); } } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 42a40b190..20389e7c5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -20,6 +20,7 @@ class UserpicButton; class FlatLabel; template class SlideWrap; +class RoundButton; } // namespace Ui namespace HistoryView { @@ -126,6 +127,7 @@ private: Role role, rpl::producer title); + void setupShowLastSeen(); void setupChildGeometry(); void initViewers(rpl::producer title); void refreshStatusText(); @@ -149,6 +151,7 @@ private: object_ptr _iconButton; object_ptr _name = { nullptr }; object_ptr _status = { nullptr }; + object_ptr _showLastSeen = { nullptr }; //object_ptr _dropArea = { nullptr }; base::Timer _refreshStatusTimer; From e63d573414622b79b53d571a33f12a4958b88f13 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 Jan 2024 04:19:55 -0800 Subject: [PATCH 003/109] Proof-of-concept last seen hidden. --- .../icons/settings/premium/large_lastseen.png | Bin 0 -> 1864 bytes .../settings/premium/large_lastseen@2x.png | Bin 0 -> 3667 bytes .../settings/premium/large_lastseen@3x.png | Bin 0 -> 5500 bytes .../icons/settings/premium/large_readtime.png | Bin 0 -> 1768 bytes .../settings/premium/large_readtime@2x.png | Bin 0 -> 3483 bytes .../settings/premium/large_readtime@3x.png | Bin 0 -> 5251 bytes Telegram/Resources/langs/lang.strings | 16 +- .../SourceFiles/boxes/premium_preview_box.cpp | 195 ++++++++++++++++++ .../SourceFiles/boxes/premium_preview_box.h | 11 + .../info/profile/info_profile_cover.cpp | 45 +++- .../settings/settings_privacy_controllers.cpp | 21 +- .../settings/settings_privacy_controllers.h | 2 + Telegram/SourceFiles/ui/effects/premium.style | 26 +++ 13 files changed, 291 insertions(+), 25 deletions(-) create mode 100644 Telegram/Resources/icons/settings/premium/large_lastseen.png create mode 100644 Telegram/Resources/icons/settings/premium/large_lastseen@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/large_lastseen@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/large_readtime.png create mode 100644 Telegram/Resources/icons/settings/premium/large_readtime@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/large_readtime@3x.png diff --git a/Telegram/Resources/icons/settings/premium/large_lastseen.png b/Telegram/Resources/icons/settings/premium/large_lastseen.png new file mode 100644 index 0000000000000000000000000000000000000000..e47bd7935bc38ba2adea14a70d045dc999917df4 GIT binary patch literal 1864 zcmV-O2ePx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NIvPnciR9Fe^m}_XxZ5Y5G=i?kV za-Lx)d|1tL$g%JNV{1;M>)nTS-Ph^9p6kAjf1mIFzkkC6&6+iV@!xhWVety{MqJ9g~I zkt4fx>(;Sj$4;F(HEGi1^XJb`o;-Q{`0>Ms4~vV7j~_o?US9r3OmtP!xN+lo^X8p8 zb&9+H^6+=>-o0JBcJ=Dj%T;601fFBsv}t$m-gPtM&6_vhzI}6~e);lc@7}!;5fMS( zZVL_=Frc)w)V%EL*RN;Ko?WzP(a@nod-m+vv}sdh>esK&^Tox*Vfujs2cABCYKq|0 zty{M?Y}nA&{;TJiGiQGK^hs3{9}5;NXxFY?Ks|Nq)*U=}@UdgZR2HD5q=bMADC^9? zGp$*(M%D2sD_5>0o1D4bvHkn^KY8+`>LKcf4uw-%a^M)d3kvfB}xqr7&vfXb#=92H(Ev-|hPPNJAxkry4`dCAT3^7T+30s|R?b@~Sv#I8^9arVMckdQHI*}{WuMn^! zEiH{C#U->hs;EdeZ{D1eCU>n^vBFs;iLIGQb#`X@6>N(ZEl!*`A-BqZGiYYam?6k# z&z?1C(7-`4jZ-)Z3kw~{1}Zc(bkn9yXg_-NC_!paeu7kr^XJc-)(EUl!g;rD-4f)O zF=Kv8Ds=epVIgCgBK?8!(&XCBCN<7pkBRt z6e}U?kFc41KGbP~=uL@D-l+asxVC@_2WY=i4Nn46ocR14_ZwX4Z|{rYub=-02G;A^Tdv!9&F2vfj>SFKtV z5)xu!1g27$NJ{I32@_0=!VE3sxgG(oT)84geU=)7+O%mS1Y+J4P?#7@NlDS830R{l zqpe$gU%h&@DF}Sy#tp&fU}+mfBv~?Y!#~~^zsAtex<>)# zXj8-uX0#>d9H`oTaK1%mce!@$8a0#Doib&Lf8Y~&YKH*ydG(*A&Fhh^PjumJ9A=6o zCnuZtF{P|uzusg3))kC3lArXu)}WEl(P=t0HPyUt;>3xjLBNzBlL44ozjW!+Wy_XD zM@L(v;E!}GxtJ(~8Px9Hy?ZMYc|@K=s;H=-uPNR;Fl*K<%gxdU1B}Z!nNn1LmoI0)f_f&x{jKu%7M6T1tJ&Zmp) z`}glzH)-iEysl_w64FXH2uvv}E2Hj$fXd#XLk9{K0YTAw@#2NGgqY0#N%(Kn!uSPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91V4wp41ONa40RR91U;qFB0I4%yP5=N6y-7qtRCodHoOg^>MHj$nB2}bH z6D;(q5E3AQ2}L48P>~W!2p}Mch=6pZ*l3Zep+ppnfOJCWy+~Cl5>!A?s`TEZeZPFq zoxShO-1_d@UCF*bVCLL8WzL;DbJ}d0G^xK-4Ww!yRRgISNU<77n-U~Tn>KCs?AdeV z$dN8xx*IocT)%$(=FOWA9y~}9vL&$)>C>lw{`u!CR;*a6RH>3BOY-j%PdtHk(2-RV zv~%&|#Xk~$zyA8`4?q0y=bwLC$2?SRbn))H?=D)j=+dQ2Og`A}`0?X2XU^={v18u6 zc^?w(hT{)vH$zi#L{3ty;Cd{q|eu6M%L4_SFMd%F__wmOcbJ7Q)?g6Jzp+dul4|g`K=FOXzIdf(Y#KInuB}Pwa^ zu@i~IG3i9&jqy&5qTpe^_10U$t1`|fPC#sHwmhS3=gyrgTefVR_$HF&%d$JL4j(>T zsZyoHp~=+3EZDYfn^8xnPMwOc&CSx7KYzZFH~Ue_af>h`rca-4l=H8@{(ADsC*$?* zI5Lb}@wvR1aPbn$hVJ0OgN@Se-@l(d)CMUAaI03WjQp_fIqzd297ohQ-+a^PvfvMe z%(ZCIqDE_wm7I)QvX@B`Y%#vqo;`bdLH9HU6|7sgPG`eOAIxd+#5KG@ef##+g}-y> z4o*)vOnUU_p)YZ9~eVIUw!qJk!`nb-Nd};HfYemIM^0Q7Fc(SiGJB4B*%l|#f#g;6(TBV)Togz z*Q1X8x1g!I-+lL;aSlcxir%?%r^0n@5VSYaDN>{e`B;Je(n~KH z2TLGnT!9(O>HhM|FB>BPRuMpcnR_ChBM1iRl(xOVN@2^2hVyTOU@r=EIBpxwTGo6R+G zgy$ApDT!b~&DeFAhLzjgxpR%^4nXpYRtnZOez7zz?>k!^Q zaNvL-W!EL(?@E8ELd>(?*P!Zl_*E63h}GGGvJR+enbb z%LzbO%CD2m0>Pb3U}E@x&6+g=N_n7wNCiFp^wVMlWf%hD+Rc?KSLe>1U5BXwe2J=U zY>+Z(+qSLokpqw~mk@`;0BRh>Q$2Gf6~y@`Z~~N0vao$wHx$7LrlQLM93c8Pa^y%s zM|5}X+9e>I17bP5E9mIaqk@WS*KYv9&L>ceyjmy29UQfS zSjJ+35s=1^u_Cy(aU=bH4%pcaWbrNhrbB1Un6YZrDhHbC5k{YCM;aXeR!EeB^u>Z< zW*zONO}U(xVQLKTkc_bE>J9ze(8pLIczo}@_nsBv&?aFoo)T2ELZTF;dux7|52W>N z-pWsJ+qp|>UYyzz#@ z_0@pBk|)w|G1)O{1%hsUJ4-+aMo_TpPMtc{NQ&)-g{-i!s`~WlgN>#S#&c}aq)9CG zK3s}Xj3Ny}G&-Y12uFrMXx+MX;$si3@#Dt}3dSxfz`)VzkMPokn!_Y~c(FVIp00WJC`D#0|9WckU**(be=GRCg8; zOPwRhZtZ~EqD2d%Mg$Ng28#k4#p6sM`-HvPv}sf0014Fh=|O`A$wBey^8$q~j9r4$ z@!@Zv8f7R(o^$evf$Sy%V(ezhl!>huD~`k|#ap}(j6LH>J0O;k-6I9(UIBO0R5uwK zHX1TwdN02CqE5jIAuwdL8wH)XcPVhRibdN?>Y5y> zz_NCUytrNg;sn7vvtn7>RD=07>*J-VfddCxC8N#x=jxPE!X&WtF83v!g(eegJys$V zFbn$k@9!#rGvzR!62a4@v^$#>k_>KYLcFDntpO4*q#D6Yaf9DIs?iUP}rgh$dS0^f*K8ZOQ= z&phM2EyN^`aS;*5=Z?P}T|&Z2Bcc&|Fw~Bz4xcq^mg0`Ulu@uh;=xhkO1OuM{(5>s zC*ldhOHiCtgKWLsiHjj)yzPf`5SmOL1Sc?}q%}5(RZ>!d&p>S$`tm^1eh488ori-z z&GM!)5Cb}bMY-hr@4p{QJiuNw+b6#Mgov*!h=C==QxP+6F_{PRP+!tjN=xN zz)Sz1{5o}&`g=7(f(5*ynCfMnaKgIPt7)RplC^_d$9H7Zhyaof2|-fheP_-rfA!ZP z2;zf;$BIvvD7{C9WV?(db-34lbIl$(#y*S_!;5?9RPW+T2F z4xneT#P{W;Jkp13#K3eX7MADFpZ8nT;e)IHp&Xh7Bvc4oD8<^vV)9Sk5rf2wm4RyW zhdW$yl8$6Ue285maurHQy+Pebh^+r5g~v$2{rdIOZvZM` zNbR+3+48m5UQ>`r!(-)5a82Qz|Cg&q{zlgj3IIP`PGO%p3Rcv3^6k= z(~h7sVNN~16#m8-)+Teq`{&21qzr|#QS=0?Zkb7D*H24s({13QW)iD55aAW`g002ovPDHLkV1inv{YC%) literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/large_lastseen@3x.png b/Telegram/Resources/icons/settings/premium/large_lastseen@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd1e12ab09dc27ab556a77078fb322b56164616 GIT binary patch literal 5500 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/large_readtime.png b/Telegram/Resources/icons/settings/premium/large_readtime.png new file mode 100644 index 0000000000000000000000000000000000000000..c06294a82e44e211c095e6c567a4a3e266597aef GIT binary patch literal 1768 zcmVP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NIQb|NXR9Fe^SX)R{O%&c{8Jd>@ zyZqhMitHsSFG(huDB?o|Y7`NLz8FYKn1}``n77hE(e%X!>p{FnVreNESqP$J<}Jds zyk()5rd`zj|I-!YoW1Alv-hc&A36_*nYF%k*)wa_S~HUL_xC?r0Soi|UA1b}kt0Xk z+}t*A-u%zf&(hNJ{rmT0V`KR5?Ch+sumAGpi+Rd*NZHufoI7`}tgP(w=gJFH-MxFax-K{u9vCb;w2-l8&6*c4UXc9!{QRX$mxM4~#NgoI>FH_GIW#ncXwxc=b-r`w4oQCf`ZXvh zNUNkN+}^!=KYsj3nwy)OSFc`eN@9thn3za1_&;;zj3yb)AYWhKxw$#gUszaZT5kLH z?cctABLN&#(*pVt$Y<<>O$XEM+qVh*?AfywD^}?|BQ zbVv>Qd%T@HcmDYCgM9My^E0ZfsHliQm6er7L4Sx*P*6a&8X6kJDp#&tIX*s4Ae3=2 z$Si6|NC@&_US1wAy=BW5BovrKjIUk0mS_DMqX3aNZ{EBSu+1gmSh3wfpTeto`t&JT zM6G1><*-HKB5GAt6=VPA0QDHVkAet2EiEmm!MwtI_wLd3*s)_g{nz-(lPBb_zrVkL zt)FCXZ{O9`MLOZi)~#D*I`-|`N8G`|K^bdt`tIF3VmmuK3)nhIs9&|UwWJW~{@}rb zN(I26X#oad#P=$TNUSRH5NL>QrLbN~c6K&-ga6Z~Pa6~e@Zkf&)~{c0fI(M94Dsq2 zU>7G>uU^F#A3l6o!C53kL_{!el9Q8#7$SCbbPy6{&Hx4niGilafUz{$vu6*Y;nAZ< zRHB*p6B85YQe=1}I(Dqe%gg0^VTf*OY9hNnK0XEXd3 zQdn(x`SPWbE8&tr+N-Op1#C&el@Epqg8+w%Jca1^NBb-kR}yn_a!4vGD@)0hkmiYv z1zfjood6+8I6;)IfJ9>n48D?4Kt!V>6ciF!kQ_aFR0xpRHR|%^%VLBiu?818jWDUH zsq`hNR@Tc*(IZE_ASjQNT z4eZPKfwExXqw#8kSA=WVu9cRSq8;Gz7^AA-f~Lc^5{eTcQ_8035@mRn=op3#P(g7I zRBvQu+Qd`zls9kQ#Onkuvh1vq8QDbu=Y_rCFhB4{hBJZgfKOv$ONP|tms>-+!3-vm zEzvL255}%vzs@7@vWalP1p%e-?c2BLl%GF;4(E8x-|-(>f&TzKwvsa^bnR;Z0000< KMNUMnLSTZsmOowq literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/large_readtime@2x.png b/Telegram/Resources/icons/settings/premium/large_readtime@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f30a355ee8d7aa1c3d7b77632e1b2ebefd048d84 GIT binary patch literal 3483 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 JPDHLkV1msInZW=6 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/large_readtime@3x.png b/Telegram/Resources/icons/settings/premium/large_readtime@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1e7d022a30e9918d4aa81a699eebb8daf0ff6823 GIT binary patch literal 5251 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index bf186e1f7..73d0fde25 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -170,21 +170,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_remember" = "Remember this choice"; -"lng_lastseen_show_title" = "Show your last seen"; -"lng_lastseen_show_about" = "To see **{user}'s** Last Seen time, either start showing your own Last Seen time..."; -"lng_lastseen_show_button" = "Show my Last Seen"; +"lng_lastseen_show_title" = "Show Your Last Seen"; +"lng_lastseen_show_about" = "To see **{user}'s** Last Seen time, either start\nshowing your own Last Seen time..."; +"lng_lastseen_show_button" = "Show My Last Seen"; "lng_lastseen_or" = "or"; "lng_lastseen_premium_title" = "Upgrade to Premium"; -"lng_lastseen_premium_about" = "Subscription will let you see **{user}'s** Last Seen status without showing yours."; +"lng_lastseen_premium_about" = "Subscription will let you see **{user}'s** Last Seen\nstatus without showing yours."; "lng_lastseen_premium_button" = "Subscribe to Telegram Premium"; "lng_lastseen_shown_toast" = "Your last seen time is now visible."; -"lng_readtime_show_title" = "Show your read date"; -"lng_readtime_show_about" = "To see when **{user}** read the message, either start showing your own read time..."; -"lng_readtime_show_button" = "Show my Read Time"; +"lng_readtime_show_title" = "Show Your Read Date"; +"lng_readtime_show_about" = "To see when **{user}** read the message,\neither start showing your own read time..."; +"lng_readtime_show_button" = "Show My Read Time"; "lng_readtime_or" = "or"; "lng_readtime_premium_title" = "Upgrade to Premium"; -"lng_readtime_premium_about" = "Subscription will let you see **{user}'s** read time without showing yours."; +"lng_readtime_premium_about" = "Subscription will let you see **{user}'s** read time\nwithout showing yours."; "lng_readtime_premium_button" = "Subscribe to Telegram Premium"; "lng_readtime_shown_toast" = "Your read times are now visible."; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 06835d603..4f5afd8f0 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -55,6 +55,7 @@ constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); constexpr auto kStarOpacityOff = 0.1; constexpr auto kStarOpacityOn = 1.; constexpr auto kStarPeriod = 3 * crl::time(1000); +constexpr auto kShowOrLineOpacity = 0.3; using Data::ReactionId; @@ -1315,6 +1316,200 @@ void PremiumUnavailableBox(not_null box) { }); } +[[nodiscard]] object_ptr MakeShowOrPremiumIcon( + not_null parent, + not_null icon) { + const auto margin = st::showOrIconMargin; + const auto padding = st::showOrIconPadding; + const auto inner = padding.top() + icon->height() + padding.bottom(); + const auto full = margin.top() + inner + margin.bottom(); + auto result = object_ptr(parent, full); + const auto raw = result.data(); + + raw->resize(st::boxWideWidth, full); + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + const auto width = raw->width(); + const auto position = QPoint((width - inner) / 2, margin.top()); + const auto rect = QRect(position, QSize(inner, inner)); + const auto shift = QPoint(padding.left(), padding.top()); + p.setPen(Qt::NoPen); + p.setBrush(st::showOrIconBg); + p.drawEllipse(rect); + icon->paint(p, position + shift, width); + }, raw->lifetime()); + + return result; +} + +[[nodiscard]] object_ptr MakeShowOrLabel( + not_null parent, + rpl::producer text) { + auto result = object_ptr( + parent, + std::move(text), + st::showOrLabel); + const auto raw = result.data(); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + + const auto full = st::showOrLineWidth; + const auto left = (raw->width() - full) / 2; + const auto text = raw->textMaxWidth() + 2 * st::showOrLabelSkip; + const auto fill = (full - text) / 2; + const auto stroke = st::lineWidth; + const auto top = st::showOrLineTop; + p.setOpacity(kShowOrLineOpacity); + p.fillRect(left, top, fill, stroke, st::windowSubTextFg); + const auto start = left + full - fill; + p.fillRect(start, top, fill, stroke, st::windowSubTextFg); + }, raw->lifetime()); + + return result; +} + +void ShowOrPremiumBox( + not_null box, + ShowOrPremium type, + QString shortName, + Fn justShow, + Fn toPremium) { + struct Skin { + rpl::producer showTitle; + rpl::producer showAbout; + rpl::producer showButton; + rpl::producer orPremium; + rpl::producer premiumTitle; + rpl::producer premiumAbout; + rpl::producer premiumButton; + QString toast; + const style::icon *icon = nullptr; + }; + auto skin = (type == ShowOrPremium::LastSeen) + ? Skin{ + tr::lng_lastseen_show_title(), + tr::lng_lastseen_show_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Ui::Text::RichLangValue), + tr::lng_lastseen_show_button(), + tr::lng_lastseen_or(), + tr::lng_lastseen_premium_title(), + tr::lng_lastseen_premium_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Ui::Text::RichLangValue), + tr::lng_lastseen_premium_button(), + tr::lng_lastseen_shown_toast(tr::now), + &st::showOrIconLastSeen, + } + : (type == ShowOrPremium::ReadTime) + ? Skin{ + tr::lng_readtime_show_title(), + tr::lng_readtime_show_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Ui::Text::RichLangValue), + tr::lng_readtime_show_button(), + tr::lng_readtime_or(), + tr::lng_readtime_premium_title(), + tr::lng_readtime_premium_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Ui::Text::RichLangValue), + tr::lng_readtime_premium_button(), + tr::lng_readtime_shown_toast(tr::now), + &st::showOrIconReadTime, + } + : Skin(); + + box->setStyle(st::showOrBox); + box->setWidth(st::boxWideWidth); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + box->addRow(MakeShowOrPremiumIcon(box, skin.icon)); + box->addRow( + object_ptr( + box, + std::move(skin.showTitle), + st::boostCenteredTitle), + st::showOrTitlePadding); + box->addRow( + object_ptr( + box, + std::move(skin.showAbout), + st::boostText), + st::showOrAboutPadding); + const auto show = box->addRow( + object_ptr( + box, + std::move(skin.showButton), + st::showOrShowButton), + QMargins( + st::showOrBox.buttonPadding.left(), + 0, + st::showOrBox.buttonPadding.right(), + 0)); + show->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + box->addRow( + MakeShowOrLabel(box, std::move(skin.orPremium)), + st::showOrLabelPadding); + box->addRow( + object_ptr( + box, + std::move(skin.premiumTitle), + st::boostCenteredTitle), + st::showOrTitlePadding); + box->addRow( + object_ptr( + box, + std::move(skin.premiumAbout), + st::boostText), + st::showOrPremiumAboutPadding); + + const auto premium = Ui::CreateChild( + box.get(), + Ui::Premium::ButtonGradientStops()); + + const auto &st = st::premiumPreviewBox.button; + premium->resize(st::showOrShowButton.width, st::showOrShowButton.height); + + const auto label = Ui::CreateChild( + premium, + std::move(skin.premiumButton), + st::premiumPreviewButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + premium->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outer, int width) { + label->moveToLeft( + (outer - width) / 2, + st::premiumPreviewBox.button.textTop, + outer); + }, label->lifetime()); + + box->setShowFinishedCallback([=] { + premium->startGlareAnimation(); + }); + + box->addButton( + object_ptr::fromRaw(premium)); + + show->setClickedCallback([box, justShow, toast = skin.toast] { + justShow(); + box->uiShow()->showToast(toast); + box->closeBox(); + }); + premium->setClickedCallback(std::move(toPremium)); +} + void DoubledLimitsPreviewBox( not_null box, not_null session) { diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 60c5699ca..f49cdf618 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -83,6 +83,17 @@ void ShowPremiumPreviewToBuy( void PremiumUnavailableBox(not_null box); +enum class ShowOrPremium : uchar { + LastSeen, + ReadTime, +}; +void ShowOrPremiumBox( + not_null box, + ShowOrPremium type, + QString shortName, + Fn justShow, + Fn toPremium); + [[nodiscard]] object_ptr CreateUnlockButton( QWidget *parent, rpl::producer text); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 3f9445787..15ec3df2b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/profile/info_profile_cover.h" +#include "api/api_user_privacy.h" #include "data/data_peer_values.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_emoji_status_panel.h" #include "info/info_controller.h" #include "boxes/peers/edit_forum_topic_box.h" +#include "boxes/premium_preview_box.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "ui/controls/userpic_button.h" @@ -373,23 +375,54 @@ void Cover::setupShowLastSeen() { && !user->isBot() && !user->isServiceUser() && user->session().premiumPossible()) { + if (user->session().premium()) { + if (user->onlineTill == kOnlineHidden) { + user->updateFullForced(); + } + _showLastSeen->hide(); + return; + } + rpl::combine( user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::OnlineStatus), Data::AmPremiumValue(&user->session()) - ) | rpl::start_with_next([=] { - _showLastSeen->setVisible( - (user->onlineTill == kOnlineHidden) - && !user->session().premium() - && user->session().premiumPossible()); + ) | rpl::start_with_next([=](auto, bool premium) { + const auto wasShown = !_showLastSeen->isHidden(); + const auto onlineHidden = (user->onlineTill == kOnlineHidden); + const auto shown = onlineHidden + && !premium + && user->session().premiumPossible(); + _showLastSeen->setVisible(shown); + if (wasShown && premium && onlineHidden) { + user->updateFullForced(); + } + }, _showLastSeen->lifetime()); + + _controller->session().api().userPrivacy().value( + Api::UserPrivacy::Key::LastSeen + ) | rpl::filter([=](Api::UserPrivacy::Rule rule) { + return (rule.option == Api::UserPrivacy::Option::Everyone); + }) | rpl::start_with_next([=] { + if (user->onlineTill == kOnlineHidden) { + user->updateFullForced(); + } }, _showLastSeen->lifetime()); } else { _showLastSeen->hide(); } _showLastSeen->setClickedCallback([=] { - ::Settings::ShowPremium(_controller, u"lastseen_hidden"_q); + const auto type = ShowOrPremium::LastSeen; + auto box = Box(ShowOrPremiumBox, type, user->shortName(), [=] { + _controller->session().api().userPrivacy().save( + ::Api::UserPrivacy::Key::LastSeen, + {}); + }, [=] { + ::Settings::ShowPremium(_controller, u"lastseen_hidden"_q); + }); + _controller->show(std::move(box)); }); } diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index c38afcd32..34e98bd42 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -710,21 +710,13 @@ object_ptr LastSeenPrivacyController::setupBelowWidget( void LastSeenPrivacyController::confirmSave( bool someAreDisallowed, Fn saveCallback) { - const auto privacy = &_session->api().globalPrivacy(); - const auto hideReadTime = _hideReadTime; - const auto save = [=, saveCallback = std::move(saveCallback)] { - if (privacy->hideReadTimeCurrent() != hideReadTime) { - privacy->updateHideReadTime(hideReadTime); - } - saveCallback(); - }; if (someAreDisallowed && !Core::App().settings().lastSeenWarningSeen()) { auto callback = [ =, - save = std::move(save) + saveCallback = std::move(saveCallback) ](Fn &&close) { close(); - save(); + saveCallback(); Core::App().settings().setLastSeenWarningSeen(true); Core::App().saveSettingsDelayed(); }; @@ -735,7 +727,14 @@ void LastSeenPrivacyController::confirmSave( }); Ui::show(std::move(box), Ui::LayerOption::KeepOther); } else { - save(); + saveCallback(); + } +} + +void LastSeenPrivacyController::saveAdditional() { + const auto privacy = &_session->api().globalPrivacy(); + if (privacy->hideReadTimeCurrent() != _hideReadTime) { + privacy->updateHideReadTime(_hideReadTime); } } diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 5211d0731..7b097ec44 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -117,6 +117,8 @@ public: bool someAreDisallowed, Fn saveCallback) override; + void saveAdditional() override; + private: const not_null<::Main::Session*> _session; bool _hideReadTime = false; diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index c9c9ccd84..5b97d8cb4 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -303,3 +303,29 @@ boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); boostReplaceArrow: icon{{ "mediaview/next", windowSubTextFg }}; + +showOrIconLastSeen: icon{{ "settings/premium/large_lastseen", windowFgActive }}; +showOrIconReadTime: icon{{ "settings/premium/large_readtime", windowFgActive }}; +showOrIconBg: windowBgActive; +showOrIconPadding: margins(12px, 12px, 12px, 12px); +showOrIconMargin: margins(0px, 28px, 0px, 12px); +showOrTitlePadding: margins(0px, 0px, 0px, 5px); +showOrAboutPadding: margins(0px, 0px, 0px, 16px); +showOrShowButton: RoundButton(defaultActiveButton) { + width: 308px; + height: 42px; + textTop: 12px; + font: font(13px semibold); +} +showOrLabel: FlatLabel(boostText) { + textFg: windowSubTextFg; +} +showOrLineWidth: 190px; +showOrLabelSkip: 7px; +showOrLineTop: 10px; +showOrLabelPadding: margins(0px, 17px, 0px, 13px); +showOrPremiumAboutPadding: margins(0px, 0px, 0px, 0px); +showOrBox: Box(boostBox) { + buttonPadding: margins(28px, 16px, 28px, 27px); + button: showOrShowButton; +} From 474f1118b606e305d84d2eb995d39e96dbb23562 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 Jan 2024 17:49:02 +0400 Subject: [PATCH 004/109] Proof-of-concept read time in private chats. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 103 +++++++++++++++--- .../view/history_view_context_menu.cpp | 19 +++- .../controls/who_reacted_context_action.cpp | 16 ++- .../ui/controls/who_reacted_context_action.h | 10 +- 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index f32358c06..27134dea9 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_who_reacted.h" +#include "api/api_global_privacy.h" #include "history/history_item.h" #include "history/history.h" #include "data/stickers/data_custom_emoji.h" @@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_message_reaction_id.h" +#include "data/data_peer_values.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -36,10 +38,11 @@ namespace { constexpr auto kContextReactionsLimit = 50; using Data::ReactionId; +using WhoReadState = Ui::WhoReadState; struct Peers { std::vector list; - bool unknown = false; + WhoReadState state = WhoReadState::Empty; friend inline bool operator==( const Peers &a, @@ -59,7 +62,7 @@ struct PeersWithReactions { std::vector list; std::vector read; int fullReactionsCount = 0; - bool unknown = false; + WhoReadState state = WhoReadState::Empty; friend inline bool operator==( const PeersWithReactions &a, @@ -68,7 +71,7 @@ struct PeersWithReactions { struct CachedRead { CachedRead() - : data(Peers{ .unknown = true }) { + : data(Peers{ .state = WhoReadState::Unknown }) { } rpl::variable data; mtpRequestId requestId = 0; @@ -76,7 +79,7 @@ struct CachedRead { struct CachedReacted { CachedReacted() - : data(PeersWithReactions{ .unknown = true }) { + : data(PeersWithReactions{ .state = WhoReadState::Unknown }) { } rpl::variable data; mtpRequestId requestId = 0; @@ -186,6 +189,27 @@ struct State { context->cachedReacted.erase(j); } }, context->subscriptions[session]); + Data::AmPremiumValue( + session + ) | rpl::skip(1) | rpl::filter( + rpl::mappers::_1 + ) | rpl::start_with_next([=] { + for (auto &[item, cache] : context->cachedRead) { + if (cache.data.current().state == Ui::WhoReadState::MyHidden) { + cache.data = Peers{ .state = Ui::WhoReadState::Unknown }; + } + } + }, context->subscriptions[session]); + session->api().globalPrivacy().hideReadTime( + ) | rpl::skip(1) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next([=] { + for (auto &[item, cache] : context->cachedRead) { + if (cache.data.current().state == Ui::WhoReadState::MyHidden) { + cache.data = Peers{ .state = Ui::WhoReadState::Unknown }; + } + } + }, context->subscriptions[session]); return context; } @@ -222,7 +246,38 @@ struct State { } const auto context = PreparedContextAt(weak.data(), session); auto &entry = context->cacheRead(item); - if (!entry.requestId) { + if (entry.requestId) { + } else if (const auto user = item->history()->peer->asUser()) { + entry.requestId = session->api().request( + MTPmessages_GetOutboxReadDate( + user->input, + MTP_int(item->id) + ) + ).done([=](const MTPOutboxReadDate &result) { + const auto &data = result.data(); + auto &entry = context->cacheRead(item); + entry.requestId = 0; + auto parsed = Peers(); + parsed.list.push_back({ + .peer = user->id, + .date = data.vdate().v, + }); + entry.data = std::move(parsed); + }).fail([=](const MTP::Error &error) { + auto &entry = context->cacheRead(item); + entry.requestId = 0; + if (entry.data.current().state == WhoReadState::Unknown) { + const auto &text = error.type(); + entry.data = (text == u"YOUR_PRIVACY_RESTRICTED"_q) + ? Peers{ .state = WhoReadState::MyHidden } + : (text == u"USER_PRIVACY_RESTRICTED"_q) + ? Peers{ .state = WhoReadState::HisHidden } + : (text == u"MESSAGE_TOO_OLD"_q) + ? Peers{ .state = WhoReadState::TooOld } + : Peers{ .state = WhoReadState::Empty }; + } + }).send(); + } else { entry.requestId = session->api().request( MTPmessages_GetMessageReadParticipants( item->history()->peer->input, @@ -243,8 +298,8 @@ struct State { }).fail([=] { auto &entry = context->cacheRead(item); entry.requestId = 0; - if (entry.data.current().unknown) { - entry.data = Peers(); + if (entry.data.current().state == WhoReadState::Unknown) { + entry.data = Peers{ .state = WhoReadState::Empty }; } }).send(); } @@ -258,7 +313,7 @@ struct State { .list = peers.list | ranges::views::transform([](WhoReadPeer peer) { return PeerWithReaction{ .peerWithDate = peer }; }) | ranges::to_vector, - .unknown = peers.unknown, + .state = peers.state, }; result.read = std::move(peers.list); return result; @@ -319,8 +374,10 @@ struct State { }).fail([=] { auto &entry = context->cacheReacted(item, reaction); entry.requestId = 0; - if (entry.data.current().unknown) { - entry.data = PeersWithReactions(); + if (entry.data.current().state == WhoReadState::Unknown) { + entry.data = PeersWithReactions{ + .state = WhoReadState::Empty, + }; } }).send(); } @@ -336,8 +393,9 @@ struct State { WhoReactedIds(item, {}, context), WhoReadIds(item, context) ) | rpl::map([=](PeersWithReactions &&reacted, Peers &&read) { - if (reacted.unknown || read.unknown) { - return PeersWithReactions{ .unknown = true }; + if (reacted.state == WhoReadState::Unknown + || read.state == WhoReadState::Unknown) { + return PeersWithReactions{ .state = WhoReadState::Unknown}; } auto &list = reacted.list; for (const auto &peerWithDate : read.list) { @@ -531,16 +589,17 @@ rpl::producer WhoReacted( std::move( idsWithReactions ) | rpl::start_with_next([=](PeersWithReactions &&peers) { - if (peers.unknown) { + if (peers.state == WhoReadState::Unknown) { state->userpics.clear(); consumer.put_next(Ui::WhoReadContent{ .type = state->current.type, .fullReactionsCount = state->current.fullReactionsCount, .fullReadCount = state->current.fullReadCount, - .unknown = true, + .state = WhoReadState::Unknown, }); return; } + state->current.state = peers.state; state->current.fullReadCount = int(peers.read.size()); state->current.fullReactionsCount = peers.fullReactionsCount; if (whoReadIds) { @@ -631,6 +690,22 @@ bool WhoReadExists(not_null item) { } const auto history = item->history(); const auto peer = history->peer; + if (const auto user = peer->asUser()) { + if (user->isSelf() + || user->isBot() + || user->isServiceUser() + || user->readDatesPrivate()) { + return false; + } + const auto &appConfig = peer->session().account().appConfig(); + const auto expirePeriod = appConfig.get( + "pm_read_date_expire_period", + 7 * 86400); + if (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) { + return false; + } + return true; + } const auto chat = peer->asChat(); const auto megagroup = peer->asMegagroup(); if ((!chat && !megagroup) diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 856639aa8..7cb610211 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_attached_stickers.h" #include "api/api_editing.h" +#include "api/api_global_privacy.h" #include "api/api_polls.h" #include "api/api_report.h" #include "api/api_ringtones.h" @@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" +#include "boxes/premium_preview_box.h" #include "boxes/sticker_set_box.h" #include "boxes/stickers_box.h" #include "boxes/translate_box.h" @@ -62,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "base/platform/base_platform_info.h" #include "base/call_delayed.h" +#include "settings/settings_premium.h" #include "window/window_peer_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -1265,11 +1268,23 @@ void AddWhoReactedAction( not_null controller) { const auto whoReadIds = std::make_shared(); const auto weak = Ui::MakeWeak(menu.get()); + const auto user = item->history()->peer; const auto participantChosen = [=](uint64 id) { if (const auto strong = weak.data()) { strong->hideMenu(); } - controller->showPeerInfo(PeerId(id)); + if (id) { + controller->showPeerInfo(PeerId(id)); + } else { + const auto type = ShowOrPremium::ReadTime; + auto box = Box(ShowOrPremiumBox, type, user->shortName(), [=] { + const auto api = &controller->session().api(); + api->globalPrivacy().updateHideReadTime({}); + }, [=] { + Settings::ShowPremium(controller, u"revtime_hidden"_q); + }); + controller->show(std::move(box)); + } }; const auto showAllChosen = [=, itemId = item->fullId()]{ // Pressing on an item that has a submenu doesn't hide it :( @@ -1396,7 +1411,7 @@ void ShowWhoReactedMenu( context, st::defaultWhoRead ) | rpl::filter([=](const Ui::WhoReadContent &content) { - return !content.unknown; + return content.state != Ui::WhoReadState::Unknown; }) | rpl::start_with_next([=, &lifetime](Ui::WhoReadContent &&content) { const auto creating = !*menu; const auto refillTop = [=] { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 3d6ae7638..b6d24e3b8 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -178,7 +178,7 @@ Action::Action( ) | rpl::start_with_next([=](WhoReadContent &&content) { checkAppeared(); const auto changed = (_content.participants != content.participants) - || (_content.unknown != content.unknown); + || (_content.state != content.state); _content = content; if (changed) { PostponeCall(this, [=] { populateSubmenu(); }); @@ -219,6 +219,10 @@ Action::Action( if (const auto onstack = _showAllChosen) { onstack(); } + } else if (_content.state == WhoReadState::MyHidden) { + if (const auto onstack = _participantChosen) { + onstack(0); + } } }, lifetime()); @@ -379,8 +383,13 @@ void Action::refreshText() { const auto count = std::max(_content.fullReactionsCount, usersCount); _text.setMarkedText( _st.itemStyle, - { (_content.unknown + { ((_content.state == WhoReadState::Unknown) ? tr::lng_context_seen_loading(tr::now) + : (_content.state == WhoReadState::MyHidden) + ? tr::lng_context_read_show(tr::now) + : (_content.state == WhoReadState::HisHidden + || _content.state == WhoReadState::TooOld) + ? tr::lng_context_read_hidden(tr::now) : (usersCount == 1) ? _content.participants.front().name : (_content.fullReactionsCount > 0 @@ -431,7 +440,8 @@ void Action::refreshDimensions() { } bool Action::isEnabled() const { - return !_content.participants.empty(); + return !_content.participants.empty() + || (_content.state == WhoReadState::MyHidden); } not_null Action::action() const { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index 0b668b524..77a54a4e2 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -38,13 +38,21 @@ enum class WhoReadType { Reacted, }; +enum class WhoReadState : uchar { + Empty, + Unknown, + MyHidden, + HisHidden, + TooOld, +}; + struct WhoReadContent { std::vector participants; WhoReadType type = WhoReadType::Seen; QString singleCustomEntityData; int fullReactionsCount = 0; int fullReadCount = 0; - bool unknown = false; + WhoReadState state = WhoReadState::Empty; }; [[nodiscard]] base::unique_qptr WhoReactedContextAction( From ad03431b0a447ef7366ffa164dc01749f1455e2b Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 10:31:09 +0400 Subject: [PATCH 005/109] Improve "read time" context menu info design. --- .../SourceFiles/boxes/premium_preview_box.cpp | 195 ------------ .../SourceFiles/boxes/premium_preview_box.h | 11 - .../chat_helpers/chat_helpers.style | 7 + .../view/history_view_context_menu.cpp | 48 +-- .../info/profile/info_profile_cover.cpp | 6 +- .../ui/boxes/show_or_premium_box.cpp | 223 ++++++++++++++ .../ui/boxes/show_or_premium_box.h | 25 ++ .../controls/who_reacted_context_action.cpp | 279 +++++++++++++++++- .../ui/controls/who_reacted_context_action.h | 5 + Telegram/cmake/td_ui.cmake | 2 + 10 files changed, 564 insertions(+), 237 deletions(-) create mode 100644 Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp create mode 100644 Telegram/SourceFiles/ui/boxes/show_or_premium_box.h diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 4f5afd8f0..06835d603 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -55,7 +55,6 @@ constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); constexpr auto kStarOpacityOff = 0.1; constexpr auto kStarOpacityOn = 1.; constexpr auto kStarPeriod = 3 * crl::time(1000); -constexpr auto kShowOrLineOpacity = 0.3; using Data::ReactionId; @@ -1316,200 +1315,6 @@ void PremiumUnavailableBox(not_null box) { }); } -[[nodiscard]] object_ptr MakeShowOrPremiumIcon( - not_null parent, - not_null icon) { - const auto margin = st::showOrIconMargin; - const auto padding = st::showOrIconPadding; - const auto inner = padding.top() + icon->height() + padding.bottom(); - const auto full = margin.top() + inner + margin.bottom(); - auto result = object_ptr(parent, full); - const auto raw = result.data(); - - raw->resize(st::boxWideWidth, full); - raw->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(raw); - auto hq = PainterHighQualityEnabler(p); - const auto width = raw->width(); - const auto position = QPoint((width - inner) / 2, margin.top()); - const auto rect = QRect(position, QSize(inner, inner)); - const auto shift = QPoint(padding.left(), padding.top()); - p.setPen(Qt::NoPen); - p.setBrush(st::showOrIconBg); - p.drawEllipse(rect); - icon->paint(p, position + shift, width); - }, raw->lifetime()); - - return result; -} - -[[nodiscard]] object_ptr MakeShowOrLabel( - not_null parent, - rpl::producer text) { - auto result = object_ptr( - parent, - std::move(text), - st::showOrLabel); - const auto raw = result.data(); - - raw->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(raw); - - const auto full = st::showOrLineWidth; - const auto left = (raw->width() - full) / 2; - const auto text = raw->textMaxWidth() + 2 * st::showOrLabelSkip; - const auto fill = (full - text) / 2; - const auto stroke = st::lineWidth; - const auto top = st::showOrLineTop; - p.setOpacity(kShowOrLineOpacity); - p.fillRect(left, top, fill, stroke, st::windowSubTextFg); - const auto start = left + full - fill; - p.fillRect(start, top, fill, stroke, st::windowSubTextFg); - }, raw->lifetime()); - - return result; -} - -void ShowOrPremiumBox( - not_null box, - ShowOrPremium type, - QString shortName, - Fn justShow, - Fn toPremium) { - struct Skin { - rpl::producer showTitle; - rpl::producer showAbout; - rpl::producer showButton; - rpl::producer orPremium; - rpl::producer premiumTitle; - rpl::producer premiumAbout; - rpl::producer premiumButton; - QString toast; - const style::icon *icon = nullptr; - }; - auto skin = (type == ShowOrPremium::LastSeen) - ? Skin{ - tr::lng_lastseen_show_title(), - tr::lng_lastseen_show_about( - lt_user, - rpl::single(TextWithEntities{ shortName }), - Ui::Text::RichLangValue), - tr::lng_lastseen_show_button(), - tr::lng_lastseen_or(), - tr::lng_lastseen_premium_title(), - tr::lng_lastseen_premium_about( - lt_user, - rpl::single(TextWithEntities{ shortName }), - Ui::Text::RichLangValue), - tr::lng_lastseen_premium_button(), - tr::lng_lastseen_shown_toast(tr::now), - &st::showOrIconLastSeen, - } - : (type == ShowOrPremium::ReadTime) - ? Skin{ - tr::lng_readtime_show_title(), - tr::lng_readtime_show_about( - lt_user, - rpl::single(TextWithEntities{ shortName }), - Ui::Text::RichLangValue), - tr::lng_readtime_show_button(), - tr::lng_readtime_or(), - tr::lng_readtime_premium_title(), - tr::lng_readtime_premium_about( - lt_user, - rpl::single(TextWithEntities{ shortName }), - Ui::Text::RichLangValue), - tr::lng_readtime_premium_button(), - tr::lng_readtime_shown_toast(tr::now), - &st::showOrIconReadTime, - } - : Skin(); - - box->setStyle(st::showOrBox); - box->setWidth(st::boxWideWidth); - box->addTopButton(st::boxTitleClose, [=] { - box->closeBox(); - }); - - box->addRow(MakeShowOrPremiumIcon(box, skin.icon)); - box->addRow( - object_ptr( - box, - std::move(skin.showTitle), - st::boostCenteredTitle), - st::showOrTitlePadding); - box->addRow( - object_ptr( - box, - std::move(skin.showAbout), - st::boostText), - st::showOrAboutPadding); - const auto show = box->addRow( - object_ptr( - box, - std::move(skin.showButton), - st::showOrShowButton), - QMargins( - st::showOrBox.buttonPadding.left(), - 0, - st::showOrBox.buttonPadding.right(), - 0)); - show->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - box->addRow( - MakeShowOrLabel(box, std::move(skin.orPremium)), - st::showOrLabelPadding); - box->addRow( - object_ptr( - box, - std::move(skin.premiumTitle), - st::boostCenteredTitle), - st::showOrTitlePadding); - box->addRow( - object_ptr( - box, - std::move(skin.premiumAbout), - st::boostText), - st::showOrPremiumAboutPadding); - - const auto premium = Ui::CreateChild( - box.get(), - Ui::Premium::ButtonGradientStops()); - - const auto &st = st::premiumPreviewBox.button; - premium->resize(st::showOrShowButton.width, st::showOrShowButton.height); - - const auto label = Ui::CreateChild( - premium, - std::move(skin.premiumButton), - st::premiumPreviewButtonLabel); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - rpl::combine( - premium->widthValue(), - label->widthValue() - ) | rpl::start_with_next([=](int outer, int width) { - label->moveToLeft( - (outer - width) / 2, - st::premiumPreviewBox.button.textTop, - outer); - }, label->lifetime()); - - box->setShowFinishedCallback([=] { - premium->startGlareAnimation(); - }); - - box->addButton( - object_ptr::fromRaw(premium)); - - show->setClickedCallback([box, justShow, toast = skin.toast] { - justShow(); - box->uiShow()->showToast(toast); - box->closeBox(); - }); - premium->setClickedCallback(std::move(toPremium)); -} - void DoubledLimitsPreviewBox( not_null box, not_null session) { diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index f49cdf618..60c5699ca 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -83,17 +83,6 @@ void ShowPremiumPreviewToBuy( void PremiumUnavailableBox(not_null box); -enum class ShowOrPremium : uchar { - LastSeen, - ReadTime, -}; -void ShowOrPremiumBox( - not_null box, - ShowOrPremium type, - QString shortName, - Fn justShow, - Fn toPremium); - [[nodiscard]] object_ptr CreateUnlockButton( QWidget *parent, rpl::producer text); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 99f6f9900..454c58d0e 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -249,6 +249,13 @@ defaultWhoRead: WhoRead { iconPosition: point(15px, 7px); itemPadding: margins(44px, 9px, 17px, 7px); } +whenReadStyle: TextStyle(defaultTextStyle) { + font: font(12px); +} +whenReadPadding: margins(34px, 3px, 17px, 4px); +whenReadIconPosition: point(8px, 0px); +whenReadSkip: 3px; +whenReadShowPadding: margins(6px, 0px, 6px, 2px); switchPmButton: RoundButton(defaultBoxButton) { width: 320px; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 7cb610211..52170fa51 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -40,9 +40,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_item_download_files.h" #include "menu/menu_send.h" #include "ui/boxes/confirm_box.h" +#include "ui/boxes/show_or_premium_box.h" #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" -#include "boxes/premium_preview_box.h" #include "boxes/sticker_set_box.h" #include "boxes/stickers_box.h" #include "boxes/translate_box.h" @@ -1269,22 +1269,25 @@ void AddWhoReactedAction( const auto whoReadIds = std::make_shared(); const auto weak = Ui::MakeWeak(menu.get()); const auto user = item->history()->peer; + const auto showOrPremium = [=] { + if (const auto strong = weak.data()) { + strong->hideMenu(); + } + const auto type = Ui::ShowOrPremium::ReadTime; + const auto name = user->shortName(); + auto box = Box(Ui::ShowOrPremiumBox, type, name, [=] { + const auto api = &controller->session().api(); + api->globalPrivacy().updateHideReadTime({}); + }, [=] { + Settings::ShowPremium(controller, u"revtime_hidden"_q); + }); + controller->show(std::move(box)); + }; const auto participantChosen = [=](uint64 id) { if (const auto strong = weak.data()) { strong->hideMenu(); } - if (id) { - controller->showPeerInfo(PeerId(id)); - } else { - const auto type = ShowOrPremium::ReadTime; - auto box = Box(ShowOrPremiumBox, type, user->shortName(), [=] { - const auto api = &controller->session().api(); - api->globalPrivacy().updateHideReadTime({}); - }, [=] { - Settings::ShowPremium(controller, u"revtime_hidden"_q); - }); - controller->show(std::move(box)); - } + controller->showPeerInfo(PeerId(id)); }; const auto showAllChosen = [=, itemId = item->fullId()]{ // Pressing on an item that has a submenu doesn't hide it :( @@ -1302,12 +1305,19 @@ void AddWhoReactedAction( if (!menu->empty()) { menu->addSeparator(&st::expandedMenuSeparator); } - menu->addAction(Ui::WhoReactedContextAction( - menu.get(), - Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), - Data::ReactedMenuFactory(&controller->session()), - participantChosen, - showAllChosen)); + if (item->history()->peer->isUser()) { + menu->addAction(Ui::WhenReadContextAction( + menu.get(), + Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), + showOrPremium)); + } else { + menu->addAction(Ui::WhoReactedContextAction( + menu.get(), + Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), + Data::ReactedMenuFactory(&controller->session()), + participantChosen, + showAllChosen)); + } } void ShowTagMenu( diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 15ec3df2b..f430c03d7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -24,9 +24,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_emoji_status_panel.h" #include "info/info_controller.h" #include "boxes/peers/edit_forum_topic_box.h" -#include "boxes/premium_preview_box.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" +#include "ui/boxes/show_or_premium_box.h" #include "ui/controls/userpic_button.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -414,8 +414,8 @@ void Cover::setupShowLastSeen() { } _showLastSeen->setClickedCallback([=] { - const auto type = ShowOrPremium::LastSeen; - auto box = Box(ShowOrPremiumBox, type, user->shortName(), [=] { + const auto type = Ui::ShowOrPremium::LastSeen; + auto box = Box(Ui::ShowOrPremiumBox, type, user->shortName(), [=] { _controller->session().api().userPrivacy().save( ::Api::UserPrivacy::Key::LastSeen, {}); diff --git a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp new file mode 100644 index 000000000..0e210cb72 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp @@ -0,0 +1,223 @@ +/* +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 "ui/boxes/show_or_premium_box.h" + +#include "base/object_ptr.h" +#include "lang/lang_keys.h" +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/widgets/labels.h" +#include "ui/painter.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" + +namespace Ui { +namespace { + +constexpr auto kShowOrLineOpacity = 0.3; + +[[nodiscard]] object_ptr MakeShowOrPremiumIcon( + not_null parent, + not_null icon) { + const auto margin = st::showOrIconMargin; + const auto padding = st::showOrIconPadding; + const auto inner = padding.top() + icon->height() + padding.bottom(); + const auto full = margin.top() + inner + margin.bottom(); + auto result = object_ptr(parent, full); + const auto raw = result.data(); + + raw->resize(st::boxWideWidth, full); + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + const auto width = raw->width(); + const auto position = QPoint((width - inner) / 2, margin.top()); + const auto rect = QRect(position, QSize(inner, inner)); + const auto shift = QPoint(padding.left(), padding.top()); + p.setPen(Qt::NoPen); + p.setBrush(st::showOrIconBg); + p.drawEllipse(rect); + icon->paint(p, position + shift, width); + }, raw->lifetime()); + + return result; +} + +[[nodiscard]] object_ptr MakeShowOrLabel( + not_null parent, + rpl::producer text) { + auto result = object_ptr( + parent, + std::move(text), + st::showOrLabel); + const auto raw = result.data(); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + + const auto full = st::showOrLineWidth; + const auto left = (raw->width() - full) / 2; + const auto text = raw->textMaxWidth() + 2 * st::showOrLabelSkip; + const auto fill = (full - text) / 2; + const auto stroke = st::lineWidth; + const auto top = st::showOrLineTop; + p.setOpacity(kShowOrLineOpacity); + p.fillRect(left, top, fill, stroke, st::windowSubTextFg); + const auto start = left + full - fill; + p.fillRect(start, top, fill, stroke, st::windowSubTextFg); + }, raw->lifetime()); + + return result; +} + +} // namespace + +void ShowOrPremiumBox( + not_null box, + ShowOrPremium type, + QString shortName, + Fn justShow, + Fn toPremium) { + struct Skin { + rpl::producer showTitle; + rpl::producer showAbout; + rpl::producer showButton; + rpl::producer orPremium; + rpl::producer premiumTitle; + rpl::producer premiumAbout; + rpl::producer premiumButton; + QString toast; + const style::icon *icon = nullptr; + }; + auto skin = (type == ShowOrPremium::LastSeen) + ? Skin{ + tr::lng_lastseen_show_title(), + tr::lng_lastseen_show_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Text::RichLangValue), + tr::lng_lastseen_show_button(), + tr::lng_lastseen_or(), + tr::lng_lastseen_premium_title(), + tr::lng_lastseen_premium_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Text::RichLangValue), + tr::lng_lastseen_premium_button(), + tr::lng_lastseen_shown_toast(tr::now), + &st::showOrIconLastSeen, + } + : (type == ShowOrPremium::ReadTime) + ? Skin{ + tr::lng_readtime_show_title(), + tr::lng_readtime_show_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Text::RichLangValue), + tr::lng_readtime_show_button(), + tr::lng_readtime_or(), + tr::lng_readtime_premium_title(), + tr::lng_readtime_premium_about( + lt_user, + rpl::single(TextWithEntities{ shortName }), + Text::RichLangValue), + tr::lng_readtime_premium_button(), + tr::lng_readtime_shown_toast(tr::now), + &st::showOrIconReadTime, + } + : Skin(); + + box->setStyle(st::showOrBox); + box->setWidth(st::boxWideWidth); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + box->addRow(MakeShowOrPremiumIcon(box, skin.icon)); + box->addRow( + object_ptr( + box, + std::move(skin.showTitle), + st::boostCenteredTitle), + st::showOrTitlePadding); + box->addRow( + object_ptr( + box, + std::move(skin.showAbout), + st::boostText), + st::showOrAboutPadding); + const auto show = box->addRow( + object_ptr( + box, + std::move(skin.showButton), + st::showOrShowButton), + QMargins( + st::showOrBox.buttonPadding.left(), + 0, + st::showOrBox.buttonPadding.right(), + 0)); + show->setTextTransform(RoundButton::TextTransform::NoTransform); + box->addRow( + MakeShowOrLabel(box, std::move(skin.orPremium)), + st::showOrLabelPadding); + box->addRow( + object_ptr( + box, + std::move(skin.premiumTitle), + st::boostCenteredTitle), + st::showOrTitlePadding); + box->addRow( + object_ptr( + box, + std::move(skin.premiumAbout), + st::boostText), + st::showOrPremiumAboutPadding); + + const auto premium = CreateChild( + box.get(), + Premium::ButtonGradientStops()); + + const auto &st = st::premiumPreviewBox.button; + premium->resize(st::showOrShowButton.width, st::showOrShowButton.height); + + const auto label = CreateChild( + premium, + std::move(skin.premiumButton), + st::premiumPreviewButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + premium->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outer, int width) { + label->moveToLeft( + (outer - width) / 2, + st::premiumPreviewBox.button.textTop, + outer); + }, label->lifetime()); + + box->setShowFinishedCallback([=] { + premium->startGlareAnimation(); + }); + + box->addButton( + object_ptr::fromRaw(premium)); + + show->setClickedCallback([box, justShow, toast = skin.toast] { + justShow(); + box->uiShow()->showToast(toast); + box->closeBox(); + }); + premium->setClickedCallback(std::move(toPremium)); +} + +} // namespace Ui \ No newline at end of file diff --git a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h new file mode 100644 index 000000000..4c0a10104 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h @@ -0,0 +1,25 @@ +/* +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 +*/ +#pragma once + +namespace Ui { + +class GenericBox; + +enum class ShowOrPremium : uchar { + LastSeen, + ReadTime, +}; +void ShowOrPremiumBox( + not_null box, + ShowOrPremium type, + QString shortName, + Fn justShow, + Fn toPremium); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index b6d24e3b8..f9c3b0458 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -124,6 +124,45 @@ private: }; +class WhenAction final : public Menu::ItemBase { +public: + WhenAction( + not_null parentMenu, + rpl::producer content, + Fn showOrPremium); + + bool isEnabled() const override; + not_null action() const override; + +protected: + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + + int contentHeight() const override; + +private: + void paint(Painter &p); + void resizeEvent(QResizeEvent *e) override; + + void resolveMinWidth(); + void refreshText(); + void refreshDimensions(); + + const not_null _parentMenu; + const not_null _dummyAction; + const Fn _showOrPremium; + const style::Menu &_st; + + Text::String _text; + Text::String _show; + QRect _showRect; + int _textWidth = 0; + const int _height = 0; + + WhoReadContent _content; + +}; + TextParseOptions MenuTextOptions = { TextParseLinks, // flags 0, // maxw @@ -219,10 +258,6 @@ Action::Action( if (const auto onstack = _showAllChosen) { onstack(); } - } else if (_content.state == WhoReadState::MyHidden) { - if (const auto onstack = _participantChosen) { - onstack(0); - } } }, lifetime()); @@ -385,11 +420,6 @@ void Action::refreshText() { _st.itemStyle, { ((_content.state == WhoReadState::Unknown) ? tr::lng_context_seen_loading(tr::now) - : (_content.state == WhoReadState::MyHidden) - ? tr::lng_context_read_show(tr::now) - : (_content.state == WhoReadState::HisHidden - || _content.state == WhoReadState::TooOld) - ? tr::lng_context_read_hidden(tr::now) : (usersCount == 1) ? _content.participants.front().name : (_content.fullReactionsCount > 0 @@ -470,6 +500,227 @@ void Action::handleKeyPress(not_null e) { } } +WhenAction::WhenAction( + not_null parentMenu, + rpl::producer content, + Fn showOrPremium) +: ItemBase(parentMenu->menu(), parentMenu->menu()->st()) +, _parentMenu(parentMenu) +, _dummyAction(CreateChild(parentMenu->menu().get())) +, _showOrPremium(std::move(showOrPremium)) +, _st(parentMenu->menu()->st()) +, _height(st::whenReadPadding.top() + + st::whenReadStyle.font->height + + st::whenReadPadding.bottom()) { + const auto parent = parentMenu->menu(); + + setAcceptBoth(true); + initResizeHook(parent->sizeValue()); + + std::move( + content + ) | rpl::start_with_next([=](WhoReadContent &&content) { + const auto changed = (_content.participants != content.participants) + || (_content.state != content.state); + _content = content; + refreshText(); + refreshDimensions(); + setPointerCursor(isEnabled()); + _dummyAction->setEnabled(isEnabled()); + if (!isEnabled()) { + setSelected(false); + } + update(); + }, lifetime()); + + resolveMinWidth(); + refreshDimensions(); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + paint(p); + }, lifetime()); + + clicks( + ) | rpl::start_with_next([=] { + if (_content.state == WhoReadState::MyHidden) { + if (const auto onstack = _showOrPremium) { + onstack(); + } + } + }, lifetime()); + + enableMouseSelecting(); +} + +void WhenAction::resolveMinWidth() { + const auto width = [&](const QString &text) { + return st::whenReadStyle.font->width(text); + }; + const auto added = st::whenReadShowPadding.left() + + st::whenReadShowPadding.right(); + + const auto sampleDate = QDate::currentDate(); + const auto sampleTime = QLocale().toString( + QTime::currentTime(), + QLocale::ShortFormat); + const auto maxTextWidth = added + std::max({ + width(tr::lng_contacts_loading(tr::now)), + (width(tr::lng_context_read_hidden(tr::now)) + + st::whenReadSkip + + width(tr::lng_context_read_show(tr::now))), + width(tr::lng_mediaview_today(tr::now, lt_time, sampleTime)), + width(tr::lng_mediaview_yesterday(tr::now, lt_time, sampleTime)), + width(tr::lng_mediaview_date_time( + tr::now, + lt_date, + tr::lng_month_day( + tr::now, + lt_month, + Lang::MonthDay(sampleDate.month())(tr::now), + lt_day, + QString::number(sampleDate.day())), + lt_time, + sampleTime)), + }); + + const auto maxWidth = st::whenReadPadding.left() + + maxTextWidth + + st::whenReadPadding.right(); + setMinWidth(maxWidth); +} + +void WhenAction::paint(Painter &p) { + const auto loading = !isEnabled() && _content.participants.empty(); + const auto selected = isSelected(); + if (selected && _st.itemBgOver->c.alpha() < 255) { + p.fillRect(0, 0, width(), _height, _st.itemBg); + } + p.fillRect(0, 0, width(), _height, _st.itemBg); + const auto &icon = loading + ? st::whoReadChecksDisabled + : selected + ? st::whoReadChecksOver + : st::whoReadChecks; + icon.paint(p, st::whenReadIconPosition, width()); + p.setPen(loading ? _st.itemFgDisabled : _st.itemFg); + _text.drawLeftElided( + p, + st::whenReadPadding.left(), + st::whenReadPadding.top(), + _textWidth, + width()); + if (!_show.isEmpty()) { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(_st.itemBgOver); + const auto radius = _showRect.height() / 2.; + p.drawRoundedRect(_showRect, radius, radius); + paintRipple(p, 0, 0); + const auto inner = _showRect.marginsRemoved(st::whenReadShowPadding); + p.setPen(_st.itemFgOver); + _show.drawLeftElided( + p, + inner.x(), + inner.y(), + inner.width(), + width()); + } +} + +void WhenAction::refreshText() { + const auto usersCount = int(_content.participants.size()); + const auto onlySeenCount = ranges::count( + _content.participants, + QString(), + &WhoReadParticipant::customEntityData); + const auto count = std::max(_content.fullReactionsCount, usersCount); + _text.setMarkedText( + st::whenReadStyle, + { ((_content.state == WhoReadState::Unknown) + ? tr::lng_context_seen_loading(tr::now) + : _content.participants.empty() + ? tr::lng_context_read_hidden(tr::now) + : _content.participants.front().date) }, + MenuTextOptions); + if (_content.state == WhoReadState::MyHidden) { + _show.setMarkedText( + st::whenReadStyle, + { tr::lng_context_read_show(tr::now) }, + MenuTextOptions); + } else { + _show = Text::String(); + } +} + +void WhenAction::resizeEvent(QResizeEvent *e) { + ItemBase::resizeEvent(e); + refreshDimensions(); +} + +void WhenAction::refreshDimensions() { + if (!minWidth()) { + return; + } + const auto textWidth = _text.maxWidth(); + const auto showWidth = _show.isEmpty() ? 0 : _show.maxWidth(); + const auto &padding = st::whenReadPadding; + + const auto goodWidth = padding.left() + + textWidth + + (showWidth + ? (st::whenReadSkip + + st::whenReadShowPadding.left() + + showWidth + + st::whenReadShowPadding.right()) + : 0) + + padding.right(); + + const auto w = std::clamp( + goodWidth, + _st.widthMin, + std::max(width(), _st.widthMin)); + _textWidth = std::min(w - (goodWidth - textWidth), textWidth); + if (showWidth) { + _showRect = QRect( + padding.left() + _textWidth + st::whenReadSkip, + padding.top() - st::whenReadShowPadding.top(), + (st::whenReadShowPadding.left() + + showWidth + + st::whenReadShowPadding.right()), + (st::whenReadShowPadding.top() + + st::whenReadStyle.font->height + + st::whenReadShowPadding.bottom())); + } +} + +bool WhenAction::isEnabled() const { + return (_content.state == WhoReadState::MyHidden); +} + +not_null WhenAction::action() const { + return _dummyAction; +} + +QPoint WhenAction::prepareRippleStartPosition() const { + const auto result = mapFromGlobal(QCursor::pos()); + return _showRect.contains(result) + ? result + : Ui::RippleButton::DisabledRippleStartPosition(); +} + +QImage WhenAction::prepareRippleMask() const { + return Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) { + const auto radius = _showRect.height() / 2.; + p.drawRoundedRect(_showRect, radius, radius); + }); +} + +int WhenAction::contentHeight() const { + return _height; +} + } // namespace WhoReactedEntryAction::WhoReactedEntryAction( @@ -676,6 +927,16 @@ base::unique_qptr WhoReactedContextAction( std::move(showAllChosen)); } +base::unique_qptr WhenReadContextAction( + not_null menu, + rpl::producer content, + Fn showOrPremium) { + return base::make_unique_q( + menu, + std::move(content), + std::move(showOrPremium)); +} + WhoReactedListMenu::WhoReactedListMenu( CustomEmojiFactory factory, Fn participantChosen, diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index 77a54a4e2..1f35a3216 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -62,6 +62,11 @@ struct WhoReadContent { Fn participantChosen, Fn showAllChosen); +[[nodiscard]] base::unique_qptr WhenReadContextAction( + not_null menu, + rpl::producer content, + Fn showOrPremium); + enum class WhoReactedType : uchar { Viewed, Reacted, diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 4df7b5c6e..53db41bc6 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -235,6 +235,8 @@ PRIVATE ui/boxes/rate_call_box.h ui/boxes/report_box.cpp ui/boxes/report_box.h + ui/boxes/show_or_premium_box.cpp + ui/boxes/show_or_premium_box.h ui/boxes/single_choice_box.cpp ui/boxes/single_choice_box.h ui/boxes/time_picker_box.cpp From 4d86ced1e6419abd74c70b8658f11e1c364a1f13 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 11:02:12 +0400 Subject: [PATCH 006/109] Improve "show" last seen button design. --- Telegram/SourceFiles/info/info.style | 13 +++++++++---- .../SourceFiles/info/profile/info_profile_cover.cpp | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index fbd6157e3..de5e2f9ac 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -338,12 +338,17 @@ infoProfileCover: InfoProfileCover { statusLeft: 109px; statusTop: 58px; showLastSeen: RoundButton(defaultActiveButton) { + textFg: windowSubTextFg; + textFgOver: windowSubTextFg; + textBg: windowBgOver; + textBgOver: windowBgOver; width: -12px; - height: 20px; - textTop: 2px; - font: font(11px semibold); + height: 18px; + textTop: 0px; + font: font(12px); + ripple: defaultRippleAnimation; } - showLastSeenPosition: point(4px, 56px); + showLastSeenPosition: point(3px, 58px); showLastSeenVisible: true; rightSkip: 20px; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index f430c03d7..7968c48d5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -413,6 +413,10 @@ void Cover::setupShowLastSeen() { _showLastSeen->hide(); } + using TextTransform = Ui::RoundButton::TextTransform; + _showLastSeen->setTextTransform(TextTransform::NoTransform); + _showLastSeen->setFullRadius(true); + _showLastSeen->setClickedCallback([=] { const auto type = Ui::ShowOrPremium::LastSeen; auto box = Box(Ui::ShowOrPremiumBox, type, user->shortName(), [=] { From 5cb4cfef3268ef6a37e43217220b9af27dca1f75 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 11:02:27 +0400 Subject: [PATCH 007/109] Improve geometry restore on secondary screen. --- Telegram/build/prepare/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 51cc2c298..38a32a4b1 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -418,7 +418,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout d3107bf4a5 + git checkout 94be868240 """) stage('msys64', """ From 16942d487aec3dd0944ad9bdef79872dab999446 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 12:29:23 +0400 Subject: [PATCH 008/109] New "Messages" privacy option and box. --- Telegram/Resources/langs/lang.strings | 12 ++ .../SourceFiles/boxes/edit_privacy_box.cpp | 155 ++++++++++++++++++ Telegram/SourceFiles/boxes/edit_privacy_box.h | 5 + Telegram/SourceFiles/settings/settings.style | 12 ++ .../settings/settings_privacy_security.cpp | 25 ++- Telegram/SourceFiles/ui/menu_icons.style | 1 + 6 files changed, 207 insertions(+), 3 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 73d0fde25..2e676016e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -644,6 +644,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_phone_number_privacy" = "Phone number"; "lng_settings_forwards_privacy" = "Forwarded messages"; "lng_settings_profile_photo_privacy" = "Profile photo"; +"lng_settings_messages_privacy" = "Messages"; "lng_settings_voices_privacy" = "Voice messages"; "lng_settings_bio_privacy" = "Bio"; "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages."; @@ -1080,6 +1081,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_contacts" = "My contacts"; "lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_nobody" = "Nobody"; +"lng_edit_privacy_premium" = "Premium users"; "lng_edit_privacy_exceptions" = "Add exceptions"; "lng_edit_privacy_exceptions_count#one" = "{count} user"; @@ -1181,6 +1183,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_voices_always_title" = "Always allow"; "lng_edit_privacy_voices_never_title" = "Never allow"; +"lng_messages_privacy_title" = "Messages"; +"lng_messages_privacy_subtitle" = "Who can send me messages?"; +"lng_messages_privacy_everyone" = "Everybody"; +"lng_messages_privacy_restricted" = "My Contacts and Premium Users"; +"lng_messages_privacy_about" = "You can restrict incoming messages to only contacts and Premium users."; +"lng_messages_privacy_premium_button" = "Subscribe to Telegram Premium"; +"lng_messages_privacy_premium_about" = "Subscribe now to change this setting and get access to other exclusive features of Telegram Premium."; +"lng_messages_privacy_premium" = "Only subscribers of {link} can select this option."; +"lng_messages_privacy_premium_link" = "Telegram Premium"; + "lng_self_destruct_title" = "Account self-destruction"; "lng_self_destruct_description" = "If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts."; "lng_self_destruct_sessions_title" = "Session termination"; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 899c8be02..0a8bc866d 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -7,15 +7,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/edit_privacy_box.h" +#include "api/api_global_privacy.h" +#include "ui/layers/generic_box.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/shadow.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" #include "ui/vertical_list.h" #include "history/history.h" #include "boxes/peer_list_controllers.h" +#include "settings/settings_common.h" +#include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "calls/calls_instance.h" #include "base/binary_guard.h" @@ -28,8 +35,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" namespace { +namespace { + +void CreateRadiobuttonLock( + not_null widget, + const style::Checkbox &st) { + const auto lock = Ui::CreateChild(widget.get()); + lock->setAttribute(Qt::WA_TransparentForMouseEvents); + + lock->resize(st::defaultRadio.diameter, st::defaultRadio.diameter); + + widget->sizeValue( + ) | rpl::start_with_next([=, &st](QSize size) { + lock->move(st.checkPosition); + }, lock->lifetime()); + + lock->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(lock); + auto hq = PainterHighQualityEnabler(p); + const auto &icon = st::messagePrivacyLock; + const auto size = st::defaultRadio.diameter; + const auto image = icon.instance(st::checkboxFg->c); + const auto dimensions = image.size(); + p.drawImage(QRectF( + (size - icon.width()) / 2., + (size - icon.height()) / 2., + icon.width(), + icon.height()), image); + }, lock->lifetime()); +} + +} // namespace class PrivacyExceptionsBoxController : public ChatsListBoxController { public: @@ -394,3 +433,119 @@ void EditPrivacyBox::setupContent() { setDimensions(st::boxWideWidth, height); }, content->lifetime()); } + +void EditMessagesPrivacyBox( + not_null box, + not_null controller) { + box->setTitle(tr::lng_messages_privacy_title()); + box->setWidth(st::boxWideWidth); + + constexpr auto kOptionAll = 0; + constexpr auto kOptionPremium = 1; + + const auto premium = controller->session().premium(); + const auto privacy = &controller->session().api().globalPrivacy(); + const auto inner = box->verticalLayout(); + inner->add(object_ptr(box)); + + Ui::AddSkip(inner, st::messagePrivacyTopSkip); + Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle()); + const auto group = std::make_shared( + privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll); + inner->add( + object_ptr( + inner, + group, + kOptionAll, + tr::lng_messages_privacy_everyone(tr::now), + st::messagePrivacyCheck), + st::settingsSendTypePadding); + const auto restricted = inner->add( + object_ptr( + inner, + group, + kOptionPremium, + tr::lng_messages_privacy_restricted(tr::now), + st::messagePrivacyCheck), + st::settingsSendTypePadding + style::margins( + 0, + st::messagePrivacyRadioSkip, + 0, + st::messagePrivacyBottomSkip)); + + using WeakToast = base::weak_ptr; + const auto toast = std::make_shared(); + const auto showToast = [=] { + auto link = Ui::Text::Link( + Ui::Text::Semibold( + tr::lng_messages_privacy_premium_link(tr::now))); + (*toast) = controller->showToast({ + .text = tr::lng_messages_privacy_premium( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .st = &st::defaultMultilineToast, + .duration = Ui::Toast::kDefaultDuration * 2, + .multiline = true, + .filter = crl::guard(&controller->session(), [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = toast->get()) { + strong->hideAnimated(); + (*toast) = nullptr; + Settings::ShowPremium( + controller, + u"noncontact_peers_require_premium"_q); + return true; + } + } + return false; + }), + }); + }; + if (!premium) { + CreateRadiobuttonLock(restricted, st::messagePrivacyCheck); + + group->setChangedCallback([=](int value) { + if (value == kOptionPremium) { + group->setValue(kOptionAll); + showToast(); + } + }); + } + + Ui::AddDividerText(inner, tr::lng_messages_privacy_about()); + if (!premium) { + Ui::AddSkip(inner); + Settings::AddButtonWithIcon( + inner, + tr::lng_messages_privacy_premium_button(), + st::messagePrivacySubscribe, + { .icon = &st::menuBlueIconPremium } + )->setClickedCallback([=] { + Settings::ShowPremium( + controller, + u"noncontact_peers_require_premium"_q); + }); + Ui::AddSkip(inner); + Ui::AddDividerText(inner, tr::lng_messages_privacy_premium_about()); + box->addButton(tr::lng_about_done(), [=] { + box->closeBox(); + }); + } else { + box->addButton(tr::lng_settings_save(), [=] { + if (controller->session().premium()) { + privacy->updateNewRequirePremium( + group->value() == kOptionPremium); + box->closeBox(); + } else { + showToast(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + } +} diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index d9ba7dab9..66cd75948 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_user_privacy.h" namespace Ui { +class GenericBox; class VerticalLayout; class FlatLabel; class LinkButton; @@ -146,3 +147,7 @@ private: Value _value; }; + +void EditMessagesPrivacyBox( + not_null box, + not_null controller); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 616709700..c30940ca8 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -559,3 +559,15 @@ settingsColorButton: SettingsButton(settingsButton) { settingsColorRadioMargin: 17px; settingsColorRadioSkip: 13px; settingsColorRadioStroke: 2px; + +messagePrivacyTopSkip: 8px; +messagePrivacyRadioSkip: 6px; +messagePrivacyBottomSkip: 10px; +messagePrivacyCheck: Checkbox(settingsPrivacyOption) { + textPosition: point(13px, 1px); +} +messagePrivacySubscribe: SettingsButton(settingsButtonLight) { + padding: margins(56px, 10px, 22px, 8px); + iconLeft: 20px; +} +messagePrivacyLock: icon {{ "info/info_rights_lock", checkboxFg }}; diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 8da8dd901..bcd7f46b0 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -195,9 +195,8 @@ void AddPremiumPrivacyButton( const auto showToast = [=] { auto link = Ui::Text::Link( - tr::lng_settings_privacy_premium_link(tr::now)); - link.entities.push_back( - EntityInText(EntityType::Semibold, 0, link.text.size())); + Ui::Text::Semibold( + tr::lng_settings_privacy_premium_link(tr::now))); (*toast) = controller->showToast({ .text = tr::lng_settings_privacy_premium( tr::now, @@ -242,6 +241,25 @@ void AddPremiumPrivacyButton( }); } +void AddMessagesPrivacyButton( + not_null controller, + not_null container) { + const auto session = &controller->session(); + const auto privacy = &session->api().globalPrivacy(); + AddButtonWithLabel( + container, + tr::lng_settings_messages_privacy(), + rpl::conditional( + privacy->newRequirePremium(), + tr::lng_edit_privacy_premium(), + tr::lng_edit_privacy_everyone()), + st::settingsButtonNoIcon, + {} + )->addClickHandler([=] { + controller->show(Box(EditMessagesPrivacyBox, controller)); + }); +} + rpl::producer BlockedPeersCount(not_null<::Main::Session*> session) { return session->api().blockedPeers().slice( ) | rpl::map([](const Api::BlockedPeers::Slice &data) { @@ -284,6 +302,7 @@ void SetupPrivacy( tr::lng_settings_profile_photo_privacy(), Key::ProfilePhoto, [] { return std::make_unique(); }); + AddMessagesPrivacyButton(controller, container); add( tr::lng_settings_bio_privacy(), Key::About, diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 799110173..bf378fb1e 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -157,6 +157,7 @@ menuIconMuteForAnyTextPosition: point(14px, 9px); menuIconMuteForAnyTextFont: font(8px semibold); menuBlueIconPhotoSet: icon {{ "menu/photo_set", lightButtonFg }}; +menuBlueIconPremium: icon{{ "menu/premium", lightButtonFg }}; mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }}; mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }}; From c765bee0cded910f3d117a5c4da7b126b7f80b8c Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 23:20:14 +0400 Subject: [PATCH 009/109] Update API scheme on layer 172. --- Telegram/SourceFiles/mtproto/scheme/api.tl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 74f938682..67e24c28d 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -82,7 +82,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.6?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; +user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -1781,7 +1781,7 @@ account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; -//users.getIsPremiumRequiredToContact#75c9db9c id:Vector = Vector; +users.getIsPremiumRequiredToContact#a622aa10 id:Vector = Vector; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; From e5cda0e2b105d66433ab20ef155c0ef021465d30 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jan 2024 23:20:36 +0400 Subject: [PATCH 010/109] Show "premium required" information in chat. --- Telegram/CMakeLists.txt | 2 + .../Resources/icons/chat/large_lockedchat.png | Bin 0 -> 1219 bytes .../icons/chat/large_lockedchat@2x.png | Bin 0 -> 2328 bytes .../icons/chat/large_lockedchat@3x.png | Bin 0 -> 3552 bytes Telegram/Resources/langs/lang.strings | 10 + .../chat_helpers/message_field.cpp | 66 +++++ .../SourceFiles/chat_helpers/message_field.h | 7 + .../data/data_chat_participant_status.cpp | 11 + Telegram/SourceFiles/data/data_peer.cpp | 3 + .../SourceFiles/data/data_peer_values.cpp | 21 +- Telegram/SourceFiles/data/data_session.cpp | 3 +- Telegram/SourceFiles/data/data_types.h | 4 +- Telegram/SourceFiles/data/data_user.cpp | 2 +- .../history/history_inner_widget.cpp | 190 ++++--------- .../history/history_inner_widget.h | 6 +- Telegram/SourceFiles/history/history_item.h | 4 +- .../SourceFiles/history/history_widget.cpp | 99 ++++--- Telegram/SourceFiles/history/history_widget.h | 9 +- .../history/view/history_view_about_view.cpp | 249 ++++++++++++++++++ .../history/view/history_view_about_view.h | 40 +++ .../history/view/history_view_element.cpp | 8 +- .../history/view/history_view_message.cpp | 12 +- .../history/view/media/history_view_gif.cpp | 2 +- .../history/view/media/history_view_photo.cpp | 2 +- .../view/media/history_view_service_box.cpp | 8 +- .../view/media/history_view_service_box.h | 1 + Telegram/SourceFiles/ui/chat/chat.style | 7 + 27 files changed, 566 insertions(+), 200 deletions(-) create mode 100644 Telegram/Resources/icons/chat/large_lockedchat.png create mode 100644 Telegram/Resources/icons/chat/large_lockedchat@2x.png create mode 100644 Telegram/Resources/icons/chat/large_lockedchat@3x.png create mode 100644 Telegram/SourceFiles/history/view/history_view_about_view.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_about_view.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index b1fc1570d..b7ecc7c8a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -752,6 +752,8 @@ PRIVATE history/view/reactions/history_view_reactions_strip.h history/view/reactions/history_view_reactions_tabs.cpp history/view/reactions/history_view_reactions_tabs.h + history/view/history_view_about_view.cpp + history/view/history_view_about_view.h history/view/history_view_bottom_info.cpp history/view/history_view_bottom_info.h history/view/history_view_contact_status.cpp diff --git a/Telegram/Resources/icons/chat/large_lockedchat.png b/Telegram/Resources/icons/chat/large_lockedchat.png new file mode 100644 index 0000000000000000000000000000000000000000..812b705b30dc0255fee607734f1b50cc8ed2879f GIT binary patch literal 1219 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2e676016e..5b2f2978d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3612,6 +3612,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_restricted_send_voice_messages" = "{user} restricted sending of voice messages to them."; "lng_restricted_send_video_messages" = "{user} restricted sending of video messages to them."; +"lng_restricted_send_non_premium" = "Only Premium users can message {user}."; +"lng_restricted_send_non_premium_more" = "Learn more..."; + +"lng_send_non_premium_text" = "Subscribe to **Premium**\n to message {user}."; +"lng_send_non_premium_go" = "Go Premium"; +"lng_send_non_premium_story" = "Replies restricted"; +"lng_send_non_premium_unlock" = "Unlock"; +"lng_send_non_premium_story_toast" = "You need a **Premium** subscription to reply to **{user}'s** stories."; +"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and **Premium** users."; +"lng_send_non_premium_toast_button" = "View"; "lng_exceptions_list_title" = "Exceptions"; "lng_removed_list_title" = "Removed users"; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 475e87cb3..ee3418394 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwindow.h" #include "main/main_session.h" +#include "settings/settings_premium.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" @@ -904,3 +905,68 @@ base::unique_qptr CreateDisabledFieldView( }); return result; } + +base::unique_qptr TextErrorSendRestriction( + QWidget *parent, + const QString &text) { + auto result = base::make_unique_q(parent); + const auto raw = result.get(); + const auto label = CreateChild( + result.get(), + text, + st::historySendPremiumRequired); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::windowBg); + }, raw->lifetime()); + raw->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto &st = st::historyComposeField; + const auto width = size.width(); + const auto margins = (st.textMargins + st.placeholderMargins); + const auto available = width - margins.left() - margins.right(); + label->resizeToWidth(available); + label->moveToLeft( + margins.left(), + (size.height() - label->height()) / 2, + width); + }, label->lifetime()); + return result; +} + +base::unique_qptr PremiumRequiredSendRestriction( + QWidget *parent, + not_null user, + not_null controller) { + auto result = base::make_unique_q(parent); + const auto raw = result.get(); + const auto label = CreateChild( + result.get(), + tr::lng_restricted_send_non_premium( + tr::now, + lt_user, + user->shortName()), + st::historySendPremiumRequired); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto link = CreateChild( + result.get(), + tr::lng_restricted_send_non_premium_more(tr::now)); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::windowBg); + }, raw->lifetime()); + raw->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &st = st::historyComposeField; + const auto margins = (st.textMargins + st.placeholderMargins); + const auto available = width - margins.left() - margins.right(); + label->resizeToWidth(available); + label->moveToLeft(margins.left(), margins.top(), width); + link->move( + (width - link->width()) / 2, + label->y() + label->height()); + }, label->lifetime()); + link->setClickedCallback([=] { + Settings::ShowPremium(controller, u"require_premium"_q); + }); + return result; +} diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index a78abd94c..7bce7facf 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -143,3 +143,10 @@ private: [[nodiscard]] base::unique_qptr CreateDisabledFieldView( QWidget *parent, not_null peer); +[[nodiscard]] base::unique_qptr TextErrorSendRestriction( + QWidget *parent, + const QString &text); +[[nodiscard]] base::unique_qptr PremiumRequiredSendRestriction( + QWidget *parent, + not_null user, + not_null controller); diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index f875ccdb3..6644254d5 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/chat/attach/attach_prepare.h" namespace { @@ -113,6 +114,9 @@ bool CanSendAnyOf( if (const auto user = peer->asUser()) { if (user->isInaccessible() || user->isRepliesChat()) { return false; + } else if (user->meRequiresPremiumToWrite() + && !user->session().premium()) { + return false; } else if (rights & ~(ChatRestriction::SendVoiceMessages | ChatRestriction::SendVideoMessages @@ -167,6 +171,13 @@ std::optional RestrictionError( using Flag = ChatRestriction; if (const auto restricted = peer->amRestricted(restriction)) { if (const auto user = peer->asUser()) { + if (user->meRequiresPremiumToWrite() + && !user->session().premium()) { + return tr::lng_restricted_send_non_premium( + tr::now, + lt_user, + user->shortName()); + } const auto result = (restriction == Flag::SendVoiceMessages) ? tr::lng_restricted_send_voice_messages( tr::now, diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index a6f033924..1a0abf134 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1101,6 +1101,9 @@ Data::RestrictionCheckResult PeerData::amRestricted( } }; if (const auto user = asUser()) { + if (user->meRequiresPremiumToWrite() && !user->session().premium()) { + return Result::Explicit(); + } return (right == ChatRestriction::SendVoiceMessages || right == ChatRestriction::SendVideoMessages) ? ((user->flags() & UserDataFlag::VoiceMessagesForbidden) diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index c6458fc1d..7898f414f 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -217,13 +217,24 @@ inline auto DefaultRestrictionValue( using namespace rpl::mappers; const auto other = rights & ~(ChatRestriction::SendVoiceMessages | ChatRestriction::SendVideoMessages); + auto allowedAny = PeerFlagsValue( + user, + (UserDataFlag::Deleted | UserDataFlag::MeRequiresPremiumToWrite) + ) | rpl::map([=](UserDataFlags flags) { + return (flags & UserDataFlag::Deleted) + ? rpl::single(false) + : !(flags & UserDataFlag::MeRequiresPremiumToWrite) + ? rpl::single(true) + : AmPremiumValue(&user->session()); + }) | rpl::flatten_latest(); if (other) { - return PeerFlagValue(user, UserDataFlag::Deleted) - | rpl::map(!_1); + return std::move(allowedAny); } - const auto mask = UserDataFlag::Deleted - | UserDataFlag::VoiceMessagesForbidden; - return PeerFlagsValue(user, mask) | rpl::map(!_1); + const auto mask = UserDataFlag::VoiceMessagesForbidden; + return rpl::combine( + std::move(allowedAny), + PeerFlagValue(user, mask), + _1 && !_2); } else if (const auto chat = peer->asChat()) { const auto mask = ChatDataFlag() | ChatDataFlag::Deactivated diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 8d235bc44..9ec603386 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -520,6 +520,7 @@ not_null Session::processUser(const MTPUser &data) { | Flag::Premium | Flag::Support | Flag::SomeRequirePremiumToWrite + | Flag::MeRequiresPremiumToWrite AssertIsDebug() | Flag::RequirePremiumToWriteKnown | (!minimal ? Flag::Contact @@ -542,7 +543,7 @@ not_null Session::processUser(const MTPUser &data) { | (data.is_premium() ? Flag::Premium : Flag()) | (data.is_support() ? Flag::Support : Flag()) | (data.is_contact_require_premium() - ? (Flag::SomeRequirePremiumToWrite + ? ((Flag::SomeRequirePremiumToWrite | Flag::MeRequiresPremiumToWrite) AssertIsDebug() | (result->someRequirePremiumToWrite() ? (result->requirePremiumToWriteKnown() ? Flag::RequirePremiumToWriteKnown diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 46f75419a..3b511cba0 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -300,8 +300,8 @@ enum class MessageFlag : uint64 { OnlyEmojiAndSpaces = (1ULL << 35), OnlyEmojiAndSpacesSet = (1ULL << 36), - // Fake message with bot cover and information. - FakeBotAbout = (1ULL << 37), + // Fake message with some info, like bot cover and information. + FakeAboutView = (1ULL << 37), StoryItem = (1ULL << 38), diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index b455fba66..4c0d4ce5a 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -506,7 +506,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { | Flag::VoiceMessagesForbidden | Flag::ReadDatesPrivate | Flag::RequirePremiumToWriteKnown - | Flag::MeRequiresPremiumToWrite; + /*| Flag::MeRequiresPremiumToWrite*/; AssertIsDebug() user->setFlags((user->flags() & ~mask) | (update.is_phone_calls_private() ? Flag::PhoneCallsPrivate diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 17fb3257b..a50bce0ee 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/file_utilities.h" #include "core/click_handler_types.h" -#include "history/admin_log/history_admin_log_item.h" #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" @@ -17,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions_selector.h" +#include "history/view/history_view_about_view.h" #include "history/view/history_view_message.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" @@ -305,102 +305,6 @@ public: }; -class HistoryInner::BotAbout final : public ClickHandlerHost { -public: - BotAbout( - not_null history, - not_null delegate); - - [[nodiscard]] not_null history() const; - [[nodiscard]] HistoryView::Element *view() const; - [[nodiscard]] HistoryItem *item() const; - - bool refresh(); - - int top = 0; - int height = 0; - -private: - const not_null _history; - const not_null _delegate; - AdminLog::OwnedItem _item; - int _version = 0; - -}; - -HistoryInner::BotAbout::BotAbout( - not_null history, - not_null delegate) -: _history(history) -, _delegate(delegate) { -} - -not_null HistoryInner::BotAbout::history() const { - return _history; -} - -HistoryView::Element *HistoryInner::BotAbout::view() const { - return _item.get(); -} - -HistoryItem *HistoryInner::BotAbout::item() const { - if (const auto element = view()) { - return element->data(); - } - return nullptr; -} - -bool HistoryInner::BotAbout::refresh() { - const auto bot = _history->peer->asUser(); - const auto info = bot ? bot->botInfo.get() : nullptr; - if (!info) { - if (_item) { - _item = {}; - return true; - } - _version = 0; - return false; - } - const auto version = info->descriptionVersion; - if (_version == version) { - return false; - } - _version = version; - - const auto flags = MessageFlag::FakeBotAbout - | MessageFlag::FakeHistoryItem - | MessageFlag::Local; - const auto postAuthor = QString(); - const auto date = TimeId(0); - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(0); - const auto groupedId = uint64(0); - const auto textWithEntities = TextUtilities::ParseEntities( - info->description, - Ui::ItemTextBotNoMonoOptions().flags); - const auto make = [&](auto &&a, auto &&b, auto &&...other) { - return _history->makeMessage( - _history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - date, - bot->id, - postAuthor, - std::forward(a), - std::forward(b), - HistoryMessageMarkupData(), - std::forward(other)...); - }; - const auto item = info->document - ? make(info->document, textWithEntities) - : info->photo - ? make(info->photo, textWithEntities) - : make(textWithEntities, MTP_messageMediaEmpty(), groupedId); - _item = AdminLog::OwnedItem(_delegate, item); - return true; -} - HistoryInner::HistoryInner( not_null historyWidget, not_null scroll, @@ -447,7 +351,7 @@ HistoryInner::HistoryInner( setAttribute(Qt::WA_AcceptTouchEvents); - notifyIsBotChanged(); + refreshAboutView(); setMouseTracking(true); _controller->gifPauseLevelChanged( @@ -1011,10 +915,10 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto historyDisplayedEmpty = _history->isDisplayedEmpty() && (!_migrated || _migrated->isDisplayedEmpty()); - if (const auto view = _botAbout ? _botAbout->view() : nullptr) { - if (clip.y() < _botAbout->top + _botAbout->height - && clip.y() + clip.height() > _botAbout->top) { - const auto top = _botAbout->top; + if (const auto view = _aboutView ? _aboutView->view() : nullptr) { + if (clip.y() < _aboutView->top + _aboutView->height + && clip.y() + clip.height() > _aboutView->top) { + const auto top = _aboutView->top; context.translate(0, -top); context.selection = computeRenderSelection(&_selected, view); p.translate(0, top); @@ -3066,8 +2970,8 @@ void HistoryInner::recountHistoryGeometry() { auto oldHistoryPaddingTop = qMax( visibleHeight - historyHeight() - st::historyPaddingBottom, 0); - if (_botAbout) { - accumulate_max(oldHistoryPaddingTop, _botAbout->height); + if (_aboutView) { + accumulate_max(oldHistoryPaddingTop, _aboutView->height); } updateBotInfo(false); @@ -3095,20 +2999,20 @@ void HistoryInner::recountHistoryGeometry() { } } - if (const auto view = _botAbout ? _botAbout->view() : nullptr) { - _botAbout->height = view->resizeGetHeight(_contentWidth); - _botAbout->top = qMin( - _historyPaddingTop - _botAbout->height, - qMax(0, (_scroll->height() - _botAbout->height) / 2)); - } else if (_botAbout) { - _botAbout->top = _botAbout->height = 0; + if (const auto view = _aboutView ? _aboutView->view() : nullptr) { + _aboutView->height = view->resizeGetHeight(_contentWidth); + _aboutView->top = qMin( + _historyPaddingTop - _aboutView->height, + qMax(0, (_scroll->height() - _aboutView->height) / 2)); + } else if (_aboutView) { + _aboutView->top = _aboutView->height = 0; } auto newHistoryPaddingTop = qMax( visibleHeight - historyHeight() - st::historyPaddingBottom, 0); - if (_botAbout) { - accumulate_max(newHistoryPaddingTop, _botAbout->height); + if (_aboutView) { + accumulate_max(newHistoryPaddingTop, _aboutView->height); } auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop); @@ -3122,13 +3026,13 @@ void HistoryInner::recountHistoryGeometry() { } void HistoryInner::updateBotInfo(bool recount) { - if (!_botAbout) { + if (!_aboutView) { return; - } else if (_botAbout->refresh() && recount && _contentWidth > 0) { - const auto view = _botAbout->view(); + } else if (_aboutView->refresh() && recount && _contentWidth > 0) { + const auto view = _aboutView->view(); const auto now = view ? view->resizeGetHeight(_contentWidth) : 0; - if (_botAbout->height != now) { - _botAbout->height = now; + if (_aboutView->height != now) { + _aboutView->height = now; updateSize(); } } @@ -3276,14 +3180,14 @@ void HistoryInner::updateSize() { const auto visibleHeight = _scroll->height(); const auto itemsHeight = historyHeight() - _revealHeight; auto newHistoryPaddingTop = qMax(visibleHeight - itemsHeight - st::historyPaddingBottom, 0); - if (_botAbout) { - accumulate_max(newHistoryPaddingTop, _botAbout->height); + if (_aboutView) { + accumulate_max(newHistoryPaddingTop, _aboutView->height); } - if (_botAbout && _botAbout->height > 0) { - _botAbout->top = qMin( - newHistoryPaddingTop - _botAbout->height, - qMax(0, (_scroll->height() - _botAbout->height) / 2)); + if (_aboutView && _aboutView->height > 0) { + _aboutView->top = qMin( + newHistoryPaddingTop - _aboutView->height, + qMax(0, (_scroll->height() - _aboutView->height) / 2)); } if (_historyPaddingTop != newHistoryPaddingTop) { @@ -3327,7 +3231,7 @@ void HistoryInner::leaveEventHook(QEvent *e) { } HistoryInner::~HistoryInner() { - _botAbout = nullptr; + _aboutView = nullptr; for (const auto &item : _animatedStickersPlayed) { if (const auto view = item->mainView()) { if (const auto media = view->media()) { @@ -3641,11 +3545,11 @@ void HistoryInner::mouseActionUpdate() { const auto reactionView = viewByItem(reactionItem); const auto view = reactionView ? reactionView - : (_botAbout - && _botAbout->view() - && point.y() >= _botAbout->top - && point.y() < _botAbout->top + _botAbout->view()->height()) - ? _botAbout->view() + : (_aboutView + && _aboutView->view() + && point.y() >= _aboutView->top + && point.y() < _aboutView->top + _aboutView->view()->height()) + ? _aboutView->view() : (_curHistory && !_curHistory->isEmpty()) ? _curHistory->blocks[_curBlock]->messages[_curItem].get() : nullptr; @@ -4004,8 +3908,8 @@ void HistoryInner::clearChooseReportReason() { auto HistoryInner::viewByItem(const HistoryItem *item) const -> Element* { return !item ? nullptr - : (_botAbout && _botAbout->item() == item) - ? _botAbout->view() + : (_aboutView && _aboutView->item() == item) + ? _aboutView->view() : item->mainView(); } @@ -4017,8 +3921,8 @@ int HistoryInner::itemTop(const HistoryItem *item) const { int HistoryInner::itemTop(const Element *view) const { if (!view) { return -1; - } else if (_botAbout && view == _botAbout->view()) { - return _botAbout->top; + } else if (_aboutView && view == _aboutView->view()) { + return _aboutView->top; } else if (view->data()->mainView() != view) { return -1; } @@ -4056,17 +3960,27 @@ auto HistoryInner::findViewForPinnedTracking(int top) const return { nullptr, 0 }; } -void HistoryInner::notifyIsBotChanged() { +void HistoryInner::refreshAboutView() { if (const auto user = _peer->asUser()) { if (const auto info = user->botInfo.get()) { - if (!_botAbout) { - _botAbout = std::make_unique( + if (!_aboutView) { + _aboutView = std::make_unique( _history, _history->delegateMixin()->delegate()); } if (!info->inited) { session().api().requestFullPeer(_peer); } + } else if (user->meRequiresPremiumToWrite() + && !user->session().premium() + && !historyHeight()) { + if (!_aboutView) { + _aboutView = std::make_unique( + _history, + _history->delegateMixin()->delegate()); + } + } else { + _aboutView = nullptr; } } } @@ -4313,7 +4227,7 @@ void HistoryInner::applyDragSelection( if (!toItems->empty() && toItems->cbegin()->second != FullSelection) { toItems->clear(); } - const auto botAboutView = _botAbout ? _botAbout->view() : nullptr; + const auto botAboutView = _aboutView ? _aboutView->view() : nullptr; if (_dragSelecting) { auto fromblock = (_dragSelFrom != botAboutView) ? _dragSelFrom->block()->indexInHistory() diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index dcc8dc4b5..ce88f93d7 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -36,6 +36,7 @@ class Element; class TranslateTracker; struct PinnedId; struct SelectedQuote; +class AboutView; } // namespace HistoryView namespace HistoryView::Reactions { @@ -192,7 +193,7 @@ public: [[nodiscard]] std::pair findViewForPinnedTracking( int top) const; - void notifyIsBotChanged(); + void refreshAboutView(); void notifyMigrateUpdated(); // Ui::AbstractTooltipShower interface. @@ -234,7 +235,6 @@ private: void onTouchSelect(); void onTouchScrollTimer(); - class BotAbout; using ChosenReaction = HistoryView::Reactions::ChosenReaction; using VideoUserpic = Dialogs::Ui::VideoUserpic; using SelectedItems = std::map>; @@ -449,7 +449,7 @@ private: // the first _history message date (just skip it by height). int _historySkipHeight = 0; - std::unique_ptr _botAbout; + std::unique_ptr _aboutView; std::unique_ptr _emptyPainter; std::unique_ptr _translateTracker; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 75705eab8..93f1b44d1 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -314,8 +314,8 @@ public: [[nodiscard]] bool isLocal() const { return _flags & MessageFlag::Local; } - [[nodiscard]] bool isFakeBotAbout() const { - return _flags & MessageFlag::FakeBotAbout; + [[nodiscard]] bool isFakeAboutView() const { + return _flags & MessageFlag::FakeAboutView; } [[nodiscard]] bool showSimilarChannels() const { return _flags & MessageFlag::ShowSimilarChannels; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 1d518a32e..1832057a9 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -577,7 +577,7 @@ HistoryWidget::HistoryWidget( ) | rpl::filter([=](not_null user) { return (_peer == user.get()); }) | rpl::start_with_next([=](not_null user) { - _list->notifyIsBotChanged(); + _list->refreshAboutView(); _list->updateBotInfo(); updateControlsVisibility(); updateControlsGeometry(); @@ -692,6 +692,17 @@ HistoryWidget::HistoryWidget( scrollToCurrentVoiceMessage(pair.from.contextId(), pair.to); }, lifetime()); + session().user()->flagsValue( + ) | rpl::start_with_next([=](UserData::Flags::Change change) { + if (change.diff & UserData::Flag::Premium) { + if (const auto user = _peer ? _peer->asUser() : nullptr) { + if (user->meRequiresPremiumToWrite()) { + handlePeerUpdate(); + } + } + } + }, lifetime()); + using PeerUpdateFlag = Data::PeerUpdate::Flag; session().changes().peerUpdates( PeerUpdateFlag::Rights @@ -2728,18 +2739,6 @@ bool HistoryWidget::canWriteMessage() const { return true; } -std::optional HistoryWidget::writeRestriction() const { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - auto result = (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls)) - ? Data::RestrictionError(_peer, ChatRestriction::SendOther) - : std::nullopt; - if (result) { - return result; - } - return std::nullopt; -} - void HistoryWidget::updateControlsVisibility() { auto fieldDisabledRemoved = (_fieldDisabled != nullptr); const auto guard = gsl::finally([&] { @@ -2861,6 +2860,9 @@ void HistoryWidget::updateControlsVisibility() { if (_inlineResults) { _inlineResults->hide(); } + if (_sendRestriction) { + _sendRestriction->hide(); + } hideFieldIfVisible(); } else if (editingMessage() || _canSendMessages) { checkFieldAutocomplete(); @@ -2918,6 +2920,9 @@ void HistoryWidget::updateControlsVisibility() { if (_botMenuButton) { _botMenuButton->show(); } + if (_sendRestriction) { + _sendRestriction->hide(); + } { auto rightButtonsChanged = false; if (_silent) { @@ -3019,6 +3024,9 @@ void HistoryWidget::updateControlsVisibility() { if (_inlineResults) { _inlineResults->hide(); } + if (_sendRestriction) { + _sendRestriction->show(); + } _kbScroll->hide(); hideFieldIfVisible(); } @@ -5131,6 +5139,9 @@ void HistoryWidget::moveFieldControls() { _joinChannel->setGeometry(fullWidthButtonRect); _muteUnmute->setGeometry(fullWidthButtonRect); _reportMessages->setGeometry(fullWidthButtonRect); + if (_sendRestriction) { + _sendRestriction->setGeometry(fullWidthButtonRect); + } } void HistoryWidget::updateFieldSize() { @@ -5163,7 +5174,7 @@ void HistoryWidget::updateFieldSize() { } if (_fieldDisabled) { - _fieldDisabled->resize(fieldWidth, fieldHeight()); + _fieldDisabled->resize(width(), st::historySendSize.height()); } if (_field->width() != fieldWidth) { _field->resize(fieldWidth, _field->height()); @@ -5849,6 +5860,46 @@ int HistoryWidget::countAutomaticScrollTop() { return ScrollMax; } +QString HistoryWidget::computeSendRestriction() const { + if (const auto user = _peer ? _peer->asUser() : nullptr) { + if (user->meRequiresPremiumToWrite() + && !user->session().premium()) { + return u"premium_required"_q; + } + } + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto error = (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls)) + ? Data::RestrictionError(_peer, ChatRestriction::SendOther) + : std::nullopt; + return error ? (u"restriction:"_q + *error) : QString(); +} + +void HistoryWidget::updateSendRestriction() { + const auto restriction = computeSendRestriction(); + if (_sendRestrictionKey == restriction) { + return; + } + _sendRestrictionKey = restriction; + if (restriction.isEmpty()) { + _sendRestriction = nullptr; + } else if (restriction == u"premium_required"_q) { + _sendRestriction = PremiumRequiredSendRestriction( + this, + _peer->asUser(), + controller()); + } else if (restriction.startsWith(u"restriction:"_q)) { + const auto error = restriction.mid(12); + _sendRestriction = TextErrorSendRestriction(this, error); + } else { + Unexpected("Restriction type."); + } + if (_sendRestriction) { + _sendRestriction->show(); + moveFieldControls(); + } +} + void HistoryWidget::updateHistoryGeometry( bool initial, bool loadedDown, @@ -5893,8 +5944,8 @@ void HistoryWidget::updateHistoryGeometry( } else { if (editingMessage() || _canSendMessages) { newScrollHeight -= (fieldHeight() + 2 * st::historySendPadding); - } else if (writeRestriction().has_value()) { - newScrollHeight -= _unblock->height(); + } else if (_sendRestriction) { + newScrollHeight -= _sendRestriction->height(); } if (_editMsgId || replyTo() @@ -7605,6 +7656,7 @@ void HistoryWidget::fullInfoUpdated() { void HistoryWidget::handlePeerUpdate() { bool resize = false; + updateSendRestriction(); updateHistoryGeometry(); if (_peer->isChat() && _peer->asChat()->noParticipantInfo()) { session().api().requestFullPeer(_peer); @@ -8077,15 +8129,6 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } } -void HistoryWidget::drawRestrictedWrite(Painter &p, const QString &error) { - auto rect = myrtlrect(0, height() - _unblock->height(), width(), _unblock->height()); - p.fillRect(rect, st::historyReplyBg); - - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText(rect.marginsRemoved(QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), error, style::al_center); -} - void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int top) const { if (!rect.intersects(myrtlrect(left, top, width() - left, st::normalFont->height))) { return; @@ -8165,12 +8208,6 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { || _kbShown) { drawField(p, clip); } - const auto error = restrictionHidden - ? std::nullopt - : writeRestriction(); - if (error) { - drawRestrictedWrite(p, *error); - } } else { const auto w = st::msgServiceFont->width(tr::lng_willbe_history(tr::now)) + st::msgPadding.left() diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 34f101d76..6ec45fba4 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -474,7 +474,6 @@ private: [[nodiscard]] MsgId resolveReplyToTopicRootId(); [[nodiscard]] Data::ForumTopic *resolveReplyToTopic(); [[nodiscard]] bool canWriteMessage() const; - std::optional writeRestriction() const; void orderWidgets(); [[nodiscard]] InlineBotQuery parseInlineBotQuery() const; @@ -503,11 +502,8 @@ private: bool editingMessage() const { return _editMsgId != 0; } - bool jumpToDialogRow(const Dialogs::RowDescriptor &to); void setupShortcuts(); - bool showNextChat(); - bool showPreviousChat(); void handlePeerMigration(); @@ -536,7 +532,6 @@ private: const QRect &rect, int left, int top) const; - void drawRestrictedWrite(Painter &p, const QString &error); bool paintShowAnimationFrame(); void updateMouseTracking(); @@ -559,6 +554,8 @@ private: void addMessagesToFront(not_null peer, const QVector &messages); void addMessagesToBack(not_null peer, const QVector &messages); + void updateSendRestriction(); + [[nodiscard]] QString computeSendRestriction() const; void updateHistoryGeometry(bool initial = false, bool loadedDown = false, const ScrollChange &change = { ScrollChangeNone, 0 }); void updateListSize(); void startItemRevealAnimations(); @@ -765,6 +762,8 @@ private: bool _cmdStartShown = false; object_ptr _field; base::unique_qptr _fieldDisabled; + base::unique_qptr _sendRestriction; + QString _sendRestrictionKey; Ui::Animations::Simple _inPhotoEditOver; bool _inDetails = false; bool _inPhotoEdit = false; diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp new file mode 100644 index 000000000..70fd4a6f5 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -0,0 +1,249 @@ +/* +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 "history/view/history_view_about_view.h" + +#include "core/click_handler_types.h" +#include "data/data_user.h" +#include "history/view/media/history_view_service_box.h" +#include "history/view/media/history_view_sticker_player_abstract.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/history_item_reply_markup.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_premium.h" +#include "ui/chat/chat_style.h" +#include "ui/text/text_utilities.h" +#include "ui/text/text_options.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +class PremiumRequiredBox final : public ServiceBoxContent { +public: + explicit PremiumRequiredBox(not_null parent); + ~PremiumRequiredBox(); + + int width() override; + int top() override; + QSize size() override; + QString title() override; + TextWithEntities subtitle() override; + int buttonSkip() override; + rpl::producer button() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override { + return true; + } + + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + const not_null _parent; + +}; + +PremiumRequiredBox::PremiumRequiredBox(not_null parent) +: _parent(parent) { +} + +PremiumRequiredBox::~PremiumRequiredBox() = default; + +int PremiumRequiredBox::width() { + return st::premiumRequiredWidth; +} + +int PremiumRequiredBox::top() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +QSize PremiumRequiredBox::size() { + return { st::msgServicePhotoWidth, st::msgServicePhotoWidth }; +} + +QString PremiumRequiredBox::title() { + return QString(); +} + +int PremiumRequiredBox::buttonSkip() { + return st::storyMentionButtonSkip; +} + +rpl::producer PremiumRequiredBox::button() { + return tr::lng_send_non_premium_go(); +} + +TextWithEntities PremiumRequiredBox::subtitle() { + return _parent->data()->notificationText(); +} + +ClickHandlerPtr PremiumRequiredBox::createViewLink() { + const auto itemId = _parent->data()->fullId(); + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + Settings::ShowPremium(controller, u"require_premium"_q); + } + }); +} + +void PremiumRequiredBox::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + const auto padding = (geometry.width() - st::premiumRequiredCircle) / 2; + const auto size = geometry.width() - 2 * padding; + p.setBrush(context.st->msgServiceBg()); // ? + p.setPen(Qt::NoPen); + p.drawEllipse(geometry); + st::premiumRequiredIcon.paintInCenter(p, geometry); +} + +void PremiumRequiredBox::stickerClearLoopPlayed() { +} + +std::unique_ptr PremiumRequiredBox::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return nullptr; +} + +bool PremiumRequiredBox::hasHeavyPart() { + return false; +} + +void PremiumRequiredBox::unloadHeavyPart() { +} + +} // namespace + +AboutView::AboutView( + not_null history, + not_null delegate) +: _history(history) +, _delegate(delegate) { +} + +not_null AboutView::history() const { + return _history; +} + +Element *AboutView::view() const { + return _item.get(); +} + +HistoryItem *AboutView::item() const { + if (const auto element = view()) { + return element->data(); + } + return nullptr; +} + +bool AboutView::refresh() { + const auto bot = _history->peer->asUser(); + const auto info = bot ? bot->botInfo.get() : nullptr; + if (!info) { + if (bot + && bot->meRequiresPremiumToWrite() + && !bot->session().premium() + && _history->isEmpty()) { + if (_item) { + return false; + } + _item = makePremiumRequired(); + return true; + } + if (_item) { + _item = {}; + return true; + } + _version = 0; + return false; + } + const auto version = info->descriptionVersion; + if (_version == version) { + return false; + } + _version = version; + _item = makeAboutBot(info); + return true; +} + +AdminLog::OwnedItem AboutView::makeAboutBot(not_null info) { + const auto flags = MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local; + const auto postAuthor = QString(); + const auto date = TimeId(0); + const auto replyTo = FullReplyTo(); + const auto viaBotId = UserId(0); + const auto groupedId = uint64(0); + const auto textWithEntities = TextUtilities::ParseEntities( + info->description, + Ui::ItemTextBotNoMonoOptions().flags); + const auto make = [&](auto &&a, auto &&b, auto &&...other) { + return _history->makeMessage( + _history->nextNonHistoryEntryId(), + flags, + replyTo, + viaBotId, + date, + _history->peer->id, + postAuthor, + std::forward(a), + std::forward(b), + HistoryMessageMarkupData(), + std::forward(other)...); + }; + const auto item = info->document + ? make(info->document, textWithEntities) + : info->photo + ? make(info->photo, textWithEntities) + : make(textWithEntities, MTP_messageMediaEmpty(), groupedId); + return AdminLog::OwnedItem(_delegate, item); +} + +AdminLog::OwnedItem AboutView::makePremiumRequired() { + const auto flags = MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local; + const auto date = TimeId(0); + const auto item = _history->makeMessage( + _history->nextNonHistoryEntryId(), + flags, + date, + PreparedServiceText{ tr::lng_send_non_premium_text( + tr::now, + lt_user, + Ui::Text::Bold(_history->peer->shortName()), + Ui::Text::RichLangValue) }, + peerToUser(_history->peer->id)); + auto result = AdminLog::OwnedItem(_delegate, item); + result->overrideMedia(std::make_unique( + result.get(), + std::make_unique(result.get()))); + return result; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h new file mode 100644 index 000000000..bf162f451 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -0,0 +1,40 @@ +/* +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 +*/ +#pragma once + +#include "history/admin_log/history_admin_log_item.h" + +namespace HistoryView { + +class AboutView final : public ClickHandlerHost { +public: + AboutView( + not_null history, + not_null delegate); + + [[nodiscard]] not_null history() const; + [[nodiscard]] Element *view() const; + [[nodiscard]] HistoryItem *item() const; + + bool refresh(); + + int top = 0; + int height = 0; + +private: + [[nodiscard]] AdminLog::OwnedItem makeAboutBot(not_null info); + [[nodiscard]] AdminLog::OwnedItem makePremiumRequired(); + + const not_null _history; + const not_null _delegate; + AdminLog::OwnedItem _item; + int _version = 0; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index d2c56ca8e..60f0b0fe3 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_sponsored_messages.h" #include "data/data_message_reactions.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" @@ -472,8 +473,11 @@ Element::Element( if (_context == Context::History) { history()->setHasPendingResizedItems(); } - if (data->isFakeBotAbout() && !data->history()->peer->isRepliesChat()) { - AddComponents(FakeBotAboutTop::Bit()); + if (data->isFakeAboutView()) { + const auto user = data->history()->peer->asUser(); + if (user && user->isBot() && !user->isRepliesChat()) { + AddComponents(FakeBotAboutTop::Bit()); + } } } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 93db9cd12..04c2977e3 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -625,7 +625,7 @@ QSize Message::performCountOptimalSize() { refreshInfoSkipBlock(); const auto media = this->media(); - const auto botTop = item->isFakeBotAbout() + const auto botTop = item->isFakeAboutView() ? Get() : nullptr; if (botTop) { @@ -3263,7 +3263,7 @@ bool Message::drawBubble() const { const auto item = data(); if (isHidden()) { return false; - } else if (logEntryOriginal() || item->isFakeBotAbout()) { + } else if (logEntryOriginal() || item->isFakeAboutView()) { return true; } const auto media = this->media(); @@ -3760,7 +3760,7 @@ QRect Message::innerGeometry() const { QRect Message::countGeometry() const { const auto item = data(); - const auto centeredView = item->isFakeBotAbout() + const auto centeredView = item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost()); const auto media = this->media(); const auto mediaWidth = (media && media->isDisplayed()) @@ -3822,7 +3822,7 @@ Ui::BubbleRounding Message::countMessageRounding() const { const auto skipTail = smallBottom || (media && media->skipBubbleTail()) || (keyboard != nullptr) - || item->isFakeBotAbout() + || item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost()); const auto right = hasRightLayout(); using Corner = Ui::BubbleCornerRounding; @@ -3870,7 +3870,7 @@ int Message::resizeContentGetHeight(int newWidth) { } const auto item = data(); - const auto botTop = item->isFakeBotAbout() + const auto botTop = item->isFakeAboutView() ? Get() : nullptr; const auto media = this->media(); @@ -3878,7 +3878,7 @@ int Message::resizeContentGetHeight(int newWidth) { const auto bubble = drawBubble(); // This code duplicates countGeometry() but also resizes media. - const auto centeredView = item->isFakeBotAbout() + const auto centeredView = item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost()); auto contentWidth = newWidth - st::msgMargin.left() diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 03164193b..9b5ff9e12 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -2045,7 +2045,7 @@ bool Gif::dataLoaded() const { } bool Gif::needInfoDisplay() const { - if (_parent->data()->isFakeBotAbout()) { + if (_parent->data()->isFakeAboutView()) { return false; } return _parent->data()->isSending() diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index fa633ab37..e5996d1bf 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -902,7 +902,7 @@ bool Photo::dataLoaded() const { } bool Photo::needInfoDisplay() const { - if (_parent->data()->isFakeBotAbout()) { + if (_parent->data()->isFakeAboutView()) { return false; } return _parent->data()->isSending() diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 6bb085f1a..571f13a09 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -20,6 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { +int ServiceBoxContent::width() { + return st::msgServiceGiftBoxSize.width(); +} + ServiceBox::ServiceBox( not_null parent, std::unique_ptr content) @@ -27,7 +31,7 @@ ServiceBox::ServiceBox( , _parent(parent) , _content(std::move(content)) , _button({ .link = _content->createViewLink() }) -, _maxWidth(st::msgServiceGiftBoxSize.width() +, _maxWidth(_content->width() - st::msgPadding.left() - st::msgPadding.right()) , _title( @@ -48,7 +52,7 @@ ServiceBox::ServiceBox( kMarkupTextOptions, _maxWidth) , _size( - st::msgServiceGiftBoxSize.width(), + _content->width(), (st::msgServiceGiftBoxTopSkip + _content->top() + _content->size().height() diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index 2b459e027..d2d247eed 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -19,6 +19,7 @@ class ServiceBoxContent { public: virtual ~ServiceBoxContent() = default; + [[nodiscard]] virtual int width(); [[nodiscard]] virtual int top() = 0; [[nodiscard]] virtual QSize size() = 0; [[nodiscard]] virtual QString title() = 0; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 1b463cf50..114c6dadf 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -965,6 +965,9 @@ historySendDisabled: FlatLabel(defaultFlatLabel) { historySendDisabledIcon: icon {{ "emoji/premium_lock", placeholderFgActive }}; historySendDisabledIconSkip: 20px; historySendDisabledPosition: point(0px, 0px); +historySendPremiumRequired: FlatLabel(historySendDisabled) { + align: align(top); +} backgroundSwitchToDark: IconButton(defaultIconButton) { width: 48px; @@ -1031,3 +1034,7 @@ chatSimilarName: TextStyle(defaultTextStyle) { } chatSimilarWidthMax: 424px; chatSimilarSkip: 12px; + +premiumRequiredWidth: 186px; +premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; +premiumRequiredCircle: 60px; From 3829ed5880a94d598b08226bc013a385e8f38b6c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 16 Jan 2024 11:20:30 +0400 Subject: [PATCH 011/109] Fix build with Xcode. --- Telegram/SourceFiles/data/data_peer_values.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 7898f414f..874fa0ae7 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -228,7 +228,7 @@ inline auto DefaultRestrictionValue( : AmPremiumValue(&user->session()); }) | rpl::flatten_latest(); if (other) { - return std::move(allowedAny); + return allowedAny; } const auto mask = UserDataFlag::VoiceMessagesForbidden; return rpl::combine( From 1cfad1443777ea295659f64220c9b4859c52bb9a Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 Jan 2024 13:58:26 +0400 Subject: [PATCH 012/109] Show correct placeholder in require-premium story reply. --- Telegram/Resources/langs/lang.strings | 7 +- .../chat_helpers/chat_helpers.style | 8 + .../view/controls/compose_controls_common.h | 27 +- .../history_view_compose_controls.cpp | 237 +++++++++++++++--- .../controls/history_view_compose_controls.h | 7 +- .../view/history_view_replies_section.cpp | 6 +- .../view/history_view_scheduled_section.cpp | 6 +- .../media/stories/media_stories_reply.cpp | 18 ++ .../SourceFiles/media/view/media_view.style | 26 ++ .../SourceFiles/settings/settings_premium.cpp | 1 + Telegram/SourceFiles/ui/effects/premium.style | 8 + 11 files changed, 300 insertions(+), 51 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 5b2f2978d..9c6efb548 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3618,10 +3618,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_non_premium_text" = "Subscribe to **Premium**\n to message {user}."; "lng_send_non_premium_go" = "Go Premium"; "lng_send_non_premium_story" = "Replies restricted"; -"lng_send_non_premium_unlock" = "Unlock"; -"lng_send_non_premium_story_toast" = "You need a **Premium** subscription to reply to **{user}'s** stories."; -"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and **Premium** users."; -"lng_send_non_premium_toast_button" = "View"; +"lng_send_non_premium_unlock" = "unlock"; +"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers."; +"lng_send_non_premium_message_toast_link" = "Telegram Premium"; "lng_exceptions_list_title" = "Exceptions"; "lng_removed_list_title" = "Removed users"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 454c58d0e..c692b4cb6 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -208,6 +208,8 @@ ComposeControls { files: ComposeFiles; premium: PremiumLimits; boxField: InputField; + restrictionLabel: FlatLabel; + premiumRequired: ComposePremiumRequired; } ReportBox { @@ -1218,6 +1220,11 @@ defaultComposeFiles: ComposeFiles { nameFg: historyFileNameInFg; statusFg: mediaInFg; } +defaultRestrictionLabel: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: placeholderFg; + align: align(top); +} defaultComposeControls: ComposeControls { bg: historyComposeAreaBg; radius: 0px; @@ -1234,6 +1241,7 @@ defaultComposeControls: ComposeControls { files: defaultComposeFiles; premium: defaultPremiumLimits; boxField: defaultInputField; + restrictionLabel: defaultRestrictionLabel; } moreChatsBarHeight: 48px; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 8af04e830..e707523ec 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -34,6 +34,31 @@ struct SendActionUpdate { bool cancel = false; }; +enum class WriteRestrictionType { + None, + Rights, + PremiumRequired, +}; + +struct WriteRestriction { + using Type = WriteRestrictionType; + + QString text; + QString button; + Type type = Type::None; + + [[nodiscard]] bool empty() const { + return (type == Type::None); + } + explicit operator bool() const { + return !empty(); + } + + friend inline bool operator==( + const WriteRestriction &a, + const WriteRestriction &b) = default; +}; + struct SetHistoryArgs { required history; MsgId topicRootId = 0; @@ -42,7 +67,7 @@ struct SetHistoryArgs { rpl::producer slowmodeSecondsLeft; rpl::producer sendDisabledBySlowmode; rpl::producer liked; - rpl::producer> writeRestriction; + rpl::producer writeRestriction; }; struct ReplyNextRequest { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index e8e81511e..ac7735953 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -62,7 +62,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/send_as_peers.h" #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" +#include "settings/settings_premium.h" #include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" #include "ui/ui_utility.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/dropdown_menu.h" @@ -800,7 +802,6 @@ ComposeControls::ComposeControls( : not_null(_ownedSelector.get())) , _mode(descriptor.mode) , _wrap(std::make_unique(parent)) -, _writeRestricted(std::make_unique(parent)) , _send(std::make_shared(_wrap.get(), _st.send)) , _like(_features.likes ? Ui::CreateChild(_wrap.get(), _st.like) @@ -878,7 +879,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { _sendDisabledBySlowmode = rpl::single(false) | rpl::then(std::move(args.sendDisabledBySlowmode)); _liked = args.liked ? std::move(args.liked) : rpl::single(false); - _writeRestriction = rpl::single(std::optional()) + _writeRestriction = rpl::single(Controls::WriteRestriction()) | rpl::then(std::move(args.writeRestriction)); const auto history = *args.history; if (_history == history) { @@ -892,6 +893,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { registerDraftSource(); _selector->setCurrentPeer(history ? history->peer.get() : nullptr); initWebpageProcess(); + initWriteRestriction(); initForwardProcess(); updateBotCommandShown(); updateLikeShown(); @@ -941,12 +943,16 @@ PeerData *ComposeControls::sendAsPeer() const { void ComposeControls::move(int x, int y) { _wrap->move(x, y); - _writeRestricted->move(x, y); + if (_writeRestricted) { + _writeRestricted->move(x, y); + } } void ComposeControls::resizeToWidth(int width) { _wrap->resizeToWidth(width); - _writeRestricted->resizeToWidth(width); + if (_writeRestricted) { + _writeRestricted->resizeToWidth(width); + } updateHeight(); } @@ -961,12 +967,12 @@ rpl::producer ComposeControls::height() const { return rpl::conditional( _writeRestriction.value() | rpl::map(!_1), _wrap->heightValue(), - _writeRestricted->heightValue()); + rpl::single(_st.attach.height)); } int ComposeControls::heightCurrent() const { return _writeRestriction.current() - ? _writeRestricted->height() + ? _st.attach.height : _wrap->height(); } @@ -1120,7 +1126,9 @@ void ComposeControls::showStarted() { _autocomplete->hideFast(); } _wrap->hide(); - _writeRestricted->hide(); + if (_writeRestricted) { + _writeRestricted->hide(); + } } void ComposeControls::showFinished() { @@ -1319,7 +1327,8 @@ void ComposeControls::init() { _wrap->paintRequest( ) | rpl::start_with_next([=](QRect clip) { - paintBackground(clip); + auto p = QPainter(_wrap.get()); + paintBackground(p, _wrap->rect(), clip); }, _wrap->lifetime()); _header->editMsgIdValue( @@ -1466,18 +1475,6 @@ void ComposeControls::clearListenState() { _voiceRecordBar->clearListenState(); } -void ComposeControls::drawRestrictedWrite(QPainter &p, const QString &error) { - p.fillRect(_writeRestricted->rect(), st::historyReplyBg); - - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText( - _writeRestricted->rect().marginsRemoved( - QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), - error, - style::al_center); -} - void ComposeControls::initKeyHandler() { _wrap->events( ) | rpl::filter([=](not_null event) { @@ -2191,24 +2188,164 @@ void ComposeControls::inlineBotChanged() { } } +void SetupRestrictionView( + not_null widget, + not_null st, + std::shared_ptr show, + const QString &name, + rpl::producer restriction, + Fn paintBackground) { + struct State { + std::unique_ptr label; + std::unique_ptr button; + std::unique_ptr unlock; + std::unique_ptr icon; + Fn updateGeometries; + }; + const auto state = widget->lifetime().make_state(); + state->updateGeometries = [=] { + if (!state->label) { + return; + } else if (state->button) { + const auto available = widget->width() + - st->like.width + - st::historySendRight + - state->unlock->width() + - st->premiumRequired.buttonSkip + - st->premiumRequired.position.x(); + state->label->resizeToWidth(available); + state->label->moveToLeft( + st->premiumRequired.position.x(), + st->premiumRequired.position.y(), + widget->width()); + const auto left = st->premiumRequired.position.x() + + std::min(available, state->label->textMaxWidth()) + + st->premiumRequired.buttonSkip; + state->unlock->moveToLeft( + left, + st->premiumRequired.buttonTop, + widget->width()); + state->button->setGeometry(QRect( + QPoint(), + QSize(left + state->unlock->width(), widget->height()))); + state->icon->moveToLeft(0, 0, widget->width()); + } else { + const auto left = st::historySendRight; + state->label->resizeToWidth(widget->width() - 2 * left); + state->label->moveToLeft( + left, + (widget->height() - state->label->height()) / 2, + widget->width()); + } + }; + const auto makeLabel = [=]( + const QString &text, + const style::FlatLabel &st) { + auto label = std::make_unique(widget, text, st); + label->show(); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->heightValue( + ) | rpl::start_with_next( + state->updateGeometries, + label->lifetime()); + return label; + }; + const auto makeUnlock = [=](const QString &text, const QString &name) { + using namespace Ui; + auto unlock = std::make_unique( + widget, + rpl::single(text), + st->premiumRequired.button); + unlock->show(); + unlock->setAttribute(Qt::WA_TransparentForMouseEvents); + unlock->setTextTransform(RoundButton::TextTransform::NoTransform); + unlock->setFullRadius(true); + return unlock; + }; + const auto makeIcon = [=] { + auto icon = std::make_unique(widget); + icon->resize(st->premiumRequired.icon.size()); + icon->show(); + icon->paintRequest() | rpl::start_with_next([st, raw = icon.get()] { + auto p = QPainter(raw); + st->premiumRequired.icon.paint(p, {}, raw->width()); + }, icon->lifetime()); + return icon; + }; + std::move( + restriction + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](Controls::WriteRestriction value) { + using Type = Controls::WriteRestriction::Type; + if (value.type == Type::Rights) { + state->icon = nullptr; + state->unlock = nullptr; + state->button = nullptr; + state->label = makeLabel(value.text, st->restrictionLabel); + } else if (value.type == Type::PremiumRequired) { + state->icon = makeIcon(); + state->unlock = makeUnlock(value.button, name); + state->button = std::make_unique(widget); + state->button->setClickedCallback([=] { + ::Settings::ShowPremiumPromoToast( + show, + tr::lng_send_non_premium_message_toast( + tr::now, + lt_user, + TextWithEntities{ name }, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_non_premium_message_toast_link( + tr::now))), + Ui::Text::RichLangValue), + u"require_premium"_q); + }); + state->label = makeLabel(value.text, st->premiumRequired.label); + } + }, widget->lifetime()); + + widget->sizeValue( + ) | rpl::start_with_next(state->updateGeometries, widget->lifetime()); + + widget->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(widget); + paintBackground(p, clip); + }, widget->lifetime()); +} + void ComposeControls::initWriteRestriction() { + if (!_history) { + const auto was = base::take(_writeRestricted); + updateWrappingVisibility(); + return; + } + _writeRestricted = std::make_unique(_parent); + _writeRestricted->move(_wrap->pos()); + _writeRestricted->resizeToWidth(_wrap->widthNoMargins()); + _writeRestricted->sizeValue() | rpl::start_with_next([=] { + if (_like && _like->parentWidget() == _writeRestricted.get()) { + updateControlsGeometry(_wrap->size()); + } + }, _writeRestricted->lifetime()); _writeRestricted->resize( _writeRestricted->width(), st::historyUnblock.height); - _writeRestricted->paintRequest( - ) | rpl::start_with_next([=] { - if (const auto error = _writeRestriction.current()) { - auto p = Painter(_writeRestricted.get()); - drawRestrictedWrite(p, *error); - } - }, _wrap->lifetime()); + const auto background = [=](QPainter &p, QRect clip) { + paintBackground(p, _writeRestricted->rect(), clip); + }; + SetupRestrictionView( + _writeRestricted.get(), + &_st, + _show, + _history->peer->shortName(), + _writeRestriction.value(), + background); _writeRestriction.value( - ) | rpl::filter([=] { - return _wrap->isHidden() || _writeRestricted->isHidden(); - }) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=] { updateWrappingVisibility(); - }, _wrap->lifetime()); + }, _writeRestricted->lifetime()); } void ComposeControls::changeFocusedControl() { @@ -2261,9 +2398,26 @@ void ComposeControls::initVoiceRecordBar() { void ComposeControls::updateWrappingVisibility() { const auto hidden = _hidden.current(); - const auto restricted = _writeRestriction.current().has_value(); - _writeRestricted->setVisible(!hidden && restricted); + const auto &restriction = _writeRestriction.current(); + const auto restricted = !restriction.empty() && _writeRestricted; + if (_writeRestricted) { + _writeRestricted->setVisible(!hidden && restricted); + } _wrap->setVisible(!hidden && !restricted); + using namespace Controls; + if (_like) { + const auto hidden = _like->isHidden(); + if (_writeRestricted + && restriction.type == WriteRestrictionType::PremiumRequired) { + _like->setParent(_writeRestricted.get()); + } else { + _like->setParent(_wrap.get()); + } + if (!hidden) { + _like->show(); + updateControlsGeometry(_wrap->size()); + } + } if (!hidden && !restricted) { _wrap->raise(); } @@ -2363,9 +2517,14 @@ void ComposeControls::updateControlsGeometry(QSize size) { _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); if (_like) { - _like->moveToRight(right, buttonsTop); - if (_likeShown) { - right += _like->width(); + using Type = Controls::WriteRestrictionType; + if (_writeRestriction.current().type == Type::PremiumRequired) { + _like->moveToRight(st::historySendRight, 0); + } else { + _like->moveToRight(right, buttonsTop); + if (_likeShown) { + right += _like->width(); + } } } if (_botCommandStart) { @@ -2521,9 +2680,7 @@ void ComposeControls::updateAttachBotsMenu() { }, _attachBotsMenu->lifetime()); } -void ComposeControls::paintBackground(QRect clip) { - Painter p(_wrap.get()); - +void ComposeControls::paintBackground(QPainter &p, QRect full, QRect clip) { if (_backgroundRect) { //p.setCompositionMode(QPainter::CompositionMode_Source); //p.fillRect(clip, Qt::transparent); @@ -2532,7 +2689,7 @@ void ComposeControls::paintBackground(QRect clip) { auto hq = PainterHighQualityEnabler(p); p.setBrush(_st.bg); p.setPen(Qt::NoPen); - p.drawRoundedRect(_wrap->rect(), _st.radius, _st.radius); + p.drawRoundedRect(full, _st.radius, _st.radius); } else { p.fillRect(clip, _st.bg); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index a846cd4ee..795e25760 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -272,7 +272,7 @@ private: void updateControlsGeometry(QSize size); bool updateReplaceMediaButton(); void updateOuterGeometry(QRect rect); - void paintBackground(QRect clip); + void paintBackground(QPainter &p, QRect full, QRect clip); [[nodiscard]] auto computeSendButtonType() const; [[nodiscard]] SendMenu::Type sendMenuType() const; @@ -296,7 +296,6 @@ private: void setTabbedPanel(std::unique_ptr panel); bool showRecordButton() const; - void drawRestrictedWrite(QPainter &p, const QString &error); bool updateBotCommandShown(); bool updateLikeShown(); @@ -353,12 +352,12 @@ private: rpl::variable _slowmodeSecondsLeft; rpl::variable _sendDisabledBySlowmode; rpl::variable _liked; - rpl::variable> _writeRestriction; + rpl::variable _writeRestriction; rpl::variable _hidden; Mode _mode = Mode::Normal; const std::unique_ptr _wrap; - const std::unique_ptr _writeRestricted; + std::unique_ptr _writeRestricted; rpl::event_stream _jumpToItemRequests; std::optional _backgroundRect; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 157a61977..dca9b2633 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -699,7 +699,7 @@ void RepliesWidget::setupComposeControls() { const auto restriction = Data::RestrictionError( _history->peer, ChatRestriction::SendOther); - return !canSendAnything + auto text = !canSendAnything ? (restriction ? restriction : topicRestriction @@ -708,6 +708,10 @@ void RepliesWidget::setupComposeControls() { : topicRestriction ? std::move(topicRestriction) : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); }); _composeControls->setHistory({ diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index a086d71cb..befbce50d 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -198,11 +198,15 @@ void ScheduledWidget::setupComposeControls() { const auto restriction = Data::RestrictionError( _history->peer, ChatRestriction::SendOther); - return !canSendAnything + auto text = !canSendAnything ? (restriction ? restriction : tr::lng_group_not_accessible(tr::now)) : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); }); _composeControls->setHistory({ .history = _history.get(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 8a2088785..d06f32dbb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" #include "data/data_message_reaction_id.h" +#include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/controls/compose_controls_common.h" @@ -663,6 +664,22 @@ void ReplyArea::show( invalidate_weak_ptrs(&_shownPeerGuard); const auto peer = data.peer; const auto history = peer ? peer->owner().history(peer).get() : nullptr; + const auto user = peer->asUser(); + auto writeRestriction = Data::CanSendAnythingValue( + peer + ) | rpl::map([=](bool can) { + using namespace HistoryView::Controls; + return (can + || !user + || !user->meRequiresPremiumToWrite() + || user->session().premium()) + ? WriteRestriction() + : WriteRestriction{ + .text = tr::lng_send_non_premium_story(tr::now), + .button = tr::lng_send_non_premium_unlock(tr::now), + .type = WriteRestrictionType::PremiumRequired, + }; + }); _controls->setHistory({ .history = history, .liked = std::move( @@ -670,6 +687,7 @@ void ReplyArea::show( ) | rpl::map([](const Data::ReactionId &id) { return !id.empty(); }), + .writeRestriction = std::move(writeRestriction), }); _controls->clear(); const auto hidden = peer diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c857368f3..1d33b03b8 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -776,6 +776,32 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { menu: storiesPopupMenu; } + restrictionLabel: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: storiesComposeGrayText; + align: align(top); + } + premiumRequired: ComposePremiumRequired { + label: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: storiesComposeGrayText; + } + button: RoundButton(defaultActiveButton) { + textFg: storiesComposeWhiteText; + textFgOver: storiesComposeWhiteText; + textBg: groupCallMembersBgRipple; + textBgOver: groupCallMembersBgRipple; + width: -12px; + height: 18px; + textTop: 0px; + font: font(12px); + ripple: storiesComposeRipple; + } + buttonSkip: 6px; + buttonTop: 14px; + position: point(37px, 14px); + icon: icon{{ "emoji/premium_lock", storiesComposeGrayIcon, point(13px, 14px) }}; + } } storiesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) { scrollPadding: margins(0px, 6px, 0px, 4px); diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index a64e004c6..a20a15be3 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1322,6 +1322,7 @@ void ShowPremiumPromoToast( .text = std::move(textWithLink), .st = &st::defaultMultilineToast, .duration = Ui::Toast::kDefaultDuration * 2, + .adaptive = true, .multiline = true, .filter = crl::guard(&show->session(), [=]( const ClickHandlerPtr &, diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 5b97d8cb4..4134c5be9 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -35,6 +35,14 @@ PremiumCover { about: FlatLabel; additionalShadowForDarkThemes: bool; } +ComposePremiumRequired { + label: FlatLabel; + button: RoundButton; + buttonSkip: pixels; + buttonTop: pixels; + position: point; + icon: icon; +} premiumAboutTextStyle: TextStyle(defaultTextStyle) { font: font(12px); From 6e31993777f61b795a09beea5041c6aa007e92a6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 15 Jan 2024 11:36:17 +0400 Subject: [PATCH 013/109] Update API scheme on layer 172. --- Telegram/CMakeLists.txt | 1 + Telegram/Resources/langs/lang.strings | 3 +- .../SourceFiles/api/api_send_progress.cpp | 11 +- Telegram/SourceFiles/api/api_updates.cpp | 12 +- Telegram/SourceFiles/apiwrap.cpp | 55 ++++---- .../boxes/peer_list_controllers.cpp | 4 +- .../boxes/peers/edit_participant_box.cpp | 2 +- .../SourceFiles/data/data_lastseen_status.h | 131 ++++++++++++++++++ .../SourceFiles/data/data_peer_values.cpp | 123 +++++++--------- Telegram/SourceFiles/data/data_peer_values.h | 8 +- Telegram/SourceFiles/data/data_session.cpp | 13 +- Telegram/SourceFiles/data/data_user.cpp | 71 +++++----- Telegram/SourceFiles/data/data_user.h | 23 ++- .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 2 +- .../view/history_view_schedule_box.cpp | 10 +- .../view/history_view_top_bar_widget.cpp | 4 +- .../info/profile/info_profile_cover.cpp | 12 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 7 +- .../touchbar/items/mac_pinned_chats_item.mm | 48 +++---- .../profile/profile_block_group_members.cpp | 20 +-- .../profile/profile_block_group_members.h | 3 +- .../SourceFiles/storage/serialize_peer.cpp | 25 ++-- 22 files changed, 343 insertions(+), 245 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_lastseen_status.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index b7ecc7c8a..603fd3f75 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -515,6 +515,7 @@ PRIVATE data/data_groups.h data/data_histories.cpp data/data_histories.h + data/data_lastseen_status.h data/data_location.cpp data/data_location.h data/data_media_rotation.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9c6efb548..13a234755 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -141,8 +141,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_status_last_week" = "last seen within a week"; "lng_status_last_month" = "last seen within a month"; "lng_status_lastseen_now" = "last seen just now"; -"lng_status_lastseen_hidden" = "last seen hidden"; -"lng_status_lastseen_show" = "show"; +"lng_status_lastseen_when" = "when?"; "lng_status_lastseen_minutes#one" = "last seen {count} minute ago"; "lng_status_lastseen_minutes#other" = "last seen {count} minutes ago"; "lng_status_lastseen_hours#one" = "last seen {count} hour ago"; diff --git a/Telegram/SourceFiles/api/api_send_progress.cpp b/Telegram/SourceFiles/api/api_send_progress.cpp index d3e277261..93d287926 100644 --- a/Telegram/SourceFiles/api/api_send_progress.cpp +++ b/Telegram/SourceFiles/api/api_send_progress.cpp @@ -161,14 +161,13 @@ bool SendProgressManager::skipRequest(const Key &key) const { return true; } const auto recently = base::unixtime::now() - kSendTypingsToOfflineFor; - const auto online = user->onlineTill; - if (online == kOnlineRecently) { + const auto lastseen = user->lastseen(); + if (lastseen.isRecently()) { return false; - } else if (online < 0) { - return (-online < recently); - } else { - return (online < recently); + } else if (const auto value = lastseen.onlineTill()) { + return (value < recently); } + return true; } void SendProgressManager::done(mtpRequestId requestId) { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 54e2a37b2..a6d27aa63 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -944,8 +944,9 @@ void Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) { } const auto self = session().user(); - self->onlineTill = base::unixtime::now() - + (isOnline ? (config.onlineUpdatePeriod / 1000) : -1); + const auto onlineFor = (config.onlineUpdatePeriod / 1000); + self->updateLastseen(Data::LastseenStatus::OnlineTill( + base::unixtime::now() + (isOnline ? onlineFor : -1))); session().changes().peerUpdated( self, Data::PeerUpdate::Flag::OnlineStatus); @@ -1852,14 +1853,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateUserStatus: { auto &d = update.c_updateUserStatus(); if (const auto user = session().data().userLoaded(d.vuser_id())) { - const auto value = OnlineTillFromMTP( - d.vstatus(), - user->onlineTill); - if (user->onlineTill != value) { + const auto now = LastseenFromMTP(d.vstatus(), user->lastseen()); + if (user->updateLastseen(now)) { session().changes().peerUpdated( user, Data::PeerUpdate::Flag::OnlineStatus); - session().data().maybeStopWatchForOffline(user); } } if (UserId(d.vuser_id()) == session().userId()) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index b3940a5b0..73026cc88 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1922,25 +1922,28 @@ void ApiWrap::saveDraftToCloudDelayed(not_null thread) { void ApiWrap::updatePrivacyLastSeens() { const auto now = base::unixtime::now(); - _session->data().enumerateUsers([&](UserData *user) { - if (user->isSelf() || !user->isLoaded() || user->onlineTill <= 0) { - return; - } + if (!_session->premium()) { + _session->data().enumerateUsers([&](not_null user) { + if (user->isSelf() + || !user->isLoaded() + || user->lastseen().isHidden()) { + return; + } - if (user->onlineTill + 3 * 86400 >= now) { - user->onlineTill = kOnlineRecently; - } else if (user->onlineTill + 7 * 86400 >= now) { - user->onlineTill = kOnlineLastWeek; - } else if (user->onlineTill + 30 * 86400 >= now) { - user->onlineTill = kOnlineLastMonth; - } else { - user->onlineTill = kOnlineEmpty; - } - session().changes().peerUpdated( - user, - Data::PeerUpdate::Flag::OnlineStatus); - session().data().maybeStopWatchForOffline(user); - }); + const auto till = user->lastseen().onlineTill(); + user->updateLastseen((till + 3 * 86400 >= now) + ? Data::LastseenStatus::Recently(true) + : (till + 7 * 86400 >= now) + ? Data::LastseenStatus::WithinWeek(true) + : (till + 30 * 86400 >= now) + ? Data::LastseenStatus::WithinMonth(true) + : Data::LastseenStatus::LongAgo(true)); + session().changes().peerUpdated( + user, + Data::PeerUpdate::Flag::OnlineStatus); + session().data().maybeStopWatchForOffline(user); + }); + } if (_contactsStatusesRequestId) { request(_contactsStatusesRequestId).cancel(); @@ -1948,19 +1951,17 @@ void ApiWrap::updatePrivacyLastSeens() { _contactsStatusesRequestId = request(MTPcontacts_GetStatuses( )).done([=](const MTPVector &result) { _contactsStatusesRequestId = 0; - for (const auto &item : result.v) { - Assert(item.type() == mtpc_contactStatus); - auto &data = item.c_contactStatus(); - if (auto user = _session->data().userLoaded(data.vuser_id())) { - auto value = OnlineTillFromMTP( + for (const auto &status : result.v) { + const auto &data = status.data(); + const auto userId = UserId(data.vuser_id()); + if (const auto user = _session->data().userLoaded(userId)) { + const auto status = LastseenFromMTP( data.vstatus(), - user->onlineTill); - if (user->onlineTill != value) { - user->onlineTill = value; + user->lastseen()); + if (user->updateLastseen(status)) { session().changes().peerUpdated( user, Data::PeerUpdate::Flag::OnlineStatus); - session().data().maybeStopWatchForOffline(user); } } } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index bad9dbd89..6b4404d17 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -598,7 +598,9 @@ void ContactsBoxController::sortByOnline() { const auto now = base::unixtime::now(); const auto key = [&](const PeerListRow &row) { const auto user = row.peer()->asUser(); - return user ? (std::min(user->onlineTill, now) + 1) : TimeId(); + return user + ? (std::min(user->lastseen().onlineTill(), now + 1) + 1) + : TimeId(); }; const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { return key(a) > key(b); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index d867c0607..51d969d0e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -143,7 +143,7 @@ void EditParticipantBox::Inner::paintEvent(QPaintEvent *e) { ? tr::lng_status_bot_reads_all : tr::lng_status_bot_not_reads_all)(tr::now); } - return Data::OnlineText(_user->onlineTill, base::unixtime::now()); + return Data::OnlineText(_user->lastseen(), base::unixtime::now()); }(); p.setFont(st::contactsStatusFont); p.setPen(st::contactsStatusFg); diff --git a/Telegram/SourceFiles/data/data_lastseen_status.h b/Telegram/SourceFiles/data/data_lastseen_status.h new file mode 100644 index 000000000..4603c5406 --- /dev/null +++ b/Telegram/SourceFiles/data/data_lastseen_status.h @@ -0,0 +1,131 @@ +/* +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 +*/ +#pragma once + +namespace Data { + +inline constexpr auto kLifeStartDate = 1375315200; // Let it be 01.08.2013. + +class LastseenStatus final { +public: + LastseenStatus() = default; + + [[nodiscard]] static LastseenStatus Recently(bool byMe = false) { + return LastseenStatus(kRecentlyValue, false, byMe); + } + [[nodiscard]] static LastseenStatus WithinWeek(bool byMe = false) { + return LastseenStatus(kWithinWeekValue, false, byMe); + } + [[nodiscard]] static LastseenStatus WithinMonth(bool byMe = false) { + return LastseenStatus(kWithinMonthValue, false, byMe); + } + [[nodiscard]] static LastseenStatus LongAgo(bool byMe = false) { + return LastseenStatus(kLongAgoValue, false, byMe); + } + [[nodiscard]] static LastseenStatus OnlineTill( + TimeId till, + bool local = false, + bool hiddenByMe = false) { + return (till >= kLifeStartDate + kSpecialValueSkip) + ? LastseenStatus(till - kLifeStartDate, !local, hiddenByMe) + : LongAgo(hiddenByMe); + } + + [[nodiscard]] bool isHidden() const { + return !_available; + } + [[nodiscard]] bool isRecently() const { + return !_available && (_value == kRecentlyValue); + } + [[nodiscard]] bool isWithinWeek() const { + return !_available && (_value == kWithinWeekValue); + } + [[nodiscard]] bool isWithinMonth() const { + return !_available && (_value == kWithinMonthValue); + } + [[nodiscard]] bool isLongAgo() const { + return !_available && (_value == kLongAgoValue); + } + [[nodiscard]] bool isHiddenByMe() const { + return _hiddenByMe; + } + + [[nodiscard]] bool isOnline(TimeId now) const { + return (_value >= kSpecialValueSkip) + && (kLifeStartDate + _value > now); + } + [[nodiscard]] bool isLocalOnlineValue() const { + return !_available && (_value >= kSpecialValueSkip); + } + [[nodiscard]] TimeId onlineTill() const { + return (_value >= kSpecialValueSkip) + ? (kLifeStartDate + _value) + : 0; + } + + [[nodiscard]] uint32 serialize() const { + return (_value & 0x3FFFFFFF) + | (_available << 30) + | (_hiddenByMe << 31); + } + [[nodiscard]] static LastseenStatus FromSerialized(uint32 value) { + auto result = LastseenStatus(); + result._value = value & 0x3FFFFFFF; + result._available = (value >> 30) & 1; + result._hiddenByMe = (value >> 31) & 1; + return result.valid() ? result : LastseenStatus(); + } + + [[nodiscard]] static LastseenStatus FromLegacy(int32 value) { + auto result = LastseenStatus(); + if (value == -2) { + return LastseenStatus::Recently(); + } else if (value == -3) { + return LastseenStatus::WithinWeek(); + } else if (value == -4) { + return LastseenStatus::WithinMonth(); + } else if (value < -30) { + return LastseenStatus::OnlineTill(-value, true); + } else if (value > 0) { + return LastseenStatus::OnlineTill(value); + } + return LastseenStatus(); + } + + friend inline constexpr auto operator<=>( + LastseenStatus, + LastseenStatus) = default; + friend inline constexpr bool operator==( + LastseenStatus a, + LastseenStatus b) = default; + +private: + static constexpr auto kLongAgoValue = uint32(0); + static constexpr auto kRecentlyValue = uint32(1); + static constexpr auto kWithinWeekValue = uint32(2); + static constexpr auto kWithinMonthValue = uint32(3); + static constexpr auto kSpecialValueSkip = uint32(4); + static constexpr auto kValidAfter = kLifeStartDate + kSpecialValueSkip; + + [[nodiscard]] bool valid() const { + return !_available || (_value >= kSpecialValueSkip); + } + + LastseenStatus(uint32 value, bool available, bool hiddenByMe) + : _value(value) + , _available(available ? 1 : 0) + , _hiddenByMe(hiddenByMe ? 1 : 0) { + } + + uint32 _value : 30 = 0; + uint32 _available : 1 = 0; + uint32 _hiddenByMe : 1 = 0; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 874fa0ae7..58b85fdbd 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -28,23 +28,20 @@ constexpr auto kMinOnlineChangeTimeout = crl::time(1000); constexpr auto kMaxOnlineChangeTimeout = 86400 * crl::time(1000); constexpr auto kSecondsInDay = 86400; -int OnlinePhraseChangeInSeconds(TimeId online, TimeId now) { - if (online <= 0) { - if (-online > now) { - return (-online - now); - } - return std::numeric_limits::max(); +int OnlinePhraseChangeInSeconds(LastseenStatus status, TimeId now) { + const auto till = status.onlineTill(); + if (till > now) { + return till - now; + } else if (status.isHidden()) { + return std::numeric_limits::max(); } - if (online > now) { - return online - now; - } - const auto minutes = (now - online) / 60; + const auto minutes = (now - till) / 60; if (minutes < 60) { - return (minutes + 1) * 60 - (now - online); + return (minutes + 1) * 60 - (now - till); } - const auto hours = (now - online) / 3600; + const auto hours = (now - till) / 3600; if (hours < 12) { - return (hours + 1) * 3600 - (now - online); + return (hours + 1) * 3600 - (now - till); } const auto nowFull = base::unixtime::parse(now); const auto tomorrow = nowFull.date().addDays(1).startOfDay(); @@ -64,20 +61,17 @@ std::optional OnlineTextSpecial(not_null user) { return std::nullopt; } -std::optional OnlineTextCommon(TimeId online, TimeId now) { - if (online <= 0) { - switch (online) { - case kOnlineEmpty: return tr::lng_status_offline(tr::now); - case kOnlineRecently: return tr::lng_status_recently(tr::now); - case kOnlineLastWeek: return tr::lng_status_last_week(tr::now); - case kOnlineLastMonth: return tr::lng_status_last_month(tr::now); - case kOnlineHidden: return tr::lng_status_lastseen_hidden(tr::now); - } - return IsRecentOnline(online, now) - ? tr::lng_status_online(tr::now) - : tr::lng_status_recently(tr::now); - } else if (online > now) { +std::optional OnlineTextCommon(LastseenStatus status, TimeId now) { + if (status.isOnline(now)) { return tr::lng_status_online(tr::now); + } else if (status.isLongAgo()) { + return tr::lng_status_offline(tr::now); + } else if (status.isRecently()) { + return tr::lng_status_recently(tr::now); + } else if (status.isWithinWeek()) { + return tr::lng_status_last_week(tr::now); + } else if (status.isWithinMonth()) { + return tr::lng_status_last_month(tr::now); } return std::nullopt; } @@ -395,31 +389,22 @@ TimeId SortByOnlineValue(not_null user, TimeId now) { if (user->isServiceUser() || user->isBot()) { return -1; } - const auto online = user->onlineTill; - if (online <= 0) { - switch (online) { - case 0: - case -1: return online; - - case -2: { - return now - 3 * kSecondsInDay; - } break; - - case -3: { - return now - 7 * kSecondsInDay; - } break; - - case -4: { - return now - 30 * kSecondsInDay; - } break; - } - return -online; + const auto lastseen = user->lastseen(); + if (const auto till = lastseen.onlineTill()) { + return till; + } else if (lastseen.isRecently()) { + return now - 3 * kSecondsInDay; + } else if (lastseen.isWithinWeek()) { + return now - 7 * kSecondsInDay; + } else if (lastseen.isWithinMonth()) { + return now - 30 * kSecondsInDay; + } else { + return 0; } - return online; } -crl::time OnlineChangeTimeout(TimeId online, TimeId now) { - const auto result = OnlinePhraseChangeInSeconds(online, now); +crl::time OnlineChangeTimeout(Data::LastseenStatus status, TimeId now) { + const auto result = OnlinePhraseChangeInSeconds(status, now); Assert(result >= 0); return std::clamp( result * crl::time(1000), @@ -431,24 +416,26 @@ crl::time OnlineChangeTimeout(not_null user, TimeId now) { if (user->isServiceUser() || user->isBot()) { return kMaxOnlineChangeTimeout; } - return OnlineChangeTimeout(user->onlineTill, now); + return OnlineChangeTimeout(user->lastseen(), now); } -QString OnlineText(TimeId online, TimeId now) { - if (const auto common = OnlineTextCommon(online, now)) { +QString OnlineText(Data::LastseenStatus status, TimeId now) { + if (const auto common = OnlineTextCommon(status, now)) { return *common; } - const auto minutes = (now - online) / 60; + const auto till = status.onlineTill(); + Assert(till > 0); + const auto minutes = (now - till) / 60; if (!minutes) { return tr::lng_status_lastseen_now(tr::now); } else if (minutes < 60) { return tr::lng_status_lastseen_minutes(tr::now, lt_count, minutes); } - const auto hours = (now - online) / 3600; + const auto hours = (now - till) / 3600; if (hours < 12) { return tr::lng_status_lastseen_hours(tr::now, lt_count, hours); } - const auto onlineFull = base::unixtime::parse(online); + const auto onlineFull = base::unixtime::parse(till); const auto nowFull = base::unixtime::parse(now); const auto locale = QLocale(); if (onlineFull.date() == nowFull.date()) { @@ -466,16 +453,17 @@ QString OnlineText(not_null user, TimeId now) { if (const auto special = OnlineTextSpecial(user)) { return *special; } - return OnlineText(user->onlineTill, now); + return OnlineText(user->lastseen(), now); } QString OnlineTextFull(not_null user, TimeId now) { if (const auto special = OnlineTextSpecial(user)) { return *special; - } else if (const auto common = OnlineTextCommon(user->onlineTill, now)) { + } else if (const auto common = OnlineTextCommon(user->lastseen(), now)) { return *common; } - const auto onlineFull = base::unixtime::parse(user->onlineTill); + const auto till = user->lastseen().onlineTill(); + const auto onlineFull = base::unixtime::parse(till); const auto nowFull = base::unixtime::parse(now); const auto locale = QLocale(); if (onlineFull.date() == nowFull.date()) { @@ -490,25 +478,10 @@ QString OnlineTextFull(not_null user, TimeId now) { return tr::lng_status_lastseen_date_time(tr::now, lt_date, date, lt_time, time); } -bool OnlineTextActive(TimeId online, TimeId now) { - if (online <= 0) { - switch (online) { - case 0: - case -1: - case -2: - case -3: - case -4: return false; - } - return (-online > now); - } - return (online > now); -} - bool OnlineTextActive(not_null user, TimeId now) { - if (user->isServiceUser() || user->isBot()) { - return false; - } - return OnlineTextActive(user->onlineTill, now); + return !user->isServiceUser() + && !user->isBot() + && user->lastseen().isOnline(now); } bool IsUserOnline(not_null user, TimeId now) { diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index ad20d8243..c3a1ac9ad 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -20,6 +20,7 @@ namespace Data { struct Reaction; class ForumTopic; +class LastseenStatus; template inline auto FlagsValueWithMask( @@ -151,14 +152,15 @@ inline auto PeerFullFlagValue( not_null session); [[nodiscard]] TimeId SortByOnlineValue(not_null user, TimeId now); -[[nodiscard]] crl::time OnlineChangeTimeout(TimeId online, TimeId now); +[[nodiscard]] crl::time OnlineChangeTimeout( + LastseenStatus status, + TimeId now); [[nodiscard]] crl::time OnlineChangeTimeout( not_null user, TimeId now); -[[nodiscard]] QString OnlineText(TimeId online, TimeId now); +[[nodiscard]] QString OnlineText(LastseenStatus status, TimeId now); [[nodiscard]] QString OnlineText(not_null user, TimeId now); [[nodiscard]] QString OnlineTextFull(not_null user, TimeId now); -[[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now); [[nodiscard]] bool OnlineTextActive(not_null user, TimeId now); [[nodiscard]] bool IsUserOnline(not_null user, TimeId now = 0); [[nodiscard]] bool ChannelHasActiveCall(not_null channel); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 9ec603386..42aa348c3 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -731,11 +731,9 @@ not_null Session::processUser(const MTPUser &data) { } if (status && !minimal) { - const auto value = OnlineTillFromMTP(*status, result->onlineTill); - if (result->onlineTill != value) { - result->onlineTill = value; + const auto lastseen = LastseenFromMTP(*status, result->lastseen()); + if (result->updateLastseen(lastseen)) { flags |= UpdateFlag::OnlineStatus; - session().data().maybeStopWatchForOffline(result); } } @@ -1106,7 +1104,8 @@ void Session::watchForOffline(not_null user, TimeId now) { if (!Data::IsUserOnline(user, now)) { return; } - const auto till = user->onlineTill; + const auto lastseen = user->lastseen(); + const auto till = lastseen.onlineTill(); const auto &[i, ok] = _watchingForOffline.emplace(user, till); if (!ok) { if (i->second == till) { @@ -1114,7 +1113,7 @@ void Session::watchForOffline(not_null user, TimeId now) { } i->second = till; } - const auto timeout = Data::OnlineChangeTimeout(till, now); + const auto timeout = Data::OnlineChangeTimeout(lastseen, now); const auto fires = _watchForOfflineTimer.isActive() ? _watchForOfflineTimer.remainingTime() : -1; @@ -4397,7 +4396,7 @@ void Session::serviceNotification( MTPstring(), // username MTP_string("42777"), MTP_userProfilePhotoEmpty(), - MTP_userStatusRecently(), + MTP_userStatusRecently(MTP_flags(0)), MTPint(), // bot_info_version MTPVector(), MTPstring(), // bot_inline_placeholder diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 4c0d4ce5a..fbf1e6e8f 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -34,37 +34,26 @@ using UpdateFlag = Data::PeerUpdate::Flag; BotInfo::BotInfo() = default; -int RecentOnlineAfter(TimeId when) { - return (when > 0) ? (-when - kSetOnlineAfterActivity) : 0; -} - -bool IsRecentOnlineValue(int value) { - return (value < -kSetOnlineAfterActivity); -} - -bool IsRecentOnline(int value, TimeId now) { - return IsRecentOnlineValue(value) && (now < -value); -} - -int OnlineTillFromMTP( +Data::LastseenStatus LastseenFromMTP( const MTPUserStatus &status, - int currentOnlineTill) { - return status.match([](const MTPDuserStatusEmpty &) { - return kOnlineEmpty; - }, [&](const MTPDuserStatusRecently&) { - return IsRecentOnlineValue(currentOnlineTill) - ? currentOnlineTill - : kOnlineRecently; - }, [](const MTPDuserStatusLastWeek &) { - return kOnlineLastWeek; - }, [](const MTPDuserStatusLastMonth &) { - return kOnlineLastMonth; - }, [](const MTPDuserStatusHidden &) { - return kOnlineHidden; + Data::LastseenStatus currentStatus) { + return status.match([](const MTPDuserStatusEmpty &data) { + return Data::LastseenStatus::LongAgo(); + }, [&](const MTPDuserStatusRecently &data) { + return currentStatus.isLocalOnlineValue() + ? Data::LastseenStatus::OnlineTill( + currentStatus.onlineTill(), + true, + data.is_by_me()) + : Data::LastseenStatus::Recently(data.is_by_me()); + }, [](const MTPDuserStatusLastWeek &data) { + return Data::LastseenStatus::WithinWeek(data.is_by_me()); + }, [](const MTPDuserStatusLastMonth &data) { + return Data::LastseenStatus::WithinMonth(data.is_by_me()); }, [](const MTPDuserStatusOnline& data) { - return data.vexpires().v; + return Data::LastseenStatus::OnlineTill(data.vexpires().v); }, [](const MTPDuserStatusOffline &data) { - return data.vwas_online().v; + return Data::LastseenStatus::OnlineTill(data.vwas_online().v); }); } @@ -88,6 +77,19 @@ void UserData::setIsContact(bool is) { } } +Data::LastseenStatus UserData::lastseen() const { + return _lastseen; +} + +bool UserData::updateLastseen(Data::LastseenStatus value) { + if (_lastseen == value) { + return false; + } + _lastseen = value; + owner().maybeStopWatchForOffline(this); + return true; +} + // see Serialize::readPeer as well void UserData::setPhoto(const MTPUserProfilePhoto &photo) { photo.match([&](const MTPDuserProfilePhoto &data) { @@ -305,11 +307,14 @@ void UserData::setNameOrPhone(const QString &newNameOrPhone) { void UserData::madeAction(TimeId when) { if (isBot() || isServiceUser() || when <= 0) { return; - } else if (onlineTill <= 0 && -onlineTill < when) { - onlineTill = -when - kSetOnlineAfterActivity; - session().changes().peerUpdated(this, UpdateFlag::OnlineStatus); - } else if (onlineTill > 0 && onlineTill < when + 1) { - onlineTill = when + kSetOnlineAfterActivity; + } + const auto till = lastseen().onlineTill(); + if (till < when + 1 + && updateLastseen( + Data::LastseenStatus::OnlineTill( + when + kSetOnlineAfterActivity, + !till || lastseen().isLocalOnlineValue(), + lastseen().isHiddenByMe()))) { session().changes().peerUpdated(this, UpdateFlag::OnlineStatus); } } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 2951cd9ef..ff3062b81 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_chat_participant_status.h" +#include "data/data_lastseen_status.h" #include "data/data_user_names.h" #include "dialogs/dialogs_key.h" @@ -43,7 +44,7 @@ struct BotInfo { ChatAdminRights channelAdminRights; }; -enum class UserDataFlag { +enum class UserDataFlag : uint32 { Contact = (1 << 0), MutualContact = (1 << 1), Deleted = (1 << 2), @@ -73,18 +74,9 @@ enum class UserDataFlag { inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; -inline constexpr auto kOnlineEmpty = 0; -inline constexpr auto kOnlineRecently = -2; -inline constexpr auto kOnlineLastWeek = -3; -inline constexpr auto kOnlineLastMonth = -4; -inline constexpr auto kOnlineHidden = -5; - -[[nodiscard]] int RecentOnlineAfter(TimeId when); -[[nodiscard]] bool IsRecentOnlineValue(int value); -[[nodiscard]] bool IsRecentOnline(int value, TimeId now); -[[nodiscard]] int OnlineTillFromMTP( - const MTPUserStatus& status, - int currentOnlineTill); +[[nodiscard]] Data::LastseenStatus LastseenFromMTP( + const MTPUserStatus &status, + Data::LastseenStatus currentStatus); class UserData final : public PeerData { public: @@ -160,7 +152,6 @@ public: [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; QString nameOrPhone; - TimeId onlineTill = 0; enum class ContactStatus : char { Unknown, @@ -171,6 +162,9 @@ public: [[nodiscard]] bool isContact() const; void setIsContact(bool is); + [[nodiscard]] Data::LastseenStatus lastseen() const; + bool updateLastseen(Data::LastseenStatus value); + enum class CallsStatus : char { Unknown, Enabled, @@ -202,6 +196,7 @@ private: -> const std::vector & override; Flags _flags; + Data::LastseenStatus _lastseen; Data::UsernamesInfo _username; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 91362c73e..a625d8aca 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -57,7 +57,7 @@ const auto kPsaBadgePrefix = "cloud_lng_badge_psa_"; if (!history) { return false; } else if (const auto user = history->peer->asUser()) { - return (user->onlineTill > 0); + return !user->lastseen().isHidden(); } return !history->isForum(); } diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index c5e60091d..136a3f73c 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -59,10 +59,12 @@ TimeId DefaultScheduleTime() { } bool CanScheduleUntilOnline(not_null peer) { - return !peer->isSelf() - && peer->isUser() - && !peer->asUser()->isBot() - && (peer->asUser()->onlineTill > 0); + if (const auto user = peer->asUser()) { + return !user->isSelf() + && !user->isBot() + && !user->lastseen().isHidden(); + } + return false; } void ScheduleBox( 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 a670d8a91..c06eddf3f 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1620,7 +1620,7 @@ void TopBarWidget::updateOnlineDisplay() { auto online = 0; auto onlyMe = true; for (const auto &user : chat->participants) { - if (user->onlineTill > now) { + if (user->lastseen().isOnline(now)) { ++online; if (onlyMe && user != self) onlyMe = false; } @@ -1648,7 +1648,7 @@ void TopBarWidget::updateOnlineDisplay() { auto online = 0; auto onlyMe = true; for (auto &participant : std::as_const(channel->mgInfo->lastParticipants)) { - if (participant->onlineTill > now) { + if (participant->lastseen().isOnline(now)) { ++online; if (onlyMe && participant != self) { onlyMe = false; diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 7968c48d5..373a2723a 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -326,7 +326,7 @@ Cover::Cover( : nullptr) , _name(this, _st.name) , _status(this, _st.status) -, _showLastSeen(this, tr::lng_status_lastseen_show(), _st.showLastSeen) +, _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { _peer->updateFull(); @@ -376,7 +376,7 @@ void Cover::setupShowLastSeen() { && !user->isServiceUser() && user->session().premiumPossible()) { if (user->session().premium()) { - if (user->onlineTill == kOnlineHidden) { + if (user->lastseen().isHiddenByMe()) { user->updateFullForced(); } _showLastSeen->hide(); @@ -390,12 +390,12 @@ void Cover::setupShowLastSeen() { Data::AmPremiumValue(&user->session()) ) | rpl::start_with_next([=](auto, bool premium) { const auto wasShown = !_showLastSeen->isHidden(); - const auto onlineHidden = (user->onlineTill == kOnlineHidden); - const auto shown = onlineHidden + const auto hiddenByMe = user->lastseen().isHiddenByMe(); + const auto shown = hiddenByMe && !premium && user->session().premiumPossible(); _showLastSeen->setVisible(shown); - if (wasShown && premium && onlineHidden) { + if (wasShown && premium && hiddenByMe) { user->updateFullForced(); } }, _showLastSeen->lifetime()); @@ -405,7 +405,7 @@ void Cover::setupShowLastSeen() { ) | rpl::filter([=](Api::UserPrivacy::Rule rule) { return (rule.option == Api::UserPrivacy::Option::Everyone); }) | rpl::start_with_next([=] { - if (user->onlineTill == kOnlineHidden) { + if (user->lastseen().isHiddenByMe()) { user->updateFullForced(); } }, _showLastSeen->lifetime()); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 67e24c28d..5fac44c06 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -90,10 +90,9 @@ userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true p userStatusEmpty#9d05049 = UserStatus; userStatusOnline#edb93949 expires:int = UserStatus; userStatusOffline#8c703f was_online:int = UserStatus; -userStatusRecently#e26f42f1 = UserStatus; -userStatusLastWeek#7bf09fc = UserStatus; -userStatusLastMonth#77ebc742 = UserStatus; -userStatusHidden#cf7d64b1 = UserStatus; +userStatusRecently#7b197dc8 flags:# by_me:flags.0?true = UserStatus; +userStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus; +userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm index 63ceb76fb..3c6bfd25e 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm @@ -134,22 +134,13 @@ NSRect PeerRectByIndex(int index) { kCircleDiameter); } -TimeId CalculateOnlineTill(not_null peer) { - if (peer->isSelf() || peer->isRepliesChat()) { - return 0; - } +[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus( + not_null peer) { if (const auto user = peer->asUser()) { - if (!user->isServiceUser() && !user->isBot()) { - const auto onlineTill = user->onlineTill; - return IsRecentOnlineValue(onlineTill) - ? -onlineTill - : (onlineTill <= 0) - ? 0 - : onlineTill; - } + return user->lastseen(); } - return 0; -}; + return Data::LastseenStatus(); +} } // namespace @@ -175,7 +166,7 @@ TimeId CalculateOnlineTill(not_null peer) { bool onTop = false; Ui::Animations::Simple onlineAnimation; - TimeId onlineTill = 0; + Data::LastseenStatus lastseen; }; rpl::lifetime _lifetime; Main::Session *_session; @@ -570,10 +561,10 @@ TimeId CalculateOnlineTill(not_null peer) { const auto callTimer = [=](const auto &pin) { onlineTimer->cancel(); - if (pin->onlineTill) { - const auto time = pin->onlineTill - base::unixtime::now(); - if (time > 0) { - onlineTimer->callOnce(std::min(86400, time) + if (const auto till = pin->lastseen.onlineTill()) { + const auto left = till - base::unixtime::now(); + if (left > 0) { + onlineTimer->callOnce(std::min(86400, left) * crl::time(1000)); } } @@ -595,7 +586,7 @@ TimeId CalculateOnlineTill(not_null peer) { return; } const auto &pin = *it; - pin->onlineTill = CalculateOnlineTill(pin->peer); + pin->lastseen = CalculateLastseenStatus(pin->peer); callTimer(pin); @@ -603,9 +594,8 @@ TimeId CalculateOnlineTill(not_null peer) { pin->onlineAnimation.stop(); return; } - const auto online = Data::OnlineTextActive( - pin->onlineTill, - base::unixtime::now()); + const auto now = base::unixtime::now(); + const auto online = pin->lastseen.isOnline(now); if (pin->onlineAnimation.animating()) { pin->onlineAnimation.change( online ? 1. : 0., @@ -638,13 +628,12 @@ TimeId CalculateOnlineTill(not_null peer) { const auto index = pair.second; auto peer = pair.first.history()->peer; auto view = peer->createUserpicView(); - const auto onlineTill = CalculateOnlineTill(peer); - Pin pin = { + return std::make_unique(Pin{ .peer = std::move(peer), .userpicView = std::move(view), .index = index, - .onlineTill = onlineTill }; - return std::make_unique(std::move(pin)); + .lastseen = CalculateLastseenStatus(peer), + }); }) | ranges::to_vector; _selfUnpinned = ranges::none_of(peers, &PeerData::isSelf); _repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat); @@ -837,9 +826,8 @@ TimeId CalculateOnlineTill(not_null peer) { CGImageRelease(image); return; } - const auto online = Data::OnlineTextActive( - pin->onlineTill, - base::unixtime::now()); + const auto now = base::unixtime::now(); + const auto online = pin->lastseen.isOnline(now); const auto value = pin->onlineAnimation.value(online ? 1. : 0.); if (value < 0.05) { return; diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.cpp b/Telegram/SourceFiles/profile/profile_block_group_members.cpp index 3780d8dd3..e155a19f9 100644 --- a/Telegram/SourceFiles/profile/profile_block_group_members.cpp +++ b/Telegram/SourceFiles/profile/profile_block_group_members.cpp @@ -148,9 +148,8 @@ void GroupMembersWidget::refreshUserOnline(UserData *user) { _now = base::unixtime::now(); auto member = getMember(it->second); - member->statusHasOnlineColor = !user->isBot() - && Data::OnlineTextActive(user->onlineTill, _now); - member->onlineTill = user->onlineTill; + member->lastseen = user->lastseen(); + member->statusHasOnlineColor = Data::OnlineTextActive(user, _now); member->onlineForSort = user->isSelf() ? std::numeric_limits::max() : Data::SortByOnlineValue(user, _now); @@ -184,10 +183,10 @@ void GroupMembersWidget::updateItemStatusText(Item *item) { : tr::lng_status_bot_not_reads_all(tr::now); member->onlineTextTill = _now + 86400; } else { - member->statusHasOnlineColor = Data::OnlineTextActive(member->onlineTill, _now); - member->statusText = Data::OnlineText(member->onlineTill, _now); + member->statusHasOnlineColor = member->lastseen.isOnline(_now); + member->statusText = Data::OnlineText(member->lastseen, _now); const auto changeInMs = Data::OnlineChangeTimeout( - member->onlineTill, + member->lastseen, _now); member->onlineTextTill = _now + TimeId(changeInMs / 1000); } @@ -247,7 +246,8 @@ void GroupMembersWidget::updateOnlineCount() { for (const auto item : items()) { auto member = getMember(item); auto user = member->user(); - auto isOnline = !user->isBot() && Data::OnlineTextActive(member->onlineTill, _now); + auto isOnline = !user->isBot() + && member->lastseen.isOnline(_now); if (member->statusHasOnlineColor != isOnline) { member->statusHasOnlineColor = isOnline; member->statusText = QString(); @@ -439,9 +439,9 @@ auto GroupMembersWidget::computeMember(not_null user) if (it == _membersByUser.cend()) { auto member = new Member(user); it = _membersByUser.emplace(user, member).first; + member->lastseen = user->lastseen(); member->statusHasOnlineColor = !user->isBot() - && Data::OnlineTextActive(user->onlineTill, _now); - member->onlineTill = user->onlineTill; + && member->lastseen.isOnline(_now); member->onlineForSort = Data::SortByOnlineValue(user, _now); } return it->second; @@ -462,7 +462,7 @@ void GroupMembersWidget::updateOnlineDisplay() { } auto member = getMember(item); bool isOnline = !member->user()->isBot() - && Data::OnlineTextActive(member->onlineTill, _now); + && member->lastseen.isOnline(_now); if (!isOnline) { changed = true; } diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.h b/Telegram/SourceFiles/profile/profile_block_group_members.h index 84884778b..89ae41159 100644 --- a/Telegram/SourceFiles/profile/profile_block_group_members.h +++ b/Telegram/SourceFiles/profile/profile_block_group_members.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/timer.h" +#include "data/data_lastseen_status.h" #include "profile/profile_block_peer_list.h" namespace Ui { @@ -61,7 +62,7 @@ private: not_null user() const; TimeId onlineTextTill = 0; - TimeId onlineTill = 0; + Data::LastseenStatus lastseen; TimeId onlineForSort = 0; }; Member *getMember(Item *item) { diff --git a/Telegram/SourceFiles/storage/serialize_peer.cpp b/Telegram/SourceFiles/storage/serialize_peer.cpp index a9a4ee640..861bebdc9 100644 --- a/Telegram/SourceFiles/storage/serialize_peer.cpp +++ b/Telegram/SourceFiles/storage/serialize_peer.cpp @@ -21,7 +21,7 @@ namespace { constexpr auto kModernImageLocationTag = std::numeric_limits::min(); constexpr auto kVersionTag = uint64(0x77FF'FFFF'FFFF'FFFFULL); -constexpr auto kVersion = 1; +constexpr auto kVersion = 2; } // namespace @@ -111,12 +111,12 @@ std::optional readImageLocation( } uint32 peerSize(not_null peer) { - uint32 result = sizeof(quint64) - + sizeof(quint64) - + sizeof(qint32) - + sizeof(quint64) + uint32 result = sizeof(quint64) // id + + sizeof(quint64) // version tag + + sizeof(qint32) // version + + sizeof(quint64) // userpic photo id + imageLocationSize(peer->userpicLocation()) - + sizeof(qint32); + + sizeof(qint32); // userpic has video if (const auto user = peer->asUser()) { result += stringSize(user->firstName) + stringSize(user->lastName) @@ -124,7 +124,7 @@ uint32 peerSize(not_null peer) { + stringSize(user->username()) + sizeof(quint64) // access + sizeof(qint32) // flags - + sizeof(qint32) // onlineTill + + sizeof(quint32) // lastseen + sizeof(qint32) // contact + sizeof(qint32); // botInfoVersion } else if (const auto chat = peer->asChat()) { @@ -168,7 +168,7 @@ void writePeer(QDataStream &stream, not_null peer) { << quint64(user->accessHash()) << qint32(user->flags()) << botInlinePlaceholder - << qint32(user->onlineTill) + << quint32(user->lastseen().serialize()) << qint32(user->isContact() ? 1 : 0) << qint32(user->isBot() ? user->botInfo->version : -1); } else if (const auto chat = peer->asChat()) { @@ -233,7 +233,8 @@ PeerData *readPeer( if (const auto user = result->asUser()) { QString first, last, phone, username, inlinePlaceholder; quint64 access; - qint32 flags = 0, onlineTill, contact, botInfoVersion; + qint32 flags = 0, contact, botInfoVersion; + quint32 lastseen; stream >> first >> last >> phone >> username >> access; if (streamAppVersion >= 9012) { stream >> flags; @@ -241,7 +242,7 @@ PeerData *readPeer( if (streamAppVersion >= 9016) { stream >> inlinePlaceholder; } - stream >> onlineTill >> contact >> botInfoVersion; + stream >> lastseen >> contact >> botInfoVersion; userpicAccessHash = access; @@ -285,7 +286,9 @@ PeerData *readPeer( user->setFlags((user->flags() & ~flagsMask) | flagsSet); } user->setAccessHash(access); - user->onlineTill = onlineTill; + user->updateLastseen((version > 1) + ? Data::LastseenStatus::FromSerialized(lastseen) + : Data::LastseenStatus::FromLegacy(lastseen)); user->setIsContact(contact == 1); user->setBotInfoVersion(botInfoVersion); if (!inlinePlaceholder.isEmpty() && user->isBot()) { From 5daa5a00f0f81afc9f7aaf7fa9f96d1d70542bf5 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 16 Jan 2024 11:00:24 +0400 Subject: [PATCH 014/109] Correctly use contact_require_premium bit. --- Telegram/SourceFiles/data/data_session.cpp | 3 +-- Telegram/SourceFiles/data/data_user.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 42aa348c3..3592709c9 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -520,7 +520,6 @@ not_null Session::processUser(const MTPUser &data) { | Flag::Premium | Flag::Support | Flag::SomeRequirePremiumToWrite - | Flag::MeRequiresPremiumToWrite AssertIsDebug() | Flag::RequirePremiumToWriteKnown | (!minimal ? Flag::Contact @@ -543,7 +542,7 @@ not_null Session::processUser(const MTPUser &data) { | (data.is_premium() ? Flag::Premium : Flag()) | (data.is_support() ? Flag::Support : Flag()) | (data.is_contact_require_premium() - ? ((Flag::SomeRequirePremiumToWrite | Flag::MeRequiresPremiumToWrite) AssertIsDebug() + ? (Flag::SomeRequirePremiumToWrite | (result->someRequirePremiumToWrite() ? (result->requirePremiumToWriteKnown() ? Flag::RequirePremiumToWriteKnown diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index fbf1e6e8f..4e5029afe 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -511,7 +511,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { | Flag::VoiceMessagesForbidden | Flag::ReadDatesPrivate | Flag::RequirePremiumToWriteKnown - /*| Flag::MeRequiresPremiumToWrite*/; AssertIsDebug() + | Flag::MeRequiresPremiumToWrite; user->setFlags((user->flags() & ~mask) | (update.is_phone_calls_private() ? Flag::PhoneCallsPrivate From 3710d61a09699eebd65fc18c610abb70361745df Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 16 Jan 2024 16:53:46 +0400 Subject: [PATCH 015/109] Show "Hide read time" only on non-trivial privacy. --- Telegram/Resources/langs/lang.strings | 2 +- .../SourceFiles/boxes/edit_privacy_box.cpp | 8 +++-- Telegram/SourceFiles/boxes/edit_privacy_box.h | 3 +- .../settings/settings_privacy_controllers.cpp | 34 ++++++++++++++++--- .../settings/settings_privacy_controllers.h | 12 +++++-- 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 13a234755..3e4f9da5e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1108,7 +1108,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_lastseen_always_title" = "Always share with"; "lng_edit_privacy_lastseen_never_title" = "Never share with"; "lng_edit_lastseen_hide_read_time" = "Hide read time"; -"lng_edit_lastseen_hide_read_time_about" = "Do not show the time when you read a message to people you hid your last seen from. If you turn this on, their read time will also be hidden from you. This does not affect group chats."; +"lng_edit_lastseen_hide_read_time_about" = "Hide the time when you read messages from people who can't see your last seen. If you turn this on, their read time will also be hidden from you. This setting does not affect group chats."; "lng_edit_lastseen_subscribe" = "Subscribe to Telegram Premium"; "lng_edit_lastseen_subscribe_about" = "If you subscribe to Premium, you will see other users' last seen and read time even if you hid yours from them (unless they specifically restricted it)."; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 0a8bc866d..e1cbe8e10 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -379,7 +379,7 @@ void EditPrivacyBox::setupContent() { auto middle = _controller->setupMiddleWidget( _window, content, - std::move(optionValue)); + rpl::duplicate(optionValue)); if (middle) { content->add(std::move(middle)); } @@ -396,7 +396,11 @@ void EditPrivacyBox::setupContent() { _controller->exceptionsDescription() | Ui::Text::ToWithEntities(), st::defaultVerticalListSkip); - if (auto below = _controller->setupBelowWidget(_window, content)) { + auto below = _controller->setupBelowWidget( + _window, + content, + rpl::duplicate(optionValue)); + if (below) { content->add(std::move(below)); } diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 66cd75948..c715ef473 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -75,7 +75,8 @@ public: } [[nodiscard]] virtual object_ptr setupBelowWidget( not_null controller, - not_null parent) { + not_null parent, + rpl::producer