diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c727d4c5e..4aa6eae95 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3424,6 +3424,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_forward" = "Forward"; "lng_mediaview_delete" = "Delete"; "lng_mediaview_save_to_profile" = "Save to Profile"; +"lng_mediaview_pin_story_done" = "Story pinned"; +"lng_mediaview_pin_story_about" = "Now it will be always shown on the top."; +"lng_mediaview_pin_stories_done#one" = "{count} story pinned"; +"lng_mediaview_pin_stories_done#other" = "{count} stories pinned"; +"lng_mediaview_pin_stories_about#one" = "Now it will be always shown on the top."; +"lng_mediaview_pin_stories_about#other" = "Now they will be always shown on the top."; +"lng_mediaview_unpin_story_done" = "Story unpinned."; +"lng_mediaview_unpin_stories_done#one" = "{count} story unpinned"; +"lng_mediaview_unpin_stories_done#other" = "{count} stories unpinned"; +"lng_mediaview_pin_limit#one" = "You can't pin more than {count} story."; +"lng_mediaview_pin_limit#other" = "You can't pin more than {count} stories."; "lng_mediaview_archive_story" = "Archive Story"; "lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_files_all" = "View all files"; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 391caba74..7806e1aa6 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "ui/layers/show.h" #include "ui/text/text_utilities.h" @@ -77,6 +78,47 @@ using UpdateFlag = StoryUpdate::Flag; } // namespace +int IndexRespectingPinned(const StoriesIds &ids, StoryId id) { + const auto i = ids.list.find(id); + if (ids.pinnedToTop.empty() || i == end(ids.list)) { + return int(i - begin(ids.list)); + } + const auto j = ranges::find(ids.pinnedToTop, id); + if (j != end(ids.pinnedToTop)) { + return int(j - begin(ids.pinnedToTop)); + } + auto result = int(i - begin(ids.list)); + for (const auto &pinnedId : ids.pinnedToTop) { + if (pinnedId < id) { + ++result; + } + } + + Ensures(result < int(ids.list.size())); + return result; +} + +StoryId IdRespectingPinned(const StoriesIds &ids, int index) { + Expects(index >= 0 && index < int(ids.list.size())); + + if (ids.pinnedToTop.empty()) { + return *(begin(ids.list) + index); + } else if (index < int(ids.pinnedToTop.size())) { + return ids.pinnedToTop[index]; + } + auto i = begin(ids.list) + index - int(ids.pinnedToTop.size()); + auto sorted = ids.pinnedToTop; + ranges::sort(sorted, ranges::greater()); + for (const auto &pinnedId : sorted) { + if (pinnedId >= *i) { + ++i; + } + } + + Ensures(i != end(ids.list)); + return *i; +} + StoriesSourceInfo StoriesSource::info() const { return { .id = peer->id, @@ -1674,6 +1716,10 @@ void Stories::savedLoadMore(PeerId peerId) { const auto &data = result.data(); const auto now = base::unixtime::now(); + auto pinnedToTopIds = data.vpinned_to_top().value_or_empty(); + auto pinnedToTop = pinnedToTopIds + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; saved.total = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { @@ -1691,6 +1737,7 @@ void Stories::savedLoadMore(PeerId peerId) { const auto ids = int(saved.ids.list.size()); saved.loaded = data.vstories().v.empty(); saved.total = saved.loaded ? ids : std::max(saved.total, ids); + setPinnedToTop(peerId, std::move(pinnedToTop)); _savedChanged.fire_copy(peerId); }).fail([=] { auto &saved = _saved[peerId]; @@ -1701,6 +1748,33 @@ void Stories::savedLoadMore(PeerId peerId) { }).send(); } +void Stories::setPinnedToTop( + PeerId peerId, + std::vector &&pinnedToTop) { + const auto i = _saved.find(peerId); + if (i == end(_saved) && pinnedToTop.empty()) { + return; + } + auto &saved = (i == end(_saved)) ? _saved[peerId] : i->second; + if (saved.ids.pinnedToTop != pinnedToTop) { + for (const auto id : saved.ids.pinnedToTop) { + if (!ranges::contains(pinnedToTop, id)) { + if (const auto maybeStory = lookup({ peerId, id })) { + (*maybeStory)->setPinnedToTop(false); + } + } + } + for (const auto id : pinnedToTop) { + if (!ranges::contains(saved.ids.pinnedToTop, id)) { + if (const auto maybeStory = lookup({ peerId, id })) { + (*maybeStory)->setPinnedToTop(true); + } + } + } + saved.ids.pinnedToTop = std::move(pinnedToTop); + } +} + void Stories::deleteList(const std::vector &ids) { if (ids.empty()) { return; @@ -1788,6 +1862,75 @@ void Stories::toggleInProfileList( }).send(); } +bool Stories::canTogglePinnedList( + const std::vector &ids, + bool pin) const { + Expects(!ids.empty()); + + if (!pin) { + return true; + } + + const auto peerId = ids.front().peer; + const auto i = _saved.find(peerId); + if (i == end(_saved)) { + return false; + } + + auto &already = i->second.ids.pinnedToTop; + auto count = int(already.size()); + for (const auto &id : ids) { + if (!ranges::contains(already, id.story)) { + ++count; + } + } + return count <= maxPinnedCount(); +} + +int Stories::maxPinnedCount() const { + const auto appConfig = &_owner->session().appConfig(); + return appConfig->get(u"stories_pinned_to_top_count_max"_q, 3); +} + +void Stories::togglePinnedList( + const std::vector &ids, + bool pin) { + if (ids.empty()) { + return; + } + const auto peerId = ids.front().peer; + auto &saved = _saved[peerId]; + auto list = QVector(); + list.reserve(maxPinnedCount()); + for (const auto &id : saved.ids.pinnedToTop) { + if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) { + list.push_back(MTP_int(id)); + } + } + if (pin) { + auto copy = ids; + ranges::sort(copy, ranges::greater()); + for (const auto &id : copy) { + if (id.peer == peerId + && !ranges::contains(saved.ids.pinnedToTop, id.story)) { + list.push_back(MTP_int(id.story)); + } + } + } + const auto api = &_owner->session().api(); + const auto peer = session().data().peer(peerId); + api->request(MTPstories_TogglePinnedToTop( + peer->input, + MTP_vector(list) + )).done([=] { + setPinnedToTop(peerId, list + | ranges::views::transform(&MTPint::v) + | ranges::to_vector); + _savedChanged.fire_copy(peerId); + }).send(); + +} + void Stories::report( std::shared_ptr show, FullStoryId id, diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 2508b3557..c5f340cc1 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -33,12 +33,17 @@ class StoryPreload; struct StoriesIds { base::flat_set> list; + std::vector pinnedToTop; friend inline bool operator==( const StoriesIds&, const StoriesIds&) = default; }; +// ids.list.size() if not found. +[[nodiscard]] int IndexRespectingPinned(const StoriesIds &ids, StoryId id); +[[nodiscard]] StoryId IdRespectingPinned(const StoriesIds &ids, int index); + struct StoriesSourceInfo { PeerId id = 0; TimeId last = 0; @@ -208,6 +213,11 @@ public: void toggleInProfileList( const std::vector &ids, bool inProfile); + [[nodiscard]] bool canTogglePinnedList( + const std::vector &ids, + bool pin) const; + [[nodiscard]] int maxPinnedCount() const; + void togglePinnedList(const std::vector &ids, bool pin); void report( std::shared_ptr show, FullStoryId id, @@ -314,6 +324,9 @@ private: void notifySourcesChanged(StorySourcesList list); void pushHiddenCountsToFolder(); + void setPinnedToTop( + PeerId peerId, + std::vector &&pinnedToTop); [[nodiscard]] int pollingInterval( const PollingSettings &settings) const; diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp index f8921fe8b..fbff87e09 100644 --- a/Telegram/SourceFiles/data/data_stories_ids.cpp +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -40,18 +40,23 @@ rpl::producer SavedStoriesIds( const auto &saved = stories->saved(peerId); const auto count = stories->savedCount(peerId); - const auto around = saved.list.lower_bound(aroundId); - const auto hasBefore = int(around - begin(saved.list)); - const auto hasAfter = int(end(saved.list) - around); + auto aroundIndex = IndexRespectingPinned(saved, aroundId); + if (aroundIndex == int(saved.list.size())) { + const auto around = saved.list.lower_bound(aroundId); + aroundIndex = int(around - begin(saved.list)); + } + const auto hasBefore = aroundIndex; + const auto hasAfter = int(saved.list.size()) - aroundIndex; if (hasAfter < limit) { stories->savedLoadMore(peerId); } const auto takeBefore = std::min(hasBefore, limit); const auto takeAfter = std::min(hasAfter, limit); - auto ids = base::flat_set{ - std::make_reverse_iterator(around + takeAfter), - std::make_reverse_iterator(around - takeBefore) - }; + auto ids = std::vector(); + ids.reserve(takeBefore + takeAfter); + for (auto i = aroundIndex - takeBefore; i != aroundIndex + takeAfter; ++i) { + ids.push_back(IdRespectingPinned(saved, i)); + } const auto added = int(ids.size()); state->slice = StoriesIdsSlice( std::move(ids), @@ -114,18 +119,23 @@ rpl::producer ArchiveStoriesIds( const auto &archive = stories->archive(peerId); const auto count = stories->archiveCount(peerId); - const auto i = archive.list.lower_bound(aroundId); - const auto hasBefore = int(i - begin(archive.list)); - const auto hasAfter = int(end(archive.list) - i); + auto aroundIndex = IndexRespectingPinned(archive, aroundId); + if (aroundIndex == int(archive.list.size())) { + const auto around = archive.list.lower_bound(aroundId); + aroundIndex = int(around - begin(archive.list)); + } + const auto hasBefore = aroundIndex; + const auto hasAfter = int(archive.list.size()) - aroundIndex; if (hasAfter < limit) { stories->archiveLoadMore(peerId); } const auto takeBefore = std::min(hasBefore, limit); const auto takeAfter = std::min(hasAfter, limit); - auto ids = base::flat_set{ - std::make_reverse_iterator(i + takeAfter), - std::make_reverse_iterator(i - takeBefore) - }; + auto ids = std::vector(); + ids.reserve(takeBefore + takeAfter); + for (auto i = aroundIndex - takeBefore; i != aroundIndex + takeAfter; ++i) { + ids.push_back(IdRespectingPinned(archive, i)); + } const auto added = int(ids.size()); state->slice = StoriesIdsSlice( std::move(ids), diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h index 6c95c16a9..88a5103b3 100644 --- a/Telegram/SourceFiles/data/data_stories_ids.h +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -17,7 +17,7 @@ class Session; namespace Data { -using StoriesIdsSlice = AbstractSparseIds>; +using StoriesIdsSlice = AbstractSparseIds>; [[nodiscard]] rpl::producer SavedStoriesIds( not_null peer, diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 6a525a7a6..d191d2360 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -389,6 +389,14 @@ TextWithEntities Story::inReplyText() const { Ui::Text::WithEntities); } +void Story::setPinnedToTop(bool pinned) { + _pinnedToTop = pinned; +} + +bool Story::pinnedToTop() const { + return _pinnedToTop; +} + void Story::setInProfile(bool value) { _inProfile = value; } @@ -431,8 +439,8 @@ bool Story::canDownloadChecked() const { } bool Story::canShare() const { - return _privacyPublic - && !forbidsForward() + return _privacyPublic + && !forbidsForward() && (inProfile() || !expired()); } diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index f4fd17d60..512f0ed38 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -153,6 +153,9 @@ public: [[nodiscard]] Image *replyPreview() const; [[nodiscard]] TextWithEntities inReplyText() const; + void setPinnedToTop(bool pinned); + bool pinnedToTop() const; + void setInProfile(bool value); [[nodiscard]] bool inProfile() const; [[nodiscard]] StoryPrivacy privacy() const; @@ -251,6 +254,7 @@ private: TimeId _lastUpdateTime = 0; bool _out : 1 = false; bool _inProfile : 1 = false; + bool _pinnedToTop : 1 = false; bool _privacyPublic : 1 = false; bool _privacyCloseFriends : 1 = false; bool _privacyContacts : 1 = false; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index cf6f410cd..27e3750b4 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -44,6 +44,8 @@ InfoTopBar { mediaDelete: IconButton; storiesSave: IconButton; storiesArchive: IconButton; + storiesPin: IconButton; + storiesUnpin: IconButton; search: IconButton; searchRow: SearchFieldRow; highlightBg: color; @@ -185,6 +187,14 @@ infoTopBarArchiveStories: IconButton(infoTopBarForward) { icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; } +infoTopBarPinStories: IconButton(infoTopBarForward) { + icon: icon {{ "menu/pin", boxTitleCloseFg }}; + iconOver: icon {{ "menu/pin", boxTitleCloseFgOver }}; +} +infoTopBarUnpinStories: IconButton(infoTopBarForward) { + icon: icon {{ "menu/unpin", boxTitleCloseFg }}; + iconOver: icon {{ "menu/unpin", boxTitleCloseFgOver }}; +} infoTopBar: InfoTopBar { height: infoTopBarHeight; back: infoTopBarBack; @@ -205,6 +215,8 @@ infoTopBar: InfoTopBar { mediaDelete: infoTopBarDelete; storiesSave: infoTopBarSaveStories; storiesArchive: infoTopBarArchiveStories; + storiesPin: infoTopBarPinStories; + storiesUnpin: infoTopBarUnpinStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; highlightBg: windowBgOver; @@ -268,6 +280,14 @@ infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) { icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; } +infoLayerTopBarPinStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "menu/pin", boxTitleCloseFg }}; + iconOver: icon {{ "menu/pin", boxTitleCloseFgOver }}; +} +infoLayerTopBarUnpinStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "menu/unpin", boxTitleCloseFg }}; + iconOver: icon {{ "menu/unpin", boxTitleCloseFgOver }}; +} infoLayerTopBar: InfoTopBar(infoTopBar) { height: infoLayerTopBarHeight; back: infoLayerTopBarBack; @@ -282,6 +302,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { mediaDelete: infoLayerTopBarDelete; storiesSave: infoLayerTopBarSaveStories; storiesArchive: infoLayerTopBarArchiveStories; + storiesPin: infoLayerTopBarPinStories; + storiesUnpin: infoLayerTopBarUnpinStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; radius: boxRadius; diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 2caa1ffa8..0a59d52f2 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -393,6 +393,8 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) { right += _delete->width(); } if (_canToggleStoryPin) { + _toggleStoryInProfile->moveToRight(right, 0, newWidth); + right += _toggleStoryInProfile->width(); _toggleStoryPin->moveToRight(right, 0, newWidth); right += _toggleStoryPin->width(); } @@ -609,14 +611,23 @@ rpl::producer TopBar::selectionActionRequests() const { } void TopBar::updateSelectionState() { - Expects(_selectionText && _delete && _forward && _toggleStoryPin); + Expects(_selectionText + && _delete + && _forward + && _toggleStoryInProfile + && _toggleStoryPin); _canDelete = computeCanDelete(); _canForward = computeCanForward(); + _canUnpinStories = computeCanUnpinStories(); _selectionText->entity()->setValue(generateSelectedText()); _delete->toggle(_canDelete, anim::type::instant); _forward->toggle(_canForward, anim::type::instant); + _toggleStoryInProfile->toggle(_canToggleStoryPin, anim::type::instant); _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); + _toggleStoryPin->entity()->setIconOverride( + _canUnpinStories ? &_st.storiesUnpin.icon : nullptr, + _canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr); updateSelectionControlsGeometry(width()); } @@ -631,6 +642,7 @@ void TopBar::createSelectionControls() { }; _canDelete = computeCanDelete(); _canForward = computeCanForward(); + _canUnpinStories = computeCanUnpinStories(); _canToggleStoryPin = computeCanToggleStoryPin(); _cancelSelection = wrap(Ui::CreateChild>( this, @@ -668,6 +680,7 @@ void TopBar::createSelectionControls() { _selectionActionRequests, _cancelSelection->lifetime()); _forward->entity()->setVisible(_canForward); + _delete = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaDelete), @@ -683,13 +696,38 @@ void TopBar::createSelectionControls() { _selectionActionRequests, _cancelSelection->lifetime()); _delete->entity()->setVisible(_canDelete); - const auto archive = _toggleStoryPin = wrap( + + _toggleStoryInProfile = wrap( Ui::CreateChild>( this, object_ptr( this, _storiesArchive ? _st.storiesSave : _st.storiesArchive), st::infoTopBarScale)); + registerToggleControlCallback( + _toggleStoryInProfile.data(), + [this] { return selectionMode() && _canToggleStoryPin; }); + _toggleStoryInProfile->setDuration(st::infoTopBarDuration); + _toggleStoryInProfile->entity()->clicks( + ) | rpl::map_to( + SelectionAction::ToggleStoryInProfile + ) | rpl::start_to_stream( + _selectionActionRequests, + _cancelSelection->lifetime()); + _toggleStoryInProfile->entity()->setVisible(_canToggleStoryPin); + + _toggleStoryPin = wrap( + Ui::CreateChild>( + this, + object_ptr( + this, + _st.storiesPin), + st::infoTopBarScale)); + if (_canUnpinStories) { + _toggleStoryPin->entity()->setIconOverride( + _canUnpinStories ? &_st.storiesUnpin.icon : nullptr, + _canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr); + } registerToggleControlCallback( _toggleStoryPin.data(), [this] { return selectionMode() && _canToggleStoryPin; }); @@ -713,6 +751,10 @@ bool TopBar::computeCanForward() const { return ranges::all_of(_selectedItems.list, &SelectedItem::canForward); } +bool TopBar::computeCanUnpinStories() const { + return ranges::any_of(_selectedItems.list, &SelectedItem::canUnpinStory); +} + bool TopBar::computeCanToggleStoryPin() const { return ranges::all_of( _selectedItems.list, diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h index 9e6ad80a8..2aa1093de 100644 --- a/Telegram/SourceFiles/info/info_top_bar.h +++ b/Telegram/SourceFiles/info/info_top_bar.h @@ -127,6 +127,7 @@ private: [[nodiscard]] Ui::StringWithNumbers generateSelectedText() const; [[nodiscard]] bool computeCanDelete() const; [[nodiscard]] bool computeCanForward() const; + [[nodiscard]] bool computeCanUnpinStories() const; [[nodiscard]] bool computeCanToggleStoryPin() const; void updateSelectionState(); void createSelectionControls(); @@ -174,11 +175,13 @@ private: bool _canDelete = false; bool _canForward = false; bool _canToggleStoryPin = false; + bool _canUnpinStories = false; bool _storiesArchive = false; QPointer> _cancelSelection; QPointer> _selectionText; QPointer> _forward; QPointer> _delete; + QPointer> _toggleStoryInProfile; QPointer> _toggleStoryPin; rpl::event_stream _selectionActionRequests; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index db76d4b43..5feb502f7 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -59,6 +59,7 @@ struct SelectedItem { bool canDelete = false; bool canForward = false; bool canToggleStoryPin = false; + bool canUnpinStory = false; }; struct SelectedItems { @@ -74,6 +75,7 @@ enum class SelectionAction { Forward, Delete, ToggleStoryPin, + ToggleStoryInProfile, }; class WrapWidget final : public Window::SectionWidget { diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index 27e8d4b9f..e25c860d8 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -31,6 +31,7 @@ struct ListItemSelectionData { bool canDelete = false; bool canForward = false; bool canToggleStoryPin = false; + bool canUnpinStory = false; friend inline bool operator==( ListItemSelectionData, diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index be09febef..8e9006629 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -261,6 +261,9 @@ void ListWidget::selectionAction(SelectionAction action) { case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Delete: deleteSelected(); return; + case SelectionAction::ToggleStoryInProfile: + toggleStoryInProfileSelected(); + return; case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; } } @@ -340,6 +343,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { result.canDelete = selection.canDelete; result.canForward = selection.canForward; result.canToggleStoryPin = selection.canToggleStoryPin; + result.canUnpinStory = selection.canUnpinStory; return result; }; auto transformation = [&](const auto &item) { @@ -908,21 +912,26 @@ void ListWidget::showContextMenu( } } - auto canDeleteAll = [&] { + const auto canDeleteAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canDelete; }); }; - auto canForwardAll = [&] { + const auto canForwardAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canForward; }) && (!_controller->key().storiesPeer() || _selected.size() == 1); }; - auto canToggleStoryPinAll = [&] { + const auto canToggleStoryPinAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canToggleStoryPin; }); }; + const auto canUnpinStoryAll = [&] { + return ranges::any_of(_selected, [](auto &&item) { + return item.second.canUnpinStory; + }); + }; auto link = ClickHandler::getActive(); @@ -1024,15 +1033,26 @@ void ListWidget::showContextMenu( if (overSelected == SelectionState::OverSelectedItems) { if (canToggleStoryPinAll()) { const auto tab = _controller->key().storiesTab(); - const auto pin = (tab == Stories::Tab::Archive); + const auto toProfile = (tab == Stories::Tab::Archive); _contextMenu->addAction( - (pin + (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_archived_add)(tr::now), - crl::guard(this, [this] { toggleStoryPinSelected(); }), - (pin + crl::guard(this, [this] { toggleStoryInProfileSelected(); }), + (toProfile ? &st::menuIconStoriesSave : &st::menuIconStoriesArchive)); + if (!toProfile) { + const auto unpin = canUnpinStoryAll(); + _contextMenu->addAction( + (unpin + ? tr::lng_context_unpin_from_top + : tr::lng_context_pin_to_top)(tr::now), + crl::guard( + this, + [this] { toggleStoryPinSelected(); }), + (unpin ? &st::menuIconUnpin : &st::menuIconPin)); + } } if (canForwardAll()) { _contextMenu->addAction( @@ -1065,17 +1085,28 @@ void ListWidget::showContextMenu( FullSelection); if (selectionData.canToggleStoryPin) { const auto tab = _controller->key().storiesTab(); - const auto pin = (tab == Stories::Tab::Archive); + const auto toProfile = (tab == Stories::Tab::Archive); _contextMenu->addAction( - (pin + (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_mediaview_archive_story)(tr::now), crl::guard(this, [=] { - toggleStoryPin({ 1, globalId.itemId }); + toggleStoryInProfile({ 1, globalId.itemId }); }), - (pin + (toProfile ? &st::menuIconStoriesSave : &st::menuIconStoriesArchive)); + if (!toProfile) { + const auto unpin = selectionData.canUnpinStory; + _contextMenu->addAction( + (unpin + ? tr::lng_context_unpin_from_top + : tr::lng_context_pin_to_top)(tr::now), + crl::guard(this, [=] { toggleStoryPin( + { 1, globalId.itemId }, + !unpin); }), + (unpin ? &st::menuIconUnpin : &st::menuIconPin)); + } } if (selectionData.canForward) { _contextMenu->addAction( @@ -1193,13 +1224,23 @@ void ListWidget::deleteSelected() { })); } -void ListWidget::toggleStoryPinSelected() { - toggleStoryPin(collectSelectedIds(), crl::guard(this, [=] { +void ListWidget::toggleStoryInProfileSelected() { + toggleStoryInProfile(collectSelectedIds(), crl::guard(this, [=] { clearSelected(); })); } -void ListWidget::toggleStoryPin( +void ListWidget::toggleStoryPinSelected() { + const auto items = collectSelectedItems(); + const auto pin = ranges::none_of( + items.list, + &SelectedItem::canUnpinStory); + toggleStoryPin(collectSelectedIds(items), pin, crl::guard(this, [=] { + clearSelected(); + })); +} + +void ListWidget::toggleStoryInProfile( MessageIdsList &&items, Fn confirmed) { auto list = std::vector(); @@ -1250,6 +1291,37 @@ void ListWidget::toggleStoryPin( })); } +void ListWidget::toggleStoryPin( + MessageIdsList &&items, + bool pin, + Fn confirmed) { + auto list = std::vector(); + for (const auto &id : items) { + if (IsStoryMsgId(id.msg)) { + list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); + } + } + if (list.empty()) { + return; + } + const auto channel = peerIsChannel(list.front().peer); + const auto count = int(list.size()); + const auto controller = _controller; + const auto stories = &controller->session().data().stories(); + if (stories->canTogglePinnedList(list, pin)) { + using namespace ::Media::Stories; + stories->togglePinnedList(list, pin); + controller->showToast(PrepareTogglePinToast(channel, count, pin)); + if (confirmed) { + confirmed(); + } + } else { + const auto limit = stories->maxPinnedCount(); + controller->showToast( + tr::lng_mediaview_pin_limit(tr::now, lt_count, limit)); + } +} + void ListWidget::deleteItem(GlobalMsgId globalId) { if (const auto item = MessageByGlobalId(globalId)) { auto items = SelectedItems(_provider->type()); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 10984f27e..5b486c1de 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -190,10 +190,15 @@ private: void forwardItems(MessageIdsList &&items); void deleteSelected(); void toggleStoryPinSelected(); + void toggleStoryInProfileSelected(); void deleteItem(GlobalMsgId globalId); void deleteItems(SelectedItems &&items, Fn confirmed = nullptr); + void toggleStoryInProfile( + MessageIdsList &&items, + Fn confirmed = nullptr); void toggleStoryPin( MessageIdsList &&items, + bool pin, Fn confirmed = nullptr); void applyItemSelection( HistoryItem *item, diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index ba82c297f..c3025bf85 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -189,9 +189,22 @@ void Provider::refreshViewer() { return; } _slice = std::move(slice); - if (const auto nearest = _slice.nearest(idForViewer)) { - _aroundId = *nearest; + + auto nearestId = std::optional(); + for (auto i = 0; i != _slice.size(); ++i) { + if (!nearestId + || std::abs(*nearestId - idForViewer) + > std::abs(_slice[i] - idForViewer)) { + nearestId = _slice[i]; + } } + if (nearestId) { + _aroundId = *nearestId; + } + + //if (const auto nearest = _slice.nearest(idForViewer)) { + // _aroundId = *nearest; + //} _refreshed.fire({}); }, _viewerLifetime); } @@ -208,8 +221,8 @@ std::vector Provider::fillSections( 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]; + for (auto i = 0; i != count; ++i) { + const auto storyId = _slice[i]; if (const auto layout = getLayout(storyId, delegate)) { if (!section.addItem(layout)) { section.finishSection(); @@ -361,6 +374,7 @@ ListItemSelectionData Provider::computeSelectionData( const auto story = *maybeStory; result.canForward = peer->isSelf() && story->canShare(); result.canDelete = story->canDelete(); + result.canUnpinStory = story->pinnedToTop(); } result.canToggleStoryPin = peer->isSelf() || (channel && channel->canEditStories()); @@ -417,12 +431,28 @@ int64 Provider::scrollTopStatePosition(not_null item) { HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) { if (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) { return state.item; - } else if (const auto id = _slice.nearest(state.position)) { - const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id)); + //} else if (const auto id = _slice.nearest(state.position)) { + // const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id)); + // if (const auto item = _controller->session().data().message(full)) { + // return item; + // } + } + + auto nearestId = std::optional(); + for (auto i = 0; i != _slice.size(); ++i) { + if (!nearestId + || std::abs(*nearestId - state.position) + > std::abs(_slice[i] - state.position)) { + nearestId = _slice[i]; + } + } + if (nearestId) { + const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*nearestId)); if (const auto item = _controller->session().data().message(full)) { return item; } } + return state.item; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 10490e2aa..2b06956bd 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -80,13 +80,18 @@ struct SameDayRange { int index) { Expects(index >= 0 && index < ids.list.size()); + const auto pinned = int(ids.pinnedToTop.size()); + if (index < pinned) { + return SameDayRange{ .from = 0, .till = pinned - 1 }; + } + auto result = SameDayRange{ .from = index, .till = index }; const auto peerId = story->peer()->id; const auto stories = &story->owner().stories(); const auto now = base::unixtime::parse(story->date()); - const auto b = begin(ids.list); - for (auto i = b + index; i != b;) { - if (const auto maybeStory = stories->lookup({ peerId, *--i })) { + for (auto i = index; i != 0;) { + const auto storyId = IdRespectingPinned(ids, --i); + if (const auto maybeStory = stories->lookup({ peerId, storyId })) { const auto day = base::unixtime::parse((*maybeStory)->date()); if (day.date() != now.date()) { break; @@ -94,8 +99,9 @@ struct SameDayRange { } --result.from; } - for (auto i = b + index + 1, e = end(ids.list); i != e; ++i) { - if (const auto maybeStory = stories->lookup({ peerId, *i })) { + for (auto i = index + 1, c = int(ids.list.size()); i != c; ++i) { + const auto storyId = IdRespectingPinned(ids, i); + if (const auto maybeStory = stories->lookup({ peerId, storyId })) { const auto day = base::unixtime::parse((*maybeStory)->date()); if (day.date() != now.date()) { break; @@ -694,17 +700,16 @@ void Controller::rebuildFromContext( }, [&](StoriesContextSaved) { if (stories.savedCountKnown(peerId)) { const auto &saved = stories.saved(peerId); - const auto &ids = saved.list; - const auto i = ids.find(id); - if (i != end(ids)) { + const auto i = IndexRespectingPinned(saved, id); + if (i < saved.list.size()) { list = StoriesList{ .peer = peer, .ids = saved, .total = stories.savedCount(peerId), }; - _index = int(i - begin(ids)); - if (ids.size() < list->total - && (end(ids) - i) < kPreloadStoriesCount) { + _index = i; + if (saved.list.size() < list->total + && (saved.list.size() - i) < kPreloadStoriesCount) { stories.savedLoadMore(peerId); } } @@ -713,17 +718,16 @@ void Controller::rebuildFromContext( }, [&](StoriesContextArchive) { if (stories.archiveCountKnown(peerId)) { const auto &archive = stories.archive(peerId); - const auto &ids = archive.list; - const auto i = ids.find(id); - if (i != end(ids)) { + const auto i = IndexRespectingPinned(archive, id); + if (i < archive.list.size()) { list = StoriesList{ .peer = peer, .ids = archive, .total = stories.archiveCount(peerId), }; - _index = int(i - begin(ids)); - if (ids.size() < list->total - && (end(ids) - i) < kPreloadStoriesCount) { + _index = i; + if (archive.list.size() < list->total + && (archive.list.size() - i) < kPreloadStoriesCount) { stories.archiveLoadMore(peerId); } } @@ -1520,7 +1524,7 @@ StoryId Controller::shownId(int index) const { return _source ? (_source->ids.begin() + index)->id : (index < int(_list->ids.list.size())) - ? *(_list->ids.list.begin() + index) + ? IdRespectingPinned(_list->ids, index) : StoryId(); } @@ -1801,6 +1805,39 @@ Ui::Toast::Config PrepareToggleInProfileToast( }; } +Ui::Toast::Config PrepareTogglePinToast( + bool channel, + int count, + bool pin) { + return { + .title = (pin + ? (count == 1 + ? tr::lng_mediaview_pin_story_done(tr::now) + : tr::lng_mediaview_pin_stories_done( + tr::now, + lt_count, + count)) + : QString()), + .text = (pin + ? (count == 1 + ? tr::lng_mediaview_pin_story_about(tr::now) + : tr::lng_mediaview_pin_stories_about( + tr::now, + lt_count, + count)) + : (count == 1 + ? tr::lng_mediaview_unpin_story_done(tr::now) + : tr::lng_mediaview_unpin_stories_done( + tr::now, + lt_count, + count))), + .st = &st::storiesActionToast, + .duration = (pin + ? Data::Stories::kInProfileToastDuration + : Ui::Toast::kDefaultDuration), + }; +} + void ReportRequested( std::shared_ptr show, FullStoryId id, diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 2b5de5bc7..45590e0a2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -332,6 +332,10 @@ private: bool channel, int count, bool inProfile); +[[nodiscard]] Ui::Toast::Config PrepareTogglePinToast( + bool channel, + int count, + bool pin); void ReportRequested( std::shared_ptr show, FullStoryId id,