diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c02353676..c42e141da 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -551,6 +551,8 @@ PRIVATE data/data_sponsored_messages.h data/data_stories.cpp data/data_stories.h + data/data_stories_ids.cpp + data/data_stories_ids.h data/data_streaming.cpp data/data_streaming.h data/data_thread.cpp @@ -871,6 +873,12 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/stories/info_stories_inner_widget.cpp + info/stories/info_stories_inner_widget.h + info/stories/info_stories_provider.cpp + info/stories/info_stories_provider.h + info/stories/info_stories_widget.cpp + info/stories/info_stories_widget.h info/userpic/info_userpic_colors_editor.cpp info/userpic/info_userpic_colors_editor.h info/userpic/info_userpic_emoji_builder.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 220683f8e..40783dbab 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_activate" = "Activate"; "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; +"lng_menu_my_stories" = "My Stories"; "lng_disable_notifications_from_tray" = "Disable notifications"; "lng_enable_notifications_from_tray" = "Enable notifications"; diff --git a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h index d48542d19..4a4d62d17 100644 --- a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h @@ -57,7 +57,7 @@ public: return std::nullopt; } [[nodiscard]] std::optional nearest(Id id) const { - static_assert(std::is_same_v>); + static_assert(std::is_same_v>); if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) { return *it; } else if (_ids.empty()) { @@ -70,6 +70,10 @@ public: std::swap(_skippedBefore, _skippedAfter); } + friend inline bool operator==( + const AbstractSparseIds&, + const AbstractSparseIds&) = default; + private: IdsContainer _ids; std::optional _fullCount; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index af6b73fcc..aa3bf1785 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -51,14 +51,49 @@ Q_DECLARE_METATYPE(MsgId); return MsgId(a.bare - b.bare); } +using StoryId = int32; + +struct FullStoryId { + PeerId peer = 0; + StoryId story = 0; + + [[nodiscard]] bool valid() const { + return peer != 0 && story != 0; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; +}; + +struct FullReplyTo { + MsgId msgId = 0; + MsgId topicRootId = 0; + FullStoryId storyId; + + [[nodiscard]] bool valid() const { + return msgId || storyId; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58)); constexpr auto ClientMsgIds = (1LL << 31); constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds); +constexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1); +constexpr auto ServerMaxStoryId = StoryId(1 << 30); +constexpr auto StoryMsgIds = int64(ServerMaxStoryId); +constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); constexpr auto ScheduledMsgIdsRange = (1LL << 32); constexpr auto ShowAtUnreadMsgId = MsgId(0); -constexpr auto SpecialMsgIdShift = EndClientMsgId.bare; +constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1); constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2); constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4); @@ -81,6 +116,20 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId); return MsgId(StartClientMsgId.bare + index); } +[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { + return (id >= StartStoryMsgId && id < EndStoryMsgId); +} +[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept { + Expects(IsStoryMsgId(id)); + + return StoryId(id.bare - StartStoryMsgId.bare); +} +[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept { + Expects(id >= 0); + + return MsgId(StartStoryMsgId.bare + id); +} + [[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept { return (id > 0 && id < ServerMaxMsgId); } @@ -136,37 +185,6 @@ struct GlobalMsgId { } }; -using StoryId = int32; - -struct FullStoryId { - PeerId peer = 0; - StoryId story = 0; - - [[nodiscard]] bool valid() const { - return peer != 0 && story != 0; - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullStoryId, FullStoryId) = default; - friend inline bool operator==(FullStoryId, FullStoryId) = default; -}; - -struct FullReplyTo { - MsgId msgId = 0; - MsgId topicRootId = 0; - FullStoryId storyId; - - [[nodiscard]] bool valid() const { - return msgId || storyId; - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; - friend inline bool operator==(FullReplyTo, FullReplyTo) = default; -}; - namespace std { template <> diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 5eef52f69..fc164eb17 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -35,6 +35,8 @@ constexpr auto kPreloadAroundCount = 30; constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); constexpr auto kExpiredMineFirstPerPage = 30; constexpr auto kExpiredMinePerPage = 100; +constexpr auto kSavedFirstPerPage = 30; +constexpr auto kSavedPerPage = 100; using UpdateFlag = StoryUpdate::Flag; @@ -351,6 +353,30 @@ void Stories::apply(const MTPDupdateStory &data) { } } +void Stories::apply(not_null peer, const MTPUserStories *data) { + if (!data) { + applyDeletedFromSources(peer->id, StorySourcesList::All); + _all.erase(peer->id); + const auto i = _stories.find(peer->id); + if (i != end(_stories)) { + auto stories = base::take(i->second); + _stories.erase(i); + for (const auto &[id, story] : stories) { + // Duplicated in Stories::applyDeleted. + _deleted.emplace(FullStoryId{ peer->id, id }); + _expiring.remove(story->expires(), story->fullId()); + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + } + } + _sourceChanged.fire_copy(peer->id); + } else { + parseAndApply(*data); + } +} + void Stories::requestUserStories(not_null user) { if (!_requestingUserStories.emplace(user).second) { return; @@ -480,6 +506,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } else { applyDeletedFromSources(peerId, StorySourcesList::All); } + _sourceChanged.fire_copy(peerId); } Story *Stories::parseAndApply( @@ -503,6 +530,9 @@ Story *Stories::parseAndApply( session().changes().storyUpdated( i->second.get(), UpdateFlag::Edited); + if (const auto item = lookupItem(i->second.get())) { + item->applyChanges(i->second.get()); + } } return i->second.get(); } @@ -718,6 +748,7 @@ void Stories::applyDeleted(FullStoryId id) { if (j != end(_stories)) { const auto k = j->second.find(id.story); if (k != end(j->second)) { + // Duplicated in Stories::apply(peer, const MTPUserStories*). auto story = std::move(k->second); _expiring.remove(story->expires(), story->fullId()); j->second.erase(k); @@ -746,7 +777,7 @@ void Stories::applyExpired(FullStoryId id) { } void Stories::addToExpiredMine(not_null story) { - const auto added = _expiredMine.emplace(story->id()).second; + const auto added = _expiredMine.list.emplace(story->id()).second; if (added && _expiredMineTotal >= 0) { ++_expiredMineTotal; } @@ -775,6 +806,7 @@ void Stories::applyRemovedFromActive(FullStoryId id) { removeFromList(StorySourcesList::NotHidden); removeFromList(StorySourcesList::All); } + _sourceChanged.fire_copy(id.peer); } } } @@ -824,8 +856,42 @@ void Stories::sort(StorySourcesList list) { _sourcesChanged[index].fire({}); } -const base::flat_map &Stories::all() const { - return _all; +std::shared_ptr Stories::lookupItem(not_null story) { + const auto i = _items.find(story->peer()->id); + if (i == end(_items)) { + return nullptr; + } + const auto j = i->second.find(story->id()); + if (j == end(i->second)) { + return nullptr; + } + return j->second.lock(); +} + +std::shared_ptr Stories::resolveItem(not_null story) { + auto &items = _items[story->peer()->id]; + auto i = items.find(story->id()); + if (i == end(items)) { + i = items.emplace(story->id()).first; + } else if (const auto result = i->second.lock()) { + return result; + } + const auto history = _owner->history(story->peer()); + auto result = std::shared_ptr( + history->makeMessage(story).get(), + HistoryItem::Destroyer()); + i->second = result; + return result; +} + +std::shared_ptr Stories::resolveItem(FullStoryId id) { + const auto story = lookup(id); + return story ? resolveItem(*story) : std::shared_ptr(); +} + +const StoriesSource *Stories::source(PeerId id) const { + const auto i = _all.find(id); + return (i != end(_all)) ? &i->second : nullptr; } const std::vector &Stories::sources( @@ -841,6 +907,10 @@ rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { return _sourcesChanged[static_cast(list)].events(); } +rpl::producer Stories::sourceChanged() const { + return _sourceChanged.events(); +} + rpl::producer Stories::itemsChanged() const { return _itemsChanged.events(); } @@ -1102,7 +1172,7 @@ void Stories::loadViewsSlice( }).send(); } -const base::flat_set &Stories::expiredMine() const { +const StoriesIds &Stories::expiredMine() const { return _expiredMine; } @@ -1122,6 +1192,30 @@ bool Stories::expiredMineLoaded() const { return _expiredMineLoaded; } +const StoriesIds *Stories::saved(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? &i->second.ids : nullptr; +} + +rpl::producer Stories::savedChanged() const { + return _savedChanged.events(); +} + +int Stories::savedCount(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? i->second.total : 0; +} + +bool Stories::savedCountKnown(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && (i->second.total >= 0); +} + +bool Stories::savedLoaded(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && i->second.loaded; +} + void Stories::expiredMineLoadMore() { if (_expiredMineRequestId) { return; @@ -1136,33 +1230,81 @@ void Stories::expiredMineLoadMore() { _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(); + _expiredMineTotal = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { return id.vid().v; }); - _expiredMine.emplace(id); + _expiredMine.list.emplace(id); _expiredMineLastId = id; if (!parseAndApply(self, story, now)) { - _expiredMine.remove(id); + _expiredMine.list.remove(id); if (_expiredMineTotal > 0) { --_expiredMineTotal; } } } + _expiredMineTotal = std::max( + _expiredMineTotal, + int(_expiredMine.list.size())); + _expiredMineLoaded = data.vstories().v.empty(); _expiredMineChanged.fire({}); }).fail([=] { _expiredMineRequestId = 0; _expiredMineLoaded = true; - _expiredMineTotal = int(_expiredMine.size()); + _expiredMineTotal = int(_expiredMine.list.size()); + _expiredMineChanged.fire({}); }).send(); } +void Stories::savedLoadMore(PeerId peerId) { + Expects(peerIsUser(peerId)); + + auto &saved = _saved[peerId]; + if (saved.requestId) { + return; + } + const auto api = &_owner->session().api(); + const auto peer = _owner->peer(peerId); + saved.requestId = api->request(MTPstories_GetPinnedStories( + peer->asUser()->inputUser, + MTP_int(saved.lastId), + MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + auto &saved = _saved[peerId]; + saved.requestId = 0; + + const auto &data = result.data(); + const auto now = base::unixtime::now(); + saved.total = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + saved.ids.list.emplace(id); + saved.lastId = id; + if (!parseAndApply(peer, story, now)) { + saved.ids.list.remove(id); + if (saved.total > 0) { + --saved.total; + } + } + } + saved.total = std::max(saved.total, int(saved.ids.list.size())); + saved.loaded = data.vstories().v.empty(); + _savedChanged.fire_copy(peerId); + }).fail([=] { + auto &saved = _saved[peerId]; + saved.requestId = 0; + saved.loaded = true; + saved.total = int(saved.ids.list.size()); + _savedChanged.fire_copy(peerId); + }).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 a05a1de67..5d26f7474 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -39,6 +39,14 @@ struct StoryIdDates { friend inline bool operator==(StoryIdDates, StoryIdDates) = default; }; +struct StoriesIds { + base::flat_set> list; + + friend inline bool operator==( + const StoriesIds&, + const StoriesIds&) = default; +}; + struct StoryMedia { std::variant, not_null> data; @@ -192,19 +200,24 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); + void apply(not_null peer, const MTPUserStories *data); void loadAround(FullStoryId id, StoriesContext context); - [[nodiscard]] const base::flat_map &all() const; + const StoriesSource *source(PeerId id) const; [[nodiscard]] const std::vector &sources( StorySourcesList list) const; [[nodiscard]] bool sourcesLoaded(StorySourcesList list) const; [[nodiscard]] rpl::producer<> sourcesChanged( StorySourcesList list) const; + [[nodiscard]] rpl::producer sourceChanged() const; [[nodiscard]] rpl::producer itemsChanged() const; [[nodiscard]] base::expected, NoStory> lookup( FullStoryId id) const; void resolve(FullStoryId id, Fn done); + [[nodiscard]] std::shared_ptr resolveItem(FullStoryId id); + [[nodiscard]] std::shared_ptr resolveItem( + not_null story); [[nodiscard]] bool isQuitPrevent(); void markAsRead(FullStoryId id, bool viewed); @@ -217,14 +230,29 @@ public: std::optional offset, Fn)> done); - [[nodiscard]] const base::flat_set &expiredMine() const; + [[nodiscard]] const StoriesIds &expiredMine() const; [[nodiscard]] rpl::producer<> expiredMineChanged() const; [[nodiscard]] int expiredMineCount() const; [[nodiscard]] bool expiredMineCountKnown() const; [[nodiscard]] bool expiredMineLoaded() const; - [[nodiscard]] void expiredMineLoadMore(); + void expiredMineLoadMore(); + + [[nodiscard]] const StoriesIds *saved(PeerId peerId) const; + [[nodiscard]] rpl::producer savedChanged() const; + [[nodiscard]] int savedCount(PeerId peerId) const; + [[nodiscard]] bool savedCountKnown(PeerId peerId) const; + [[nodiscard]] bool savedLoaded(PeerId peerId) const; + void savedLoadMore(PeerId peerId); private: + struct Saved { + StoriesIds ids; + int total = -1; + StoryId lastId = 0; + bool loaded = false; + mtpRequestId requestId = 0; + }; + void parseAndApply(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( not_null peer, @@ -247,20 +275,25 @@ private: void removeDependencyStory(not_null story); void sort(StorySourcesList list); - void addToExpiredMine(not_null story); + [[nodiscard]] std::shared_ptr lookupItem( + not_null story); void sendMarkAsReadRequests(); void sendMarkAsReadRequest(not_null peer, StoryId tillId); void requestUserStories(not_null user); + void addToExpiredMine(not_null story); void registerExpiring(TimeId expires, FullStoryId id); void scheduleExpireTimer(); void processExpired(); const not_null _owner; - base::flat_map< + std::unordered_map< PeerId, base::flat_map>> _stories; + std::unordered_map< + PeerId, + base::flat_map>> _items; base::flat_multi_map _expiring; base::flat_set _deleted; base::Timer _expireTimer; @@ -273,11 +306,11 @@ private: PeerId, base::flat_map>>> _resolveSent; - std::map< + std::unordered_map< not_null, base::flat_set>> _dependentMessages; - base::flat_map _all; + std::unordered_map _all; std::vector _sources[kStorySourcesListCount]; rpl::event_stream<> _sourcesChanged[kStorySourcesListCount]; bool _sourcesLoaded[kStorySourcesListCount] = { false }; @@ -285,15 +318,19 @@ private: mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 }; + rpl::event_stream _sourceChanged; rpl::event_stream _itemsChanged; - base::flat_set _expiredMine; + StoriesIds _expiredMine; int _expiredMineTotal = -1; StoryId _expiredMineLastId = 0; bool _expiredMineLoaded = false; rpl::event_stream<> _expiredMineChanged; mtpRequestId _expiredMineRequestId = 0; + std::unordered_map _saved; + rpl::event_stream _savedChanged; + base::flat_set _markReadPending; base::Timer _markReadTimer; base::flat_set _markReadRequests; diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp new file mode 100644 index 000000000..610d51c3a --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -0,0 +1,142 @@ +/* +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 "data/data_stories_ids.h" + +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "main/main_session.h" + +namespace Data { + +rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + const auto stories = &peer->owner().stories(); + if (!stories->savedCountKnown(peer->id)) { + return; + } + + const auto source = stories->source(peer->id); + const auto saved = stories->saved(peer->id); + const auto count = stories->savedCount(peer->id); + Assert(saved != nullptr); + auto ids = base::flat_set(); + ids.reserve(saved->list.size() + 1); + auto total = count; + if (source && !source->ids.empty()) { + ++total; + const auto current = source->ids.front().id; + for (const auto id : ranges::views::reverse(saved->list)) { + const auto i = source->ids.lower_bound( + StoryIdDates{ id }); + if (i != end(source->ids) && i->id == id) { + --total; + } else { + ids.emplace(id); + } + } + ids.emplace(current); + } else { + auto all = saved->list | ranges::views::reverse; + ids = { begin(all), end(all) }; + } + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + total, + 0, + total - added); + consumer.put_next_copy(state->slice); + }; + + const auto stories = &peer->owner().stories(); + stories->sourceChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + stories->savedChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + if (!stories->savedCountKnown(peer->id)) { + stories->savedLoadMore(peer->id); + } + + push(); + + return lifetime; + }; +} + +rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + const auto stories = &session->data().stories(); + if (!stories->expiredMineCountKnown()) { + return; + } + + const auto expired = stories->expiredMine(); + const auto count = stories->expiredMineCount(); + auto ids = base::flat_set(); + ids.reserve(expired.list.size() + 1); + auto all = expired.list | ranges::views::reverse; + ids = { begin(all), end(all) }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + 0, + count - added); + consumer.put_next_copy(state->slice); + }; + + const auto stories = &session->data().stories(); + stories->expiredMineChanged( + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + if (!stories->expiredMineCountKnown()) { + stories->expiredMineLoadMore(); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h new file mode 100644 index 000000000..26f827f96 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -0,0 +1,32 @@ +/* +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 "data/data_abstract_sparse_ids.h" + +class PeerData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +using StoriesIdsSlice = AbstractSparseIds>; + +[[nodiscard]] rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit); + +[[nodiscard]] rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 710cc4e1f..a296e6cf4 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -301,6 +301,8 @@ enum class MessageFlag : uint64 { // Fake message with bot cover and information. FakeBotAbout = (1ULL << 36), + + StoryItem = (1ULL << 37), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index c903008ae..148ffb246 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_peer_bot_command.h" #include "data/data_photo.h" +#include "data/data_stories.h" #include "data/data_emoji_statuses.h" #include "data/data_user_names.h" #include "data/data_wall_paper.h" @@ -474,6 +475,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setWallPaper({}); } + user->owner().stories().apply(user, update.vstories()); + user->fullUpdated(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 830950416..0e6f05b3e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -128,16 +128,14 @@ State::State(not_null data, Data::StorySourcesList list) Content State::next() { auto result = Content{ .full = (_list == Data::StorySourcesList::All) }; - const auto &all = _data->all(); const auto &sources = _data->sources(_list); result.users.reserve(sources.size()); for (const auto &info : sources) { - const auto i = all.find(info.id); - Assert(i != end(all)); - const auto &source = i->second; + const auto source = _data->source(info.id); + Assert(source != nullptr); auto userpic = std::shared_ptr(); - const auto user = source.user; + const auto user = source->user; if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; } else { diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 7e0612860..e2840acc4 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -954,6 +954,8 @@ Media ParseMedia( result.content = ParsePoll(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice + }, [](const MTPDmessageMediaStory &data) { + // #TODO stories export }, [](const MTPDmessageMediaEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 1a99b7024..e886b7ed7 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. #include "data/data_sponsored_messages.h" +#include "data/data_stories.h" #include "data/data_wall_paper.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -289,6 +290,8 @@ std::unique_ptr HistoryItem::CreateMedia( item, qs(media.vemoticon()), media.vvalue().v); + }, [&](const MTPDmessageMediaStory &media) -> Result { + return nullptr; // #TODO stories }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -673,6 +676,17 @@ HistoryItem::HistoryItem( } } +HistoryItem::HistoryItem( + not_null history, + not_null story) +: id(StoryIdToMsgId(story->id())) +, _history(history) +, _from(history->peer) +, _flags(MessageFlag::Local | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem | MessageFlag::StoryItem) +, _date(story->date()) { + setStoryFields(story); +} + HistoryItem::~HistoryItem() { _media = nullptr; clearSavedMedia(); @@ -1491,6 +1505,31 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { finishEdition(keyboardTop); } +void HistoryItem::applyChanges(not_null story) { + Expects(_flags & MessageFlag::StoryItem); + Expects(StoryIdFromMsgId(id) == story->id()); + + _media = nullptr; + setStoryFields(story); + + finishEdition(-1); +} + +void HistoryItem::setStoryFields(not_null story) { + const auto spoiler = false; + if (const auto photo = story->photo()) { + _media = std::make_unique(this, photo, spoiler); + } else { + const auto document = story->document(); + _media = std::make_unique( + this, + document, + /*skipPremiumEffect=*/false, + spoiler); + } + setText(story->caption()); +} + void HistoryItem::applyEdition(const MTPDmessageService &message) { if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index bbc074879..c2f26fb44 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -179,6 +179,7 @@ public: const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup); + HistoryItem(not_null history, not_null story); ~HistoryItem(); struct Destroyer { @@ -332,6 +333,7 @@ public: [[nodiscard]] bool isService() const; void applyEdition(HistoryMessageEdition &&edition); + void applyChanges(not_null story); void applyEdition(const MTPDmessageService &message); void applyEdition(const MTPMessageExtendedMedia &media); @@ -559,6 +561,7 @@ private: bool updateServiceDependent(bool force = false); void setServiceText(PreparedServiceText &&prepared); + void setStoryFields(not_null story); void finishEdition(int oldKeyboardTop); void finishEditionToEmpty(); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 31c73afa8..275b6ebcb 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -443,6 +443,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; + }, [](const MTPDmessageMediaStory &data) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 26a46d22a..6efd528f6 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null controller) } Memento::Memento(not_null self) -: ContentMemento(Downloads::Tag{}) +: ContentMemento(Tag{}) , _media(self, 0, Media::Type::File) { } diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index fcd405a5e..640c75d67 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -332,6 +332,8 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; + } else if (const auto peer = storiesPeer()) { + return Stories::Tag{ peer, storiesTab() }; } else { return Downloads::Tag(); } @@ -363,4 +365,9 @@ ContentMemento::ContentMemento(Settings::Tag settings) ContentMemento::ContentMemento(Downloads::Tag downloads) { } +ContentMemento::ContentMemento(Stories::Tag stories) +: _storiesPeer(stories.peer) +, _storiesTab(stories.tab) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 44f4e2d8c..1a54f5fdf 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -24,14 +24,20 @@ template class PaddingWrap; } // namespace Ui -namespace Info { -namespace Settings { +namespace Info::Settings { struct Tag; -} // namespace Settings +} // namespace Info::Settings -namespace Downloads { +namespace Info::Downloads { struct Tag; -} // namespace Downloads +} // namespace Info::Downloads + +namespace Info::Stories { +struct Tag; +enum class Tab; +} // namespace Info::Stories + +namespace Info { class ContentMemento; class Controller; @@ -150,6 +156,7 @@ public: PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); + explicit ContentMemento(Stories::Tag stories); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { @@ -172,6 +179,12 @@ public: UserData *settingsSelf() const { return _settingsSelf; } + PeerData *storiesPeer() const { + return _storiesPeer; + } + Stories::Tab storiesTab() const { + return _storiesTab; + } PollData *poll() const { return _poll; } @@ -214,6 +227,8 @@ private: const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; UserData * const _settingsSelf = nullptr; + PeerData * const _storiesPeer = nullptr; + Stories::Tab _storiesTab = {}; PollData * const _poll = nullptr; const FullMsgId _pollContextId; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 58dda92fe..a9c57a75f 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -40,6 +40,9 @@ Key::Key(Settings::Tag settings) : _value(settings) { Key::Key(Downloads::Tag downloads) : _value(downloads) { } +Key::Key(Stories::Tag stories) : _value(stories) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -72,6 +75,20 @@ bool Key::isDownloads() const { return v::is(_value); } +PeerData *Key::storiesPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +Stories::Tab Key::storiesTab() const { + if (const auto tag = std::get_if(&_value)) { + return tag->tab; + } + return Stories::Tab(); +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index cdbf53396..82eb6f8eb 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -36,6 +36,25 @@ struct Tag { } // namespace Info::Downloads +namespace Info::Stories { + +enum class Tab { + Saved, + Archive, +}; + +struct Tag { + explicit Tag(not_null peer, Tab tab = {}) + : peer(peer) + , tab(tab) { + } + + not_null peer; + Tab tab = {}; +}; + +} // namespace Info::Stories + namespace Info { class Key { @@ -44,12 +63,15 @@ public: explicit Key(not_null topic); Key(Settings::Tag settings); Key(Downloads::Tag downloads); + Key(Stories::Tag stories); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; Data::ForumTopic *topic() const; UserData *settingsSelf() const; bool isDownloads() const; + PeerData *storiesPeer() const; + Stories::Tab storiesTab() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -63,6 +85,7 @@ private: not_null, Settings::Tag, Downloads::Tag, + Stories::Tag, PollKey> _value; }; @@ -81,6 +104,7 @@ public: Members, Settings, Downloads, + Stories, PollResults, }; using SettingsType = ::Settings::Type; @@ -123,23 +147,29 @@ class AbstractController : public Window::SessionNavigation { public: AbstractController(not_null parent); - virtual Key key() const = 0; - virtual PeerData *migrated() const = 0; - virtual Section section() const = 0; + [[nodiscard]] virtual Key key() const = 0; + [[nodiscard]] virtual PeerData *migrated() const = 0; + [[nodiscard]] virtual Section section() const = 0; - PeerData *peer() const; - PeerId migratedPeerId() const; - Data::ForumTopic *topic() const { + [[nodiscard]] PeerData *peer() const; + [[nodiscard]] PeerId migratedPeerId() const; + [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } - UserData *settingsSelf() const { + [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } - bool isDownloads() const { + [[nodiscard]] bool isDownloads() const { return key().isDownloads(); } - PollData *poll() const; - FullMsgId pollContextId() const { + [[nodiscard]] PeerData *storiesPeer() const { + return key().storiesPeer(); + } + [[nodiscard]] Stories::Tab storiesTab() const { + return key().storiesTab(); + } + [[nodiscard]] PollData *poll() const; + [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); } diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 1780d741a..b460edada 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include "lang/lang_keys.h" +#include "data/data_stories_ids.h" #include "storage/storage_shared_media.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" +#include "info/stories/info_stories_widget.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" @@ -124,4 +126,29 @@ inline auto AddCommonGroupsButton( return result; }; +inline auto AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker) { + auto count = Data::SavedStoriesIds( + user, + ServerMaxStoryId - 1, + 0 + ) | rpl::map([](const Data::StoriesIdsSlice &slice) { + return slice.fullCount(); + }) | rpl::filter_optional(); + auto result = AddCountedButton( + parent, + std::move(count), + [](int count) { + return tr::lng_stories_row_count(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection(Info::Stories::Make(user)); + }); + return result; +}; + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp index d956d4d76..970b63d02 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -338,6 +338,7 @@ void ListSection::resizeToWidth(int newWidth) { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: // #TODO stories case Type::RoundFile: { _itemsLeft = st::infoMediaSkip; _itemsTop = st::infoMediaSkip; @@ -375,6 +376,7 @@ int ListSection::recountHeight() { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: // #TODO stories case Type::RoundFile: { auto itemHeight = _itemWidth + st::infoMediaSkip; auto index = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 48e00a41b..9909cb23a 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" #include "layout/layout_selection.h" @@ -88,6 +89,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->storiesPeer()) { + return std::make_unique(controller); } return std::make_unique(controller); } @@ -126,6 +129,7 @@ ListWidget::DateBadge::DateBadge( , hideTimer(std::move(hideCallback)) , goodType(type == Type::Photo || type == Type::Video + || type == Type::PhotoVideo || type == Type::GIF) { } @@ -171,6 +175,9 @@ void ListWidget::start() { ) | rpl::start_with_next([this](QString &&query) { _provider->setSearchQuery(std::move(query)); }, lifetime()); + } else if (_controller->storiesPeer()) { + trackSession(&session()); + restart(); } else { trackSession(&session()); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index e9149d336..701754c13 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -169,7 +169,6 @@ private: void itemRemoved(not_null item); void itemLayoutChanged(not_null item); - void refreshViewer(); void refreshRows(); void trackSession(not_null session); diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index a02453a0e..6a480a6d2 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -44,11 +44,15 @@ Memento::Memento(not_null controller) : Memento( (controller->peer() ? controller->peer() + : controller->storiesPeer() + ? controller->storiesPeer() : controller->parentController()->session().user()), controller->topic(), controller->migratedPeerId(), (controller->section().type() == Section::Type::Downloads ? Type::File + : controller->section().type() == Section::Type::Stories + ? Type::PhotoVideo : controller->section().mediaType())) { } diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 855a0244c..3b85f8989 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -186,7 +186,23 @@ object_ptr InnerWidget::setupSharedMedia( icon, st::infoSharedMediaButtonIconPosition); }; + auto addStoriesButton = [&]( + not_null user, + const style::icon &icon) { + auto result = Media::AddStoriesButton( + content, + _controller, + user, + tracker); + object_ptr( + result, + icon, + st::infoSharedMediaButtonIconPosition); + }; + if (auto user = _peer->asUser()) { + addStoriesButton(user, st::infoIconMediaGroup); + } addMediaButton(MediaType::Photo, st::infoIconMediaPhoto); addMediaButton(MediaType::Video, st::infoIconMediaVideo); addMediaButton(MediaType::File, st::infoIconMediaFile); diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp new file mode 100644 index 000000000..a7739a77b --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -0,0 +1,191 @@ +/* +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 "info/stories/info_stories_inner_widget.h" + +#include "info/stories/info_stories_widget.h" +#include "info/media/info_media_list_widget.h" +#include "info/info_controller.h" +#include "ui/widgets/labels.h" +#include "styles/style_info.h" + +namespace Info::Stories { + +class EmptyWidget : public Ui::RpWidget { +public: + EmptyWidget(QWidget *parent); + + void setFullHeight(rpl::producer fullHeightValue); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + +private: + object_ptr _text; + int _height = 0; + +}; + +EmptyWidget::EmptyWidget(QWidget *parent) +: RpWidget(parent) +, _text(this, st::infoEmptyLabel) { +} + +void EmptyWidget::setFullHeight(rpl::producer fullHeightValue) { + std::move( + fullHeightValue + ) | rpl::start_with_next([this](int fullHeight) { + // Make icon center be on 1/3 height. + auto iconCenter = fullHeight / 3; + auto iconHeight = st::infoEmptyFile.height(); + auto iconTop = iconCenter - iconHeight / 2; + _height = iconTop + st::infoEmptyIconTop; + resizeToWidth(width()); + }, lifetime()); +} + +int EmptyWidget::resizeGetHeight(int newWidth) { + auto labelTop = _height - st::infoEmptyLabelTop; + auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; + _text->resizeToNaturalWidth(labelWidth); + + auto labelLeft = (newWidth - _text->width()) / 2; + _text->moveToLeft(labelLeft, labelTop, newWidth); + + update(); + return _height; +} + +void EmptyWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2; + const auto iconTop = height() - st::infoEmptyIconTop; + st::infoEmptyFile.paint(p, iconLeft, iconTop, width()); +} + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller) +: RpWidget(parent) +, _controller(controller) +, _empty(this) { + _empty->heightValue( + ) | rpl::start_with_next( + [this] { refreshHeight(); }, + _empty->lifetime()); + _list = setupList(); +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool InnerWidget::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::Stories) { + restoreState(memento); + return true; + } + return false; +} + +object_ptr InnerWidget::setupList() { + auto result = object_ptr( + this, + _controller); + result->heightValue( + ) | rpl::start_with_next( + [this] { refreshHeight(); }, + result->lifetime()); + using namespace rpl::mappers; + result->scrollToRequests( + ) | rpl::map([widget = result.data()](int to) { + return Ui::ScrollToRequest { + widget->y() + to, + -1 + }; + }) | rpl::start_to_stream( + _scrollToRequests, + result->lifetime()); + _selectedLists.fire(result->selectedListValue()); + _listTops.fire(result->topValue()); + return result; +} + +void InnerWidget::saveState(not_null memento) { + _list->saveState(&memento->media()); +} + +void InnerWidget::restoreState(not_null memento) { + _list->restoreState(&memento->media()); +} + +rpl::producer InnerWidget::selectedListValue() const { + return _selectedLists.events_starting_with( + _list->selectedListValue() + ) | rpl::flatten_latest(); +} + +void InnerWidget::selectionAction(SelectionAction action) { + _list->selectionAction(action); +} + +InnerWidget::~InnerWidget() = default; + +int InnerWidget::resizeGetHeight(int newWidth) { + _inResize = true; + auto guard = gsl::finally([this] { _inResize = false; }); + + _list->resizeToWidth(newWidth); + _empty->resizeToWidth(newWidth); + return recountHeight(); +} + +void InnerWidget::refreshHeight() { + if (_inResize) { + return; + } + resize(width(), recountHeight()); +} + +int InnerWidget::recountHeight() { + auto top = 0; + auto listHeight = 0; + if (_list) { + _list->moveToLeft(0, top); + listHeight = _list->heightNoMargins(); + top += listHeight; + } + if (listHeight > 0) { + _empty->hide(); + } else { + _empty->show(); + _empty->moveToLeft(0, top); + top += _empty->heightNoMargins(); + } + return top; +} + +void InnerWidget::setScrollHeightValue(rpl::producer value) { + using namespace rpl::mappers; + _empty->setFullHeight(rpl::combine( + std::move(value), + _listTops.events_starting_with( + _list->topValue() + ) | rpl::flatten_latest(), + _1 - _2)); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h new file mode 100644 index 000000000..f874b00f2 --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h @@ -0,0 +1,78 @@ +/* +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 "ui/rp_widget.h" +#include "ui/widgets/scroll_area.h" +#include "base/unique_qptr.h" + +namespace Ui { +class SettingsSlider; +class VerticalLayout; +} // namespace Ui + +namespace Info { +class Controller; +struct SelectedItems; +enum class SelectionAction; +} // namespace Info + +namespace Info::Media { +class ListWidget; +} // namespace Info::Media + +namespace Info::Stories { + +class Memento; +class EmptyWidget; + +class InnerWidget final : public Ui::RpWidget { +public: + InnerWidget( + QWidget *parent, + not_null controller); + + bool showInternal(not_null memento); + + void saveState(not_null memento); + void restoreState(not_null memento); + + void setScrollHeightValue(rpl::producer value); + + rpl::producer scrollToRequests() const; + rpl::producer selectedListValue() const; + void selectionAction(SelectionAction action); + + ~InnerWidget(); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + + object_ptr setupList(); + + const not_null _controller; + + object_ptr _list = { nullptr }; + object_ptr _empty; + + bool _inResize = false; + + rpl::event_stream _scrollToRequests; + rpl::event_stream> _selectedLists; + rpl::event_stream> _listTops; + +}; + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp new file mode 100644 index 000000000..bd5d88f93 --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -0,0 +1,407 @@ +/* +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 "info/stories/info_stories_provider.h" + +#include "info/media/info_media_widget.h" +#include "info/media/info_media_list_section.h" +#include "info/info_controller.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_stories_ids.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/history.h" +#include "core/application.h" +#include "storage/storage_shared_media.h" +#include "layout/layout_selection.h" +#include "styles/style_info.h" + +namespace Info::Stories { +namespace { + +using namespace Media; + +constexpr auto kPreloadedScreensCount = 4; +constexpr auto kPreloadedScreensCountFull + = kPreloadedScreensCount + 1 + kPreloadedScreensCount; + +[[nodiscard]] int MinStoryHeight(int width) { + auto itemsLeft = st::infoMediaSkip; + auto itemsInRow = (width - itemsLeft) + / (st::infoMediaMinGridSize + st::infoMediaSkip); + return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; +} + +} // namespace + +Provider::Provider(not_null controller) +: _controller(controller) +, _peer(controller->key().storiesPeer()) +, _history(_peer->owner().history(_peer)) +, _tab(controller->key().storiesTab()) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &layout : _layouts) { + layout.second.item->invalidateCache(); + } + }, _lifetime); +} + +Type Provider::type() { + return Type::PhotoVideo; +} + +bool Provider::hasSelectRestriction() { + return false; +} + +rpl::producer Provider::hasSelectRestrictionChanges() { + return rpl::never(); +} + +bool Provider::sectionHasFloatingHeader() { + return false; +} + +QString Provider::sectionTitle(not_null item) { + return QString(); +} + +bool Provider::sectionItemBelongsHere( + not_null item, + not_null previous) { + return true; +} + +bool Provider::isPossiblyMyItem(not_null item) { + return true; +} + +std::optional Provider::fullCount() { + return _slice.fullCount(); +} + +void Provider::restart() { + _layouts.clear(); + _aroundId = kDefaultAroundId; + _idsLimit = kMinimalIdsLimit; + _slice = Data::StoriesIdsSlice(); + refreshViewer(); +} + +void Provider::checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) { + const auto visibleWidth = viewport.width(); + const auto visibleHeight = viewport.height(); + const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight; + const auto minItemHeight = MinStoryHeight(visibleWidth); + const auto preloadedCount = preloadedHeight / minItemHeight; + const auto preloadIdsLimitMin = (preloadedCount / 2) + 1; + const auto preloadIdsLimit = preloadIdsLimitMin + + (visibleHeight / minItemHeight); + const auto after = _slice.skippedAfter(); + const auto topLoaded = after && (*after == 0); + const auto before = _slice.skippedBefore(); + const auto bottomLoaded = before && (*before == 0); + + const auto minScreenDelta = kPreloadedScreensCount + - kPreloadIfLessThanScreens; + const auto minIdDelta = (minScreenDelta * visibleHeight) + / minItemHeight; + const auto preloadAroundItem = [&](not_null layout) { + auto preloadRequired = false; + const auto id = StoryIdFromMsgId(layout->getItem()->id); + if (!preloadRequired) { + preloadRequired = (_idsLimit < preloadIdsLimitMin); + } + if (!preloadRequired) { + auto delta = _slice.distance(_aroundId, id); + Assert(delta != std::nullopt); + preloadRequired = (qAbs(*delta) >= minIdDelta); + } + if (preloadRequired) { + _idsLimit = preloadIdsLimit; + _aroundId = id; + refreshViewer(); + } + }; + + if (preloadTop && !topLoaded) { + preloadAroundItem(topLayout); + } else if (preloadBottom && !bottomLoaded) { + preloadAroundItem(bottomLayout); + } +} + +void Provider::setSearchQuery(QString query) { +} + +void Provider::refreshViewer() { + _viewerLifetime.destroy(); + const auto idForViewer = _aroundId; + const auto session = &_peer->session(); + auto ids = (_tab == Tab::Saved) + ? Data::SavedStoriesIds(_peer, idForViewer, _idsLimit) + : Data::ArchiveStoriesIds(session, idForViewer, _idsLimit); + std::move( + ids + ) | rpl::start_with_next([=](Data::StoriesIdsSlice &&slice) { + if (!slice.fullCount()) { + // Don't display anything while full count is unknown. + return; + } + _slice = std::move(slice); + if (const auto nearest = _slice.nearest(idForViewer)) { + _aroundId = *nearest; + } + _refreshed.fire({}); + }, _viewerLifetime); +} + +rpl::producer<> Provider::refreshed() { + return _refreshed.events(); +} + +std::vector Provider::fillSections( + not_null delegate) { + markLayoutsStale(); + const auto guard = gsl::finally([&] { clearStaleLayouts(); }); + + auto result = std::vector(); + auto section = ListSection(Type::PhotoVideo, sectionDelegate()); + auto count = _slice.size(); + for (auto i = count; i != 0;) { + const auto storyId = _slice[--i]; + if (const auto layout = getLayout(storyId, delegate)) { + if (!section.addItem(layout)) { + section.finishSection(); + result.push_back(std::move(section)); + section = ListSection(Type::PhotoVideo, sectionDelegate()); + section.addItem(layout); + } + } + } + if (!section.empty()) { + section.finishSection(); + result.push_back(std::move(section)); + } + return result; +} + +void Provider::markLayoutsStale() { + for (auto &layout : _layouts) { + layout.second.stale = true; + } +} + +void Provider::clearStaleLayouts() { + for (auto i = _layouts.begin(); i != _layouts.end();) { + if (i->second.stale) { + _layoutRemoved.fire(i->second.item.get()); + const auto taken = _items.take(i->first); + i = _layouts.erase(i); + } else { + ++i; + } + } +} + +rpl::producer> Provider::layoutRemoved() { + return _layoutRemoved.events(); +} + +BaseLayout *Provider::lookupLayout(const HistoryItem *item) { + return nullptr; +} + +bool Provider::isMyItem(not_null item) { + return IsStoryMsgId(item->id) && (item->history()->peer == _peer); +} + +bool Provider::isAfter( + not_null a, + not_null b) { + return (a->id < b->id); +} + +void Provider::itemRemoved(not_null item) { + const auto id = StoryIdFromMsgId(item->id); + if (const auto i = _layouts.find(id); i != end(_layouts)) { + _layoutRemoved.fire(i->second.item.get()); + _layouts.erase(i); + } +} + +BaseLayout *Provider::getLayout( + StoryId id, + not_null delegate) { + auto it = _layouts.find(id); + if (it == _layouts.end()) { + if (auto layout = createLayout(id, delegate)) { + layout->initDimensions(); + it = _layouts.emplace(id, std::move(layout)).first; + } else { + return nullptr; + } + } + it->second.stale = false; + return it->second.item.get(); +} + +HistoryItem *Provider::ensureItem(StoryId id) { + const auto i = _items.find(id); + if (i != end(_items)) { + return i->second.get(); + } + auto item = _peer->owner().stories().resolveItem({ _peer->id, id }); + if (!item) { + return nullptr; + } + return _items.emplace(id, std::move(item)).first->second.get(); +} + +std::unique_ptr Provider::createLayout( + StoryId id, + not_null delegate) { + const auto item = ensureItem(id); + if (!item) { + return nullptr; + } + const auto getPhoto = [&]() -> PhotoData* { + if (const auto media = item->media()) { + return media->photo(); + } + return nullptr; + }; + const auto getFile = [&]() -> DocumentData* { + if (const auto media = item->media()) { + return media->document(); + } + return nullptr; + }; + // #TODO stories + const auto maybeStory = item->history()->owner().stories().lookup( + { item->history()->peer->id, StoryIdFromMsgId(item->id) }); + const auto spoiler = maybeStory && !(*maybeStory)->expired(); + + using namespace Overview::Layout; + if (const auto photo = getPhoto()) { + return std::make_unique(delegate, item, photo, spoiler); + } else if (const auto file = getFile()) { + return std::make_unique