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_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";

View File

@ -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<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) {
if (ids.empty()) {
return;
@ -1788,6 +1862,75 @@ void Stories::toggleInProfileList(
}).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(
std::shared_ptr<Ui::Show> show,
FullStoryId id,

View File

@ -33,12 +33,17 @@ class StoryPreload;
struct StoriesIds {
base::flat_set<StoryId, std::greater<>> list;
std::vector<StoryId> 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<FullStoryId> &ids,
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(
std::shared_ptr<Ui::Show> show,
FullStoryId id,
@ -314,6 +324,9 @@ private:
void notifySourcesChanged(StorySourcesList list);
void pushHiddenCountsToFolder();
void setPinnedToTop(
PeerId peerId,
std::vector<StoryId> &&pinnedToTop);
[[nodiscard]] int pollingInterval(
const PollingSettings &settings) const;

View File

@ -40,18 +40,23 @@ rpl::producer<StoriesIdsSlice> 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<StoryId>{
std::make_reverse_iterator(around + takeAfter),
std::make_reverse_iterator(around - takeBefore)
};
auto ids = std::vector<StoryId>();
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<StoriesIdsSlice> 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<StoryId>{
std::make_reverse_iterator(i + takeAfter),
std::make_reverse_iterator(i - takeBefore)
};
auto ids = std::vector<StoryId>();
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),

View File

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

View File

@ -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());
}

View File

@ -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;

View File

@ -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;

View File

@ -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<SelectionAction> 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<Ui::FadeWrap<Ui::IconButton>>(
this,
@ -668,6 +680,7 @@ void TopBar::createSelectionControls() {
_selectionActionRequests,
_cancelSelection->lifetime());
_forward->entity()->setVisible(_canForward);
_delete = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(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<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(
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<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(
_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,

View File

@ -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<Ui::FadeWrap<Ui::IconButton>> _cancelSelection;
QPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText;
QPointer<Ui::FadeWrap<Ui::IconButton>> _forward;
QPointer<Ui::FadeWrap<Ui::IconButton>> _delete;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryInProfile;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;
rpl::event_stream<SelectionAction> _selectionActionRequests;

View File

@ -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 {

View File

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

View File

@ -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<void()> confirmed) {
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) {
if (const auto item = MessageByGlobalId(globalId)) {
auto items = SelectedItems(_provider->type());

View File

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

View File

@ -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<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({});
}, _viewerLifetime);
}
@ -208,8 +221,8 @@ std::vector<ListSection> Provider::fillSections(
auto result = std::vector<ListSection>();
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<HistoryItem*> 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<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)) {
return item;
}
}
return state.item;
}

View File

@ -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<Main::SessionShow> show,
FullStoryId id,

View File

@ -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<Main::SessionShow> show,
FullStoryId id,