Implement stories pin-to-top.

This commit is contained in:
John Preston 2024-04-16 19:32:33 +04:00
parent 4b98ab1246
commit 468d8b04d6
17 changed files with 464 additions and 57 deletions

View file

@ -3424,6 +3424,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_forward" = "Forward"; "lng_mediaview_forward" = "Forward";
"lng_mediaview_delete" = "Delete"; "lng_mediaview_delete" = "Delete";
"lng_mediaview_save_to_profile" = "Save to Profile"; "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_archive_story" = "Archive Story";
"lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_photos_all" = "View all photos";
"lng_mediaview_files_all" = "View all files"; "lng_mediaview_files_all" = "View all files";

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "ui/layers/show.h" #include "ui/layers/show.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
@ -77,6 +78,47 @@ using UpdateFlag = StoryUpdate::Flag;
} // namespace } // 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 { StoriesSourceInfo StoriesSource::info() const {
return { return {
.id = peer->id, .id = peer->id,
@ -1674,6 +1716,10 @@ void Stories::savedLoadMore(PeerId peerId) {
const auto &data = result.data(); const auto &data = result.data();
const auto now = base::unixtime::now(); 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; saved.total = data.vcount().v;
for (const auto &story : data.vstories().v) { for (const auto &story : data.vstories().v) {
const auto id = story.match([&](const auto &id) { 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()); const auto ids = int(saved.ids.list.size());
saved.loaded = data.vstories().v.empty(); saved.loaded = data.vstories().v.empty();
saved.total = saved.loaded ? ids : std::max(saved.total, ids); saved.total = saved.loaded ? ids : std::max(saved.total, ids);
setPinnedToTop(peerId, std::move(pinnedToTop));
_savedChanged.fire_copy(peerId); _savedChanged.fire_copy(peerId);
}).fail([=] { }).fail([=] {
auto &saved = _saved[peerId]; auto &saved = _saved[peerId];
@ -1701,6 +1748,33 @@ void Stories::savedLoadMore(PeerId peerId) {
}).send(); }).send();
} }
void Stories::setPinnedToTop(
PeerId peerId,
std::vector<StoryId> &&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<FullStoryId> &ids) { void Stories::deleteList(const std::vector<FullStoryId> &ids) {
if (ids.empty()) { if (ids.empty()) {
return; return;
@ -1788,6 +1862,75 @@ void Stories::toggleInProfileList(
}).send(); }).send();
} }
bool Stories::canTogglePinnedList(
const std::vector<FullStoryId> &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<int>(u"stories_pinned_to_top_count_max"_q, 3);
}
void Stories::togglePinnedList(
const std::vector<FullStoryId> &ids,
bool pin) {
if (ids.empty()) {
return;
}
const auto peerId = ids.front().peer;
auto &saved = _saved[peerId];
auto list = QVector<MTPint>();
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<MTPint>(list)
)).done([=] {
setPinnedToTop(peerId, list
| ranges::views::transform(&MTPint::v)
| ranges::to_vector);
_savedChanged.fire_copy(peerId);
}).send();
}
void Stories::report( void Stories::report(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
FullStoryId id, FullStoryId id,

View file

@ -33,12 +33,17 @@ class StoryPreload;
struct StoriesIds { struct StoriesIds {
base::flat_set<StoryId, std::greater<>> list; base::flat_set<StoryId, std::greater<>> list;
std::vector<StoryId> pinnedToTop;
friend inline bool operator==( friend inline bool operator==(
const StoriesIds&, const StoriesIds&,
const StoriesIds&) = default; 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 { struct StoriesSourceInfo {
PeerId id = 0; PeerId id = 0;
TimeId last = 0; TimeId last = 0;
@ -208,6 +213,11 @@ public:
void toggleInProfileList( void toggleInProfileList(
const std::vector<FullStoryId> &ids, const std::vector<FullStoryId> &ids,
bool inProfile); bool inProfile);
[[nodiscard]] bool canTogglePinnedList(
const std::vector<FullStoryId> &ids,
bool pin) const;
[[nodiscard]] int maxPinnedCount() const;
void togglePinnedList(const std::vector<FullStoryId> &ids, bool pin);
void report( void report(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
FullStoryId id, FullStoryId id,
@ -314,6 +324,9 @@ private:
void notifySourcesChanged(StorySourcesList list); void notifySourcesChanged(StorySourcesList list);
void pushHiddenCountsToFolder(); void pushHiddenCountsToFolder();
void setPinnedToTop(
PeerId peerId,
std::vector<StoryId> &&pinnedToTop);
[[nodiscard]] int pollingInterval( [[nodiscard]] int pollingInterval(
const PollingSettings &settings) const; const PollingSettings &settings) const;

View file

@ -40,18 +40,23 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
const auto &saved = stories->saved(peerId); const auto &saved = stories->saved(peerId);
const auto count = stories->savedCount(peerId); const auto count = stories->savedCount(peerId);
const auto around = saved.list.lower_bound(aroundId); auto aroundIndex = IndexRespectingPinned(saved, aroundId);
const auto hasBefore = int(around - begin(saved.list)); if (aroundIndex == int(saved.list.size())) {
const auto hasAfter = int(end(saved.list) - around); 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) { if (hasAfter < limit) {
stories->savedLoadMore(peerId); stories->savedLoadMore(peerId);
} }
const auto takeBefore = std::min(hasBefore, limit); const auto takeBefore = std::min(hasBefore, limit);
const auto takeAfter = std::min(hasAfter, limit); const auto takeAfter = std::min(hasAfter, limit);
auto ids = base::flat_set<StoryId>{ auto ids = std::vector<StoryId>();
std::make_reverse_iterator(around + takeAfter), ids.reserve(takeBefore + takeAfter);
std::make_reverse_iterator(around - takeBefore) for (auto i = aroundIndex - takeBefore; i != aroundIndex + takeAfter; ++i) {
}; ids.push_back(IdRespectingPinned(saved, i));
}
const auto added = int(ids.size()); const auto added = int(ids.size());
state->slice = StoriesIdsSlice( state->slice = StoriesIdsSlice(
std::move(ids), std::move(ids),
@ -114,18 +119,23 @@ rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
const auto &archive = stories->archive(peerId); const auto &archive = stories->archive(peerId);
const auto count = stories->archiveCount(peerId); const auto count = stories->archiveCount(peerId);
const auto i = archive.list.lower_bound(aroundId); auto aroundIndex = IndexRespectingPinned(archive, aroundId);
const auto hasBefore = int(i - begin(archive.list)); if (aroundIndex == int(archive.list.size())) {
const auto hasAfter = int(end(archive.list) - i); 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) { if (hasAfter < limit) {
stories->archiveLoadMore(peerId); stories->archiveLoadMore(peerId);
} }
const auto takeBefore = std::min(hasBefore, limit); const auto takeBefore = std::min(hasBefore, limit);
const auto takeAfter = std::min(hasAfter, limit); const auto takeAfter = std::min(hasAfter, limit);
auto ids = base::flat_set<StoryId>{ auto ids = std::vector<StoryId>();
std::make_reverse_iterator(i + takeAfter), ids.reserve(takeBefore + takeAfter);
std::make_reverse_iterator(i - takeBefore) for (auto i = aroundIndex - takeBefore; i != aroundIndex + takeAfter; ++i) {
}; ids.push_back(IdRespectingPinned(archive, i));
}
const auto added = int(ids.size()); const auto added = int(ids.size());
state->slice = StoriesIdsSlice( state->slice = StoriesIdsSlice(
std::move(ids), std::move(ids),

View file

@ -17,7 +17,7 @@ class Session;
namespace Data { namespace Data {
using StoriesIdsSlice = AbstractSparseIds<base::flat_set<StoryId>>; using StoriesIdsSlice = AbstractSparseIds<std::vector<StoryId>>;
[[nodiscard]] rpl::producer<StoriesIdsSlice> SavedStoriesIds( [[nodiscard]] rpl::producer<StoriesIdsSlice> SavedStoriesIds(
not_null<PeerData*> peer, not_null<PeerData*> peer,

View file

@ -389,6 +389,14 @@ TextWithEntities Story::inReplyText() const {
Ui::Text::WithEntities); Ui::Text::WithEntities);
} }
void Story::setPinnedToTop(bool pinned) {
_pinnedToTop = pinned;
}
bool Story::pinnedToTop() const {
return _pinnedToTop;
}
void Story::setInProfile(bool value) { void Story::setInProfile(bool value) {
_inProfile = value; _inProfile = value;
} }
@ -431,8 +439,8 @@ bool Story::canDownloadChecked() const {
} }
bool Story::canShare() const { bool Story::canShare() const {
return _privacyPublic return _privacyPublic
&& !forbidsForward() && !forbidsForward()
&& (inProfile() || !expired()); && (inProfile() || !expired());
} }

View file

@ -153,6 +153,9 @@ public:
[[nodiscard]] Image *replyPreview() const; [[nodiscard]] Image *replyPreview() const;
[[nodiscard]] TextWithEntities inReplyText() const; [[nodiscard]] TextWithEntities inReplyText() const;
void setPinnedToTop(bool pinned);
bool pinnedToTop() const;
void setInProfile(bool value); void setInProfile(bool value);
[[nodiscard]] bool inProfile() const; [[nodiscard]] bool inProfile() const;
[[nodiscard]] StoryPrivacy privacy() const; [[nodiscard]] StoryPrivacy privacy() const;
@ -251,6 +254,7 @@ private:
TimeId _lastUpdateTime = 0; TimeId _lastUpdateTime = 0;
bool _out : 1 = false; bool _out : 1 = false;
bool _inProfile : 1 = false; bool _inProfile : 1 = false;
bool _pinnedToTop : 1 = false;
bool _privacyPublic : 1 = false; bool _privacyPublic : 1 = false;
bool _privacyCloseFriends : 1 = false; bool _privacyCloseFriends : 1 = false;
bool _privacyContacts : 1 = false; bool _privacyContacts : 1 = false;

View file

@ -44,6 +44,8 @@ InfoTopBar {
mediaDelete: IconButton; mediaDelete: IconButton;
storiesSave: IconButton; storiesSave: IconButton;
storiesArchive: IconButton; storiesArchive: IconButton;
storiesPin: IconButton;
storiesUnpin: IconButton;
search: IconButton; search: IconButton;
searchRow: SearchFieldRow; searchRow: SearchFieldRow;
highlightBg: color; highlightBg: color;
@ -185,6 +187,14 @@ infoTopBarArchiveStories: IconButton(infoTopBarForward) {
icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }};
iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; 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 { infoTopBar: InfoTopBar {
height: infoTopBarHeight; height: infoTopBarHeight;
back: infoTopBarBack; back: infoTopBarBack;
@ -205,6 +215,8 @@ infoTopBar: InfoTopBar {
mediaDelete: infoTopBarDelete; mediaDelete: infoTopBarDelete;
storiesSave: infoTopBarSaveStories; storiesSave: infoTopBarSaveStories;
storiesArchive: infoTopBarArchiveStories; storiesArchive: infoTopBarArchiveStories;
storiesPin: infoTopBarPinStories;
storiesUnpin: infoTopBarUnpinStories;
search: infoTopBarSearch; search: infoTopBarSearch;
searchRow: infoTopBarSearchRow; searchRow: infoTopBarSearchRow;
highlightBg: windowBgOver; highlightBg: windowBgOver;
@ -268,6 +280,14 @@ infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) {
icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }};
iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; 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) { infoLayerTopBar: InfoTopBar(infoTopBar) {
height: infoLayerTopBarHeight; height: infoLayerTopBarHeight;
back: infoLayerTopBarBack; back: infoLayerTopBarBack;
@ -282,6 +302,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) {
mediaDelete: infoLayerTopBarDelete; mediaDelete: infoLayerTopBarDelete;
storiesSave: infoLayerTopBarSaveStories; storiesSave: infoLayerTopBarSaveStories;
storiesArchive: infoLayerTopBarArchiveStories; storiesArchive: infoLayerTopBarArchiveStories;
storiesPin: infoLayerTopBarPinStories;
storiesUnpin: infoLayerTopBarUnpinStories;
search: infoTopBarSearch; search: infoTopBarSearch;
searchRow: infoTopBarSearchRow; searchRow: infoTopBarSearchRow;
radius: boxRadius; radius: boxRadius;

View file

@ -393,6 +393,8 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) {
right += _delete->width(); right += _delete->width();
} }
if (_canToggleStoryPin) { if (_canToggleStoryPin) {
_toggleStoryInProfile->moveToRight(right, 0, newWidth);
right += _toggleStoryInProfile->width();
_toggleStoryPin->moveToRight(right, 0, newWidth); _toggleStoryPin->moveToRight(right, 0, newWidth);
right += _toggleStoryPin->width(); right += _toggleStoryPin->width();
} }
@ -609,14 +611,23 @@ rpl::producer<SelectionAction> TopBar::selectionActionRequests() const {
} }
void TopBar::updateSelectionState() { void TopBar::updateSelectionState() {
Expects(_selectionText && _delete && _forward && _toggleStoryPin); Expects(_selectionText
&& _delete
&& _forward
&& _toggleStoryInProfile
&& _toggleStoryPin);
_canDelete = computeCanDelete(); _canDelete = computeCanDelete();
_canForward = computeCanForward(); _canForward = computeCanForward();
_canUnpinStories = computeCanUnpinStories();
_selectionText->entity()->setValue(generateSelectedText()); _selectionText->entity()->setValue(generateSelectedText());
_delete->toggle(_canDelete, anim::type::instant); _delete->toggle(_canDelete, anim::type::instant);
_forward->toggle(_canForward, anim::type::instant); _forward->toggle(_canForward, anim::type::instant);
_toggleStoryInProfile->toggle(_canToggleStoryPin, anim::type::instant);
_toggleStoryPin->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()); updateSelectionControlsGeometry(width());
} }
@ -631,6 +642,7 @@ void TopBar::createSelectionControls() {
}; };
_canDelete = computeCanDelete(); _canDelete = computeCanDelete();
_canForward = computeCanForward(); _canForward = computeCanForward();
_canUnpinStories = computeCanUnpinStories();
_canToggleStoryPin = computeCanToggleStoryPin(); _canToggleStoryPin = computeCanToggleStoryPin();
_cancelSelection = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>( _cancelSelection = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this, this,
@ -668,6 +680,7 @@ void TopBar::createSelectionControls() {
_selectionActionRequests, _selectionActionRequests,
_cancelSelection->lifetime()); _cancelSelection->lifetime());
_forward->entity()->setVisible(_canForward); _forward->entity()->setVisible(_canForward);
_delete = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>( _delete = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this, this,
object_ptr<Ui::IconButton>(this, _st.mediaDelete), object_ptr<Ui::IconButton>(this, _st.mediaDelete),
@ -683,13 +696,38 @@ void TopBar::createSelectionControls() {
_selectionActionRequests, _selectionActionRequests,
_cancelSelection->lifetime()); _cancelSelection->lifetime());
_delete->entity()->setVisible(_canDelete); _delete->entity()->setVisible(_canDelete);
const auto archive = _toggleStoryPin = wrap(
_toggleStoryInProfile = wrap(
Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>( Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this, this,
object_ptr<Ui::IconButton>( object_ptr<Ui::IconButton>(
this, this,
_storiesArchive ? _st.storiesSave : _st.storiesArchive), _storiesArchive ? _st.storiesSave : _st.storiesArchive),
st::infoTopBarScale)); 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<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(
this,
_st.storiesPin),
st::infoTopBarScale));
if (_canUnpinStories) {
_toggleStoryPin->entity()->setIconOverride(
_canUnpinStories ? &_st.storiesUnpin.icon : nullptr,
_canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr);
}
registerToggleControlCallback( registerToggleControlCallback(
_toggleStoryPin.data(), _toggleStoryPin.data(),
[this] { return selectionMode() && _canToggleStoryPin; }); [this] { return selectionMode() && _canToggleStoryPin; });
@ -713,6 +751,10 @@ bool TopBar::computeCanForward() const {
return ranges::all_of(_selectedItems.list, &SelectedItem::canForward); return ranges::all_of(_selectedItems.list, &SelectedItem::canForward);
} }
bool TopBar::computeCanUnpinStories() const {
return ranges::any_of(_selectedItems.list, &SelectedItem::canUnpinStory);
}
bool TopBar::computeCanToggleStoryPin() const { bool TopBar::computeCanToggleStoryPin() const {
return ranges::all_of( return ranges::all_of(
_selectedItems.list, _selectedItems.list,

View file

@ -127,6 +127,7 @@ private:
[[nodiscard]] Ui::StringWithNumbers generateSelectedText() const; [[nodiscard]] Ui::StringWithNumbers generateSelectedText() const;
[[nodiscard]] bool computeCanDelete() const; [[nodiscard]] bool computeCanDelete() const;
[[nodiscard]] bool computeCanForward() const; [[nodiscard]] bool computeCanForward() const;
[[nodiscard]] bool computeCanUnpinStories() const;
[[nodiscard]] bool computeCanToggleStoryPin() const; [[nodiscard]] bool computeCanToggleStoryPin() const;
void updateSelectionState(); void updateSelectionState();
void createSelectionControls(); void createSelectionControls();
@ -174,11 +175,13 @@ private:
bool _canDelete = false; bool _canDelete = false;
bool _canForward = false; bool _canForward = false;
bool _canToggleStoryPin = false; bool _canToggleStoryPin = false;
bool _canUnpinStories = false;
bool _storiesArchive = false; bool _storiesArchive = false;
QPointer<Ui::FadeWrap<Ui::IconButton>> _cancelSelection; QPointer<Ui::FadeWrap<Ui::IconButton>> _cancelSelection;
QPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText; QPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText;
QPointer<Ui::FadeWrap<Ui::IconButton>> _forward; QPointer<Ui::FadeWrap<Ui::IconButton>> _forward;
QPointer<Ui::FadeWrap<Ui::IconButton>> _delete; QPointer<Ui::FadeWrap<Ui::IconButton>> _delete;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryInProfile;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin; QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;
rpl::event_stream<SelectionAction> _selectionActionRequests; rpl::event_stream<SelectionAction> _selectionActionRequests;

View file

@ -59,6 +59,7 @@ struct SelectedItem {
bool canDelete = false; bool canDelete = false;
bool canForward = false; bool canForward = false;
bool canToggleStoryPin = false; bool canToggleStoryPin = false;
bool canUnpinStory = false;
}; };
struct SelectedItems { struct SelectedItems {
@ -74,6 +75,7 @@ enum class SelectionAction {
Forward, Forward,
Delete, Delete,
ToggleStoryPin, ToggleStoryPin,
ToggleStoryInProfile,
}; };
class WrapWidget final : public Window::SectionWidget { class WrapWidget final : public Window::SectionWidget {

View file

@ -31,6 +31,7 @@ struct ListItemSelectionData {
bool canDelete = false; bool canDelete = false;
bool canForward = false; bool canForward = false;
bool canToggleStoryPin = false; bool canToggleStoryPin = false;
bool canUnpinStory = false;
friend inline bool operator==( friend inline bool operator==(
ListItemSelectionData, ListItemSelectionData,

View file

@ -261,6 +261,9 @@ void ListWidget::selectionAction(SelectionAction action) {
case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Clear: clearSelected(); return;
case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Forward: forwardSelected(); return;
case SelectionAction::Delete: deleteSelected(); return; case SelectionAction::Delete: deleteSelected(); return;
case SelectionAction::ToggleStoryInProfile:
toggleStoryInProfileSelected();
return;
case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return;
} }
} }
@ -340,6 +343,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems {
result.canDelete = selection.canDelete; result.canDelete = selection.canDelete;
result.canForward = selection.canForward; result.canForward = selection.canForward;
result.canToggleStoryPin = selection.canToggleStoryPin; result.canToggleStoryPin = selection.canToggleStoryPin;
result.canUnpinStory = selection.canUnpinStory;
return result; return result;
}; };
auto transformation = [&](const auto &item) { 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 ranges::none_of(_selected, [](auto &&item) {
return !item.second.canDelete; return !item.second.canDelete;
}); });
}; };
auto canForwardAll = [&] { const auto canForwardAll = [&] {
return ranges::none_of(_selected, [](auto &&item) { return ranges::none_of(_selected, [](auto &&item) {
return !item.second.canForward; return !item.second.canForward;
}) && (!_controller->key().storiesPeer() || _selected.size() == 1); }) && (!_controller->key().storiesPeer() || _selected.size() == 1);
}; };
auto canToggleStoryPinAll = [&] { const auto canToggleStoryPinAll = [&] {
return ranges::none_of(_selected, [](auto &&item) { return ranges::none_of(_selected, [](auto &&item) {
return !item.second.canToggleStoryPin; return !item.second.canToggleStoryPin;
}); });
}; };
const auto canUnpinStoryAll = [&] {
return ranges::any_of(_selected, [](auto &&item) {
return item.second.canUnpinStory;
});
};
auto link = ClickHandler::getActive(); auto link = ClickHandler::getActive();
@ -1024,15 +1033,26 @@ void ListWidget::showContextMenu(
if (overSelected == SelectionState::OverSelectedItems) { if (overSelected == SelectionState::OverSelectedItems) {
if (canToggleStoryPinAll()) { if (canToggleStoryPinAll()) {
const auto tab = _controller->key().storiesTab(); const auto tab = _controller->key().storiesTab();
const auto pin = (tab == Stories::Tab::Archive); const auto toProfile = (tab == Stories::Tab::Archive);
_contextMenu->addAction( _contextMenu->addAction(
(pin (toProfile
? tr::lng_mediaview_save_to_profile ? tr::lng_mediaview_save_to_profile
: tr::lng_archived_add)(tr::now), : tr::lng_archived_add)(tr::now),
crl::guard(this, [this] { toggleStoryPinSelected(); }), crl::guard(this, [this] { toggleStoryInProfileSelected(); }),
(pin (toProfile
? &st::menuIconStoriesSave ? &st::menuIconStoriesSave
: &st::menuIconStoriesArchive)); : &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()) { if (canForwardAll()) {
_contextMenu->addAction( _contextMenu->addAction(
@ -1065,17 +1085,28 @@ void ListWidget::showContextMenu(
FullSelection); FullSelection);
if (selectionData.canToggleStoryPin) { if (selectionData.canToggleStoryPin) {
const auto tab = _controller->key().storiesTab(); const auto tab = _controller->key().storiesTab();
const auto pin = (tab == Stories::Tab::Archive); const auto toProfile = (tab == Stories::Tab::Archive);
_contextMenu->addAction( _contextMenu->addAction(
(pin (toProfile
? tr::lng_mediaview_save_to_profile ? tr::lng_mediaview_save_to_profile
: tr::lng_mediaview_archive_story)(tr::now), : tr::lng_mediaview_archive_story)(tr::now),
crl::guard(this, [=] { crl::guard(this, [=] {
toggleStoryPin({ 1, globalId.itemId }); toggleStoryInProfile({ 1, globalId.itemId });
}), }),
(pin (toProfile
? &st::menuIconStoriesSave ? &st::menuIconStoriesSave
: &st::menuIconStoriesArchive)); : &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) { if (selectionData.canForward) {
_contextMenu->addAction( _contextMenu->addAction(
@ -1193,13 +1224,23 @@ void ListWidget::deleteSelected() {
})); }));
} }
void ListWidget::toggleStoryPinSelected() { void ListWidget::toggleStoryInProfileSelected() {
toggleStoryPin(collectSelectedIds(), crl::guard(this, [=] { toggleStoryInProfile(collectSelectedIds(), crl::guard(this, [=] {
clearSelected(); 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, MessageIdsList &&items,
Fn<void()> confirmed) { Fn<void()> confirmed) {
auto list = std::vector<FullStoryId>(); auto list = std::vector<FullStoryId>();
@ -1250,6 +1291,37 @@ void ListWidget::toggleStoryPin(
})); }));
} }
void ListWidget::toggleStoryPin(
MessageIdsList &&items,
bool pin,
Fn<void()> confirmed) {
auto list = std::vector<FullStoryId>();
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) { void ListWidget::deleteItem(GlobalMsgId globalId) {
if (const auto item = MessageByGlobalId(globalId)) { if (const auto item = MessageByGlobalId(globalId)) {
auto items = SelectedItems(_provider->type()); auto items = SelectedItems(_provider->type());

View file

@ -190,10 +190,15 @@ private:
void forwardItems(MessageIdsList &&items); void forwardItems(MessageIdsList &&items);
void deleteSelected(); void deleteSelected();
void toggleStoryPinSelected(); void toggleStoryPinSelected();
void toggleStoryInProfileSelected();
void deleteItem(GlobalMsgId globalId); void deleteItem(GlobalMsgId globalId);
void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr); void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);
void toggleStoryInProfile(
MessageIdsList &&items,
Fn<void()> confirmed = nullptr);
void toggleStoryPin( void toggleStoryPin(
MessageIdsList &&items, MessageIdsList &&items,
bool pin,
Fn<void()> confirmed = nullptr); Fn<void()> confirmed = nullptr);
void applyItemSelection( void applyItemSelection(
HistoryItem *item, HistoryItem *item,

View file

@ -189,9 +189,22 @@ void Provider::refreshViewer() {
return; return;
} }
_slice = std::move(slice); _slice = std::move(slice);
if (const auto nearest = _slice.nearest(idForViewer)) {
_aroundId = *nearest; auto nearestId = std::optional<StoryId>();
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({}); _refreshed.fire({});
}, _viewerLifetime); }, _viewerLifetime);
} }
@ -208,8 +221,8 @@ std::vector<ListSection> Provider::fillSections(
auto result = std::vector<ListSection>(); auto result = std::vector<ListSection>();
auto section = ListSection(Type::PhotoVideo, sectionDelegate()); auto section = ListSection(Type::PhotoVideo, sectionDelegate());
auto count = _slice.size(); auto count = _slice.size();
for (auto i = count; i != 0;) { for (auto i = 0; i != count; ++i) {
const auto storyId = _slice[--i]; const auto storyId = _slice[i];
if (const auto layout = getLayout(storyId, delegate)) { if (const auto layout = getLayout(storyId, delegate)) {
if (!section.addItem(layout)) { if (!section.addItem(layout)) {
section.finishSection(); section.finishSection();
@ -361,6 +374,7 @@ ListItemSelectionData Provider::computeSelectionData(
const auto story = *maybeStory; const auto story = *maybeStory;
result.canForward = peer->isSelf() && story->canShare(); result.canForward = peer->isSelf() && story->canShare();
result.canDelete = story->canDelete(); result.canDelete = story->canDelete();
result.canUnpinStory = story->pinnedToTop();
} }
result.canToggleStoryPin = peer->isSelf() result.canToggleStoryPin = peer->isSelf()
|| (channel && channel->canEditStories()); || (channel && channel->canEditStories());
@ -417,12 +431,28 @@ int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) { HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
if (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) { if (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) {
return state.item; return state.item;
} else if (const auto id = _slice.nearest(state.position)) { //} else if (const auto id = _slice.nearest(state.position)) {
const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id)); // const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id));
// if (const auto item = _controller->session().data().message(full)) {
// return item;
// }
}
auto nearestId = std::optional<StoryId>();
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)) { if (const auto item = _controller->session().data().message(full)) {
return item; return item;
} }
} }
return state.item; return state.item;
} }

View file

@ -80,13 +80,18 @@ struct SameDayRange {
int index) { int index) {
Expects(index >= 0 && index < ids.list.size()); 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 }; auto result = SameDayRange{ .from = index, .till = index };
const auto peerId = story->peer()->id; const auto peerId = story->peer()->id;
const auto stories = &story->owner().stories(); const auto stories = &story->owner().stories();
const auto now = base::unixtime::parse(story->date()); const auto now = base::unixtime::parse(story->date());
const auto b = begin(ids.list); for (auto i = index; i != 0;) {
for (auto i = b + index; i != b;) { const auto storyId = IdRespectingPinned(ids, --i);
if (const auto maybeStory = stories->lookup({ peerId, *--i })) { if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
const auto day = base::unixtime::parse((*maybeStory)->date()); const auto day = base::unixtime::parse((*maybeStory)->date());
if (day.date() != now.date()) { if (day.date() != now.date()) {
break; break;
@ -94,8 +99,9 @@ struct SameDayRange {
} }
--result.from; --result.from;
} }
for (auto i = b + index + 1, e = end(ids.list); i != e; ++i) { for (auto i = index + 1, c = int(ids.list.size()); i != c; ++i) {
if (const auto maybeStory = stories->lookup({ peerId, *i })) { const auto storyId = IdRespectingPinned(ids, i);
if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
const auto day = base::unixtime::parse((*maybeStory)->date()); const auto day = base::unixtime::parse((*maybeStory)->date());
if (day.date() != now.date()) { if (day.date() != now.date()) {
break; break;
@ -694,17 +700,16 @@ void Controller::rebuildFromContext(
}, [&](StoriesContextSaved) { }, [&](StoriesContextSaved) {
if (stories.savedCountKnown(peerId)) { if (stories.savedCountKnown(peerId)) {
const auto &saved = stories.saved(peerId); const auto &saved = stories.saved(peerId);
const auto &ids = saved.list; const auto i = IndexRespectingPinned(saved, id);
const auto i = ids.find(id); if (i < saved.list.size()) {
if (i != end(ids)) {
list = StoriesList{ list = StoriesList{
.peer = peer, .peer = peer,
.ids = saved, .ids = saved,
.total = stories.savedCount(peerId), .total = stories.savedCount(peerId),
}; };
_index = int(i - begin(ids)); _index = i;
if (ids.size() < list->total if (saved.list.size() < list->total
&& (end(ids) - i) < kPreloadStoriesCount) { && (saved.list.size() - i) < kPreloadStoriesCount) {
stories.savedLoadMore(peerId); stories.savedLoadMore(peerId);
} }
} }
@ -713,17 +718,16 @@ void Controller::rebuildFromContext(
}, [&](StoriesContextArchive) { }, [&](StoriesContextArchive) {
if (stories.archiveCountKnown(peerId)) { if (stories.archiveCountKnown(peerId)) {
const auto &archive = stories.archive(peerId); const auto &archive = stories.archive(peerId);
const auto &ids = archive.list; const auto i = IndexRespectingPinned(archive, id);
const auto i = ids.find(id); if (i < archive.list.size()) {
if (i != end(ids)) {
list = StoriesList{ list = StoriesList{
.peer = peer, .peer = peer,
.ids = archive, .ids = archive,
.total = stories.archiveCount(peerId), .total = stories.archiveCount(peerId),
}; };
_index = int(i - begin(ids)); _index = i;
if (ids.size() < list->total if (archive.list.size() < list->total
&& (end(ids) - i) < kPreloadStoriesCount) { && (archive.list.size() - i) < kPreloadStoriesCount) {
stories.archiveLoadMore(peerId); stories.archiveLoadMore(peerId);
} }
} }
@ -1520,7 +1524,7 @@ StoryId Controller::shownId(int index) const {
return _source return _source
? (_source->ids.begin() + index)->id ? (_source->ids.begin() + index)->id
: (index < int(_list->ids.list.size())) : (index < int(_list->ids.list.size()))
? *(_list->ids.list.begin() + index) ? IdRespectingPinned(_list->ids, index)
: StoryId(); : 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( void ReportRequested(
std::shared_ptr<Main::SessionShow> show, std::shared_ptr<Main::SessionShow> show,
FullStoryId id, FullStoryId id,

View file

@ -332,6 +332,10 @@ private:
bool channel, bool channel,
int count, int count,
bool inProfile); bool inProfile);
[[nodiscard]] Ui::Toast::Config PrepareTogglePinToast(
bool channel,
int count,
bool pin);
void ReportRequested( void ReportRequested(
std::shared_ptr<Main::SessionShow> show, std::shared_ptr<Main::SessionShow> show,
FullStoryId id, FullStoryId id,