mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 06:07:06 +02:00
Implement stories pin-to-top.
This commit is contained in:
parent
4b98ab1246
commit
468d8b04d6
17 changed files with 464 additions and 57 deletions
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -31,6 +31,7 @@ struct ListItemSelectionData {
|
|||
bool canDelete = false;
|
||||
bool canForward = false;
|
||||
bool canToggleStoryPin = false;
|
||||
bool canUnpinStory = false;
|
||||
|
||||
friend inline bool operator==(
|
||||
ListItemSelectionData,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue