From 8eac04cb90b2b3e141d6e8047f163dc3f9f51d84 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Jun 2023 19:23:49 +0400 Subject: [PATCH] Track and load ids of expired mine stories. --- Telegram/SourceFiles/data/data_stories.cpp | 293 +++++++++++++++--- Telegram/SourceFiles/data/data_stories.h | 53 +++- .../history/history_item_helpers.cpp | 6 +- .../stories/media_stories_controller.cpp | 6 +- .../SourceFiles/window/window_main_menu.cpp | 32 ++ .../window/window_session_controller.cpp | 2 +- 6 files changed, 331 insertions(+), 61 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 69b14f0a7..5eef52f69 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_stories.h" +#include "base/unixtime.h" #include "api/api_text_entities.h" #include "apiwrap.h" #include "core/application.h" @@ -32,6 +33,8 @@ constexpr auto kMaxResolveTogether = 100; constexpr auto kIgnorePreloadAroundIfLoaded = 15; constexpr auto kPreloadAroundCount = 30; constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); +constexpr auto kExpiredMineFirstPerPage = 30; +constexpr auto kExpiredMinePerPage = 100; using UpdateFlag = StoryUpdate::Flag; @@ -80,11 +83,13 @@ Story::Story( StoryId id, not_null peer, StoryMedia media, - TimeId date) + TimeId date, + TimeId expires) : _id(id) , _peer(peer) , _media(std::move(media)) -, _date(date) { +, _date(date) +, _expires(expires) { } Session &Story::owner() const { @@ -103,8 +108,12 @@ StoryId Story::id() const { return _id; } -StoryIdDate Story::idDate() const { - return { _id, _date }; +bool Story::mine() const { + return _peer->isSelf(); +} + +StoryIdDates Story::idDates() const { + return { _id, _date, _expires }; } FullStoryId Story::fullId() const { @@ -115,6 +124,14 @@ TimeId Story::date() const { return _date; } +TimeId Story::expires() const { + return _expires; +} + +bool Story::expired(TimeId now) const { + return _expires <= (now ? now : base::unixtime::now()); +} + const StoryMedia &Story::media() const { return _media; } @@ -276,6 +293,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { Stories::Stories(not_null owner) : _owner(owner) +, _expireTimer([=] { processExpired(); }) , _markReadTimer([=] { sendMarkAsReadRequests(); }) { } @@ -293,21 +311,27 @@ Main::Session &Stories::session() const { void Stories::apply(const MTPDupdateStory &data) { const auto peerId = peerFromUser(data.vuser_id()); const auto user = not_null(_owner->peer(peerId)->asUser()); - const auto idDate = parseAndApply(user, data.vstory()); - if (!idDate) { + const auto now = base::unixtime::now(); + const auto idDates = parseAndApply(user, data.vstory(), now); + if (!idDates) { + return; + } + const auto expired = (idDates.expires <= now); + if (expired) { + applyExpired({ peerId, idDates.id }); return; } const auto i = _all.find(peerId); if (i == end(_all)) { requestUserStories(user); return; - } else if (i->second.ids.contains(idDate)) { + } else if (i->second.ids.contains(idDates)) { return; } - const auto was = i->second.info(); - i->second.ids.emplace(idDate); - const auto now = i->second.info(); - if (was == now) { + const auto wasInfo = i->second.info(); + i->second.ids.emplace(idDates); + const auto nowInfo = i->second.info(); + if (wasInfo == nowInfo) { return; } const auto refreshInList = [&](StorySourcesList list) { @@ -317,7 +341,7 @@ void Stories::apply(const MTPDupdateStory &data) { peerId, &StoriesSourceInfo::id); if (i != end(sources)) { - *i = now; + *i = nowInfo; sort(list); } }; @@ -342,7 +366,61 @@ void Stories::requestUserStories(not_null user) { _requestingUserStories.remove(user); applyDeletedFromSources(user->id, StorySourcesList::All); }).send(); +} +void Stories::registerExpiring(TimeId expires, FullStoryId id) { + for (auto i = _expiring.findFirst(expires) + ; (i != end(_expiring)) && (i->first == expires) + ; ++i) { + if (i->second == id) { + return; + } + } + const auto reschedule = _expiring.empty() + || (_expiring.front().first > expires); + _expiring.emplace(expires, id); + if (reschedule) { + scheduleExpireTimer(); + } +} + +void Stories::scheduleExpireTimer() { + if (_expireSchedulePosted) { + return; + } + _expireSchedulePosted = true; + crl::on_main(this, [=] { + if (!_expireSchedulePosted) { + return; + } + _expireSchedulePosted = false; + if (_expiring.empty()) { + _expireTimer.cancel(); + } else { + const auto nearest = _expiring.front().first; + const auto now = base::unixtime::now(); + const auto delay = (nearest > now) + ? (nearest - now) + : 0; + _expireTimer.callOnce(delay * crl::time(1000)); + } + }); +} + +void Stories::processExpired() { + const auto now = base::unixtime::now(); + auto expired = base::flat_set(); + auto i = begin(_expiring); + for (; i != end(_expiring) && i->first <= now; ++i) { + expired.emplace(i->second); + } + _expiring.erase(begin(_expiring), i); + for (const auto &id : expired) { + applyExpired(id); + } + if (!_expiring.empty()) { + scheduleExpireTimer(); + } } void Stories::parseAndApply(const MTPUserStories &stories) { @@ -357,9 +435,10 @@ void Stories::parseAndApply(const MTPUserStories &stories) { .hidden = user->hasStoriesHidden(), }; const auto &list = data.vstories().v; + const auto now = base::unixtime::now(); result.ids.reserve(list.size()); for (const auto &story : list) { - if (const auto id = parseAndApply(result.user, story)) { + if (const auto id = parseAndApply(result.user, story, now)) { result.ids.emplace(id); } } @@ -391,21 +470,31 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } sort(list); }; - add(StorySourcesList::All); - if (result.user->hasStoriesHidden()) { - applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + if (result.user->isContact()) { + add(StorySourcesList::All); + if (result.user->hasStoriesHidden()) { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + } else { + add(StorySourcesList::NotHidden); + } } else { - add(StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::All); } } Story *Stories::parseAndApply( not_null peer, - const MTPDstoryItem &data) { + const MTPDstoryItem &data, + TimeId now) { const auto media = ParseMedia(_owner, data.vmedia()); if (!media) { return nullptr; } + const auto expires = data.vexpire_date().v; + const auto expired = (expires >= now); + if (expired && !data.is_pinned() && !peer->isSelf()) { + return nullptr; + } const auto id = data.vid().v; auto &stories = _stories[peer->id]; const auto i = stories.find(id); @@ -421,25 +510,51 @@ Story *Stories::parseAndApply( id, peer, StoryMedia{ *media }, - data.vdate().v)).first->second.get(); + data.vdate().v, + data.vexpire_date().v)).first->second.get(); result->applyChanges(*media, data); + + if (expired) { + _expiring.remove(expires, result->fullId()); + applyExpired(result->fullId()); + } else { + registerExpiring(expires, result->fullId()); + } + return result; } -StoryIdDate Stories::parseAndApply( +StoryIdDates Stories::parseAndApply( not_null peer, - const MTPstoryItem &story) { + const MTPstoryItem &story, + TimeId now) { return story.match([&](const MTPDstoryItem &data) { - if (const auto story = parseAndApply(peer, data)) { - return story->idDate(); + if (const auto story = parseAndApply(peer, data, now)) { + return story->idDates(); } applyDeleted({ peer->id, data.vid().v }); - return StoryIdDate(); + return StoryIdDates(); }, [&](const MTPDstoryItemSkipped &data) { - return StoryIdDate{ data.vid().v, data.vdate().v }; + const auto expires = data.vexpire_date().v; + const auto expired = (expires >= now); + const auto fullId = FullStoryId{ peer->id, data.vid().v }; + if (!expired) { + registerExpiring(expires, fullId); + } else if (!peer->isSelf()) { + applyDeleted(fullId); + return StoryIdDates(); + } else { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } + return StoryIdDates{ + data.vid().v, + data.vdate().v, + data.vexpire_date().v, + }; }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peer->id, data.vid().v }); - return StoryIdDate(); + return StoryIdDates(); }); } @@ -571,9 +686,10 @@ void Stories::sendResolveRequests() { void Stories::processResolvedStories( not_null peer, const QVector &list) { + const auto now = base::unixtime::now(); for (const auto &item : list) { item.match([&](const MTPDstoryItem &data) { - if (!parseAndApply(peer, data)) { + if (!parseAndApply(peer, data, now)) { applyDeleted({ peer->id, data.vid().v }); } }, [&](const MTPDstoryItemSkipped &data) { @@ -595,6 +711,48 @@ void Stories::finalizeResolve(FullStoryId id) { } void Stories::applyDeleted(FullStoryId id) { + applyRemovedFromActive(id); + + _deleted.emplace(id); + const auto j = _stories.find(id.peer); + if (j != end(_stories)) { + const auto k = j->second.find(id.story); + if (k != end(j->second)) { + auto story = std::move(k->second); + _expiring.remove(story->expires(), story->fullId()); + j->second.erase(k); + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + if (j->second.empty()) { + _stories.erase(j); + } + } + } +} + +void Stories::applyExpired(FullStoryId id) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + if (story->peer()->isSelf()) { + addToExpiredMine(story); + } else if (!story->pinned()) { + applyDeleted(id); + return; + } + } + applyRemovedFromActive(id); +} + +void Stories::addToExpiredMine(not_null story) { + const auto added = _expiredMine.emplace(story->id()).second; + if (added && _expiredMineTotal >= 0) { + ++_expiredMineTotal; + } +} + +void Stories::applyRemovedFromActive(FullStoryId id) { const auto removeFromList = [&](StorySourcesList list) { const auto index = static_cast(list); auto &sources = _sources[index]; @@ -609,7 +767,7 @@ void Stories::applyDeleted(FullStoryId id) { }; const auto i = _all.find(id.peer); if (i != end(_all)) { - const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j != end(i->second.ids) && j->id == id.story) { i->second.ids.erase(j); if (i->second.ids.empty()) { @@ -619,22 +777,6 @@ void Stories::applyDeleted(FullStoryId id) { } } } - _deleted.emplace(id); - const auto j = _stories.find(id.peer); - if (j != end(_stories)) { - const auto k = j->second.find(id.story); - if (k != end(j->second)) { - auto story = std::move(k->second); - j->second.erase(k); - session().changes().storyUpdated( - story.get(), - UpdateFlag::Destroyed); - removeDependencyStory(story.get()); - if (j->second.empty()) { - _stories.erase(j); - } - } - } } void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { @@ -755,7 +897,7 @@ void Stories::loadAround(FullStoryId id, StoriesContext context) { if (i == end(_all)) { return; } - const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j == end(i->second.ids) || j->id != id.story) { return; } @@ -960,6 +1102,67 @@ void Stories::loadViewsSlice( }).send(); } +const base::flat_set &Stories::expiredMine() const { + return _expiredMine; +} + +rpl::producer<> Stories::expiredMineChanged() const { + return _expiredMineChanged.events(); +} + +int Stories::expiredMineCount() const { + return std::max(_expiredMineTotal, 0); +} + +bool Stories::expiredMineCountKnown() const { + return _expiredMineTotal >= 0; +} + +bool Stories::expiredMineLoaded() const { + return _expiredMineLoaded; +} + +void Stories::expiredMineLoadMore() { + if (_expiredMineRequestId) { + return; + } + const auto api = &_owner->session().api(); + _expiredMineRequestId = api->request(MTPstories_GetExpiredStories( + MTP_int(_expiredMineLastId), + MTP_int(_expiredMineLastId + ? kExpiredMinePerPage + : kExpiredMineFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + _expiredMineRequestId = 0; + + const auto &data = result.data(); + _expiredMineTotal = std::max( + data.vcount().v, + int(_expiredMine.size())); + _expiredMineLoaded = data.vstories().v.empty(); + const auto self = _owner->session().user(); + const auto now = base::unixtime::now(); + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + _expiredMine.emplace(id); + _expiredMineLastId = id; + if (!parseAndApply(self, story, now)) { + _expiredMine.remove(id); + if (_expiredMineTotal > 0) { + --_expiredMineTotal; + } + } + } + _expiredMineChanged.fire({}); + }).fail([=] { + _expiredMineRequestId = 0; + _expiredMineLoaded = true; + _expiredMineTotal = int(_expiredMine.size()); + }).send(); +} + bool Stories::isQuitPrevent() { if (!_markReadPending.empty()) { sendMarkAsReadRequests(); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index f8f152b77..a05a1de67 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/expected.h" #include "base/timer.h" +#include "base/weak_ptr.h" class Image; class PhotoData; @@ -22,9 +23,10 @@ namespace Data { class Session; -struct StoryIdDate { +struct StoryIdDates { StoryId id = 0; TimeId date = 0; + TimeId expires = 0; [[nodiscard]] bool valid() const { return id != 0; @@ -33,8 +35,8 @@ struct StoryIdDate { return valid(); } - friend inline auto operator<=>(StoryIdDate, StoryIdDate) = default; - friend inline bool operator==(StoryIdDate, StoryIdDate) = default; + friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default; + friend inline bool operator==(StoryIdDates, StoryIdDates) = default; }; struct StoryMedia { @@ -56,16 +58,20 @@ public: StoryId id, not_null peer, StoryMedia media, - TimeId date); + TimeId date, + TimeId expires); [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null peer() const; [[nodiscard]] StoryId id() const; - [[nodiscard]] StoryIdDate idDate() const; + [[nodiscard]] bool mine() const; + [[nodiscard]] StoryIdDates idDates() const; [[nodiscard]] FullStoryId fullId() const; [[nodiscard]] TimeId date() const; + [[nodiscard]] TimeId expires() const; + [[nodiscard]] bool expired(TimeId now = 0) const; [[nodiscard]] const StoryMedia &media() const; [[nodiscard]] PhotoData *photo() const; [[nodiscard]] DocumentData *document() const; @@ -101,6 +107,7 @@ private: std::vector _viewsList; int _views = 0; const TimeId _date = 0; + const TimeId _expires = 0; bool _pinned = false; }; @@ -119,7 +126,7 @@ struct StoriesSourceInfo { struct StoriesSource { not_null user; - base::flat_set ids; + base::flat_set ids; StoryId readTill = 0; bool hidden = false; @@ -167,7 +174,7 @@ struct StoriesContext { inline constexpr auto kStorySourcesListCount = 2; -class Stories final { +class Stories final : public base::has_weak_ptr { public: explicit Stories(not_null owner); ~Stories(); @@ -210,14 +217,23 @@ public: std::optional offset, Fn)> done); + [[nodiscard]] const base::flat_set &expiredMine() const; + [[nodiscard]] rpl::producer<> expiredMineChanged() const; + [[nodiscard]] int expiredMineCount() const; + [[nodiscard]] bool expiredMineCountKnown() const; + [[nodiscard]] bool expiredMineLoaded() const; + [[nodiscard]] void expiredMineLoadMore(); + private: void parseAndApply(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( not_null peer, - const MTPDstoryItem &data); - StoryIdDate parseAndApply( + const MTPDstoryItem &data, + TimeId now); + StoryIdDates parseAndApply( not_null peer, - const MTPstoryItem &story); + const MTPstoryItem &story, + TimeId now); void processResolvedStories( not_null peer, const QVector &list); @@ -225,20 +241,30 @@ private: void finalizeResolve(FullStoryId id); void applyDeleted(FullStoryId id); + void applyExpired(FullStoryId id); + void applyRemovedFromActive(FullStoryId id); void applyDeletedFromSources(PeerId id, StorySourcesList list); void removeDependencyStory(not_null story); void sort(StorySourcesList list); + void addToExpiredMine(not_null story); + void sendMarkAsReadRequests(); void sendMarkAsReadRequest(not_null peer, StoryId tillId); void requestUserStories(not_null user); + void registerExpiring(TimeId expires, FullStoryId id); + void scheduleExpireTimer(); + void processExpired(); const not_null _owner; base::flat_map< PeerId, base::flat_map>> _stories; + base::flat_multi_map _expiring; base::flat_set _deleted; + base::Timer _expireTimer; + bool _expireSchedulePosted = false; base::flat_map< PeerId, @@ -261,6 +287,13 @@ private: rpl::event_stream _itemsChanged; + base::flat_set _expiredMine; + int _expiredMineTotal = -1; + StoryId _expiredMineLastId = 0; + bool _expiredMineLoaded = false; + rpl::event_stream<> _expiredMineChanged; + mtpRequestId _expiredMineRequestId = 0; + base::flat_set _markReadPending; base::Timer _markReadTimer; base::flat_set _markReadRequests; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 0a1b1a8a0..31c73afa8 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -297,8 +297,10 @@ ClickHandlerPtr JumpToStoryClickHandler( ? separate->sessionController() : peer->session().tryResolveWindow(); if (controller) { - // #TODO stories decide context - controller->openPeerStory(peer, storyId, {}); + controller->openPeerStory( + peer, + storyId, + { Data::StoriesContextSingle() }); } }); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index c343a6214..bb7337650 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -422,14 +422,14 @@ void Controller::show( stories.loadMore(list); } }); - const auto idDate = story->idDate(); + const auto idDates = story->idDates(); if (!source) { return; } else if (source == &single) { - single.ids.emplace(idDate); + single.ids.emplace(idDates); _index = 0; } else { - const auto k = source->ids.find(idDate); + const auto k = source->ids.find(idDates); if (k == end(source->ids)) { return; } diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 305b88329..1a03e874f 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -59,6 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "mainwidget.h" #include "styles/style_window.h" #include "styles/style_widgets.h" @@ -759,6 +760,37 @@ void MainMenu::setupMenu() { )->setClickedCallback([=] { controller->showPeerHistory(controller->session().user()); }); + + const auto wrap = _menu->add( + object_ptr>( + _menu, + CreateButton( + _menu, + rpl::single(u"My Stories"_q), + st::mainMenuButton, + IconDescriptor{ + &st::settingsIconSavedMessages, + kIconLightOrange + }))); + const auto stories = &controller->session().data().stories(); + const auto &all = stories->all(); + const auto mine = all.find(controller->session().userPeerId()); + if ((mine != end(all) && !mine->second.ids.empty()) + || stories->expiredMineCount() > 0) { + wrap->toggle(true, anim::type::instant); + } else { + wrap->toggle(false, anim::type::instant); + if (!stories->expiredMineCountKnown()) { + stories->expiredMineLoadMore(); + wrap->toggleOn(stories->expiredMineChanged( + ) | rpl::map([=] { + return stories->expiredMineCount() > 0; + }) | rpl::filter(rpl::mappers::_1) | rpl::take(1)); + } + } + wrap->entity()->setClickedCallback([=] { + controller->showToast(u"My Stories"_q); + }); } else { addAction( tr::lng_profile_add_contact(), diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index fd26e5e28..a39f552d2 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2488,7 +2488,7 @@ void SessionController::openPeerStories( const auto i = all.find(peerId); if (i != end(all)) { const auto j = i->second.ids.lower_bound( - StoryIdDate{ i->second.readTill + 1 }); + StoryIdDates{ i->second.readTill + 1 }); openPeerStory( i->second.user, j != i->second.ids.end() ? j->id : i->second.ids.front().id,