init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/stories/info_stories_albums.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_story.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/fields/input_field.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Info::Stories {
namespace {
constexpr auto kAlbumNameLimit = 12;
void EditAlbumBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
StoryId addId,
QString currentName,
Fn<void(Data::StoryAlbum)> finished) {
box->setTitle(id
? tr::lng_stories_album_edit()
: tr::lng_stories_album_new_title());
if (!id) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_stories_album_new_text(),
st::collectionAbout));
}
const auto title = box->addRow(
object_ptr<Ui::InputField>(
box,
st::collectionNameField,
tr::lng_stories_album_new_ph(),
currentName));
title->setMaxLength(kAlbumNameLimit * 2);
box->setFocusCallback([=] {
title->setFocusFast();
});
Ui::AddLengthLimitLabel(title, kAlbumNameLimit);
const auto show = navigation->uiShow();
const auto session = &peer->session();
const auto creating = std::make_shared<bool>(false);
const auto submit = [=] {
if (*creating) {
return;
}
const auto text = title->getLastText().trimmed();
if (text.isEmpty() || text.size() > kAlbumNameLimit) {
title->showError();
return;
}
*creating = true;
const auto weak = base::make_weak(box);
const auto done = [=](Data::StoryAlbum result) {
*creating = false;
if (const auto onstack = finished) {
onstack(result);
}
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
const auto fail = [=](QString type) {
*creating = false;
if (type == u"ALBUMS_TOO_MANY"_q) {
show->show(Ui::MakeInformBox({
.text = tr::lng_stories_album_limit_text(),
.confirmText = tr::lng_box_ok(),
.title = tr::lng_stories_album_limit_title(),
}));
if (const auto strong = weak.get()) {
strong->closeBox();
}
} else {
show->showToast(type);
}
};
if (id) {
session->data().stories().albumRename(
peer,
id,
text,
done,
fail);
} else {
session->data().stories().albumCreate(
peer,
text,
addId,
done,
fail);
}
};
title->submits() | rpl::on_next(submit, title->lifetime());
auto text = id
? tr::lng_settings_save()
: tr::lng_stories_album_new_create();
box->addButton(std::move(text), submit);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace
void NewAlbumBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
StoryId addId,
Fn<void(Data::StoryAlbum)> added) {
EditAlbumBox(box, navigation, peer, 0, addId, QString(), added);
}
void EditAlbumNameBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
QString current,
Fn<void(QString)> done) {
EditAlbumBox(box, navigation, peer, id, {}, current, [=](
Data::StoryAlbum result) {
done(result.title);
});
}
} // namespace Info::Stories

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data {
struct StoryAlbum;
} // namespace Data
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Info::Stories {
void NewAlbumBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
StoryId addId,
Fn<void(Data::StoryAlbum)> added);
void EditAlbumNameBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
QString current,
Fn<void(QString)> done);
} // namespace Info::Stories

View File

@@ -0,0 +1,29 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Info::Stories {
[[nodiscard]] int ArchiveId();
struct Tag {
explicit Tag(
not_null<PeerData*> peer,
int albumId = 0,
int addingToAlbumId = 0)
: peer(peer)
, albumId(albumId)
, addingToAlbumId(addingToAlbumId) {
}
not_null<PeerData*> peer;
int albumId = 0;
int addingToAlbumId = 0;
};
} // namespace Info::Stories

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,175 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "data/data_stories.h"
#include "info/stories/info_stories_common.h"
#include "ui/rp_widget.h"
#include "ui/widgets/scroll_area.h"
namespace Data {
struct StoryAlbum;
} // namespace Data
namespace Ui {
class SubTabs;
class PopupMenu;
class VerticalLayout;
class MultiSlideTracker;
struct SubTabsReorderUpdate;
} // namespace Ui
namespace Info {
class Controller;
struct SelectedItems;
enum class SelectionAction;
} // namespace Info
namespace Info::Media {
class ListWidget;
} // namespace Info::Media
namespace Window {
class SessionNavigation;
} // namespace Window
namespace MTP {
class Sender;
} // namespace MTP
namespace Info::Stories {
class Memento;
class EmptyWidget;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
rpl::producer<int> albumId,
int addingToAlbumId = 0);
~InnerWidget();
[[nodiscard]] rpl::producer<> backRequest() const;
bool showInternal(not_null<Memento*> memento);
void setIsStackBottom(bool isStackBottom) {
_isStackBottom = isStackBottom;
setupTop();
}
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setScrollHeightValue(rpl::producer<int> value);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
void reload();
void editAlbumStories(int id);
void shareAlbumLink(const QString &username, int id);
void editAlbumName(int id);
void confirmDeleteAlbum(int id);
void albumAdded(Data::StoryAlbum result);
[[nodiscard]] rpl::producer<bool> albumEmptyValue() const {
return _albumEmpty.value();
}
[[nodiscard]] rpl::producer<int> albumIdChanges() const;
[[nodiscard]] rpl::producer<Data::StoryAlbumUpdate> changes() const;
bool hasFlexibleTopBar() const;
base::weak_qptr<Ui::RpWidget> createPinnedToTop(
not_null<Ui::RpWidget*> parent);
base::weak_qptr<Ui::RpWidget> createPinnedToBottom(
not_null<Ui::RpWidget*> parent);
void enableBackButton();
void showFinished();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
int recountHeight();
void refreshHeight();
void refreshEmpty();
void preloadArchiveCount();
void setupTop();
void setupList();
void setupEmpty();
void setupAlbums();
void createButtons();
void createProfileTop();
void createAboutArchive();
void startTop();
void addArchiveButton(Ui::MultiSlideTracker &tracker);
void addRecentButton(Ui::MultiSlideTracker &tracker);
void addGiftsButton(Ui::MultiSlideTracker &tracker);
void finalizeTop();
void refreshAlbumsTabs();
void showMenuForAlbum(int id);
void albumRenamed(int id, QString name);
void albumRemoved(int id);
void reorderAlbumsLocally(const Ui::SubTabsReorderUpdate &update);
void flushAlbumReorder();
const not_null<Controller*> _controller;
const not_null<PeerData*> _peer;
const int _addingToAlbumId = 0;
std::vector<Data::StoryAlbum> _albums;
rpl::variable<int> _albumId;
rpl::event_stream<int> _albumIdChanges;
Ui::RpWidget *_albumsWrap = nullptr;
std::unique_ptr<Ui::SubTabs> _albumsTabs;
rpl::variable<Data::StoryAlbumUpdate> _albumChanges;
bool _pendingAlbumReorder = false;
base::unique_qptr<Ui::PopupMenu> _menu;
std::unique_ptr<MTP::Sender> _api;
mtpRequestId _reorderRequestId = 0;
object_ptr<Ui::VerticalLayout> _top = { nullptr };
object_ptr<Media::ListWidget> _list = { nullptr };
object_ptr<Ui::RpWidget> _empty = { nullptr };
int _lastNonLoadingHeight = 0;
bool _emptyLoading = false;
bool _inResize = false;
bool _isStackBottom = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _listTops;
rpl::variable<int> _topHeight;
rpl::variable<bool> _albumEmpty;
rpl::variable<bool> _backToggles;
rpl::event_stream<> _backClicks;
rpl::event_stream<> _showFinished;
rpl::variable<std::optional<QColor>> _topBarColor;
Ui::VisibleRange _visibleRange;
};
} // namespace Info::Stories

View File

@@ -0,0 +1,503 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/stories/info_stories_provider.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_section.h"
#include "info/info_controller.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_stories_ids.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history.h"
#include "core/application.h"
#include "layout/layout_selection.h"
#include "styles/style_info.h"
namespace Info::Stories {
namespace {
using namespace Media;
constexpr auto kPreloadedScreensCount = 4;
constexpr auto kPreloadedScreensCountFull
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
[[nodiscard]] int MinStoryHeight(int width) {
auto itemsLeft = st::infoMediaSkip;
auto itemsInRow = (width - itemsLeft)
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
}
} // namespace
Provider::Provider(not_null<AbstractController*> controller)
: _controller(controller)
, _peer(controller->key().storiesPeer())
, _history(_peer->owner().history(_peer))
, _albumId(controller->key().storiesAlbumId())
, _addingToAlbumId(controller->key().storiesAddToAlbumId()) {
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}, _lifetime);
_peer->session().changes().storyUpdates(
Data::StoryUpdate::Flag::Destroyed
) | rpl::filter([=](const Data::StoryUpdate &update) {
return update.story->peer() == _peer;
}) | rpl::on_next([=](const Data::StoryUpdate &update) {
storyRemoved(update.story);
}, _lifetime);
}
Provider::~Provider() {
clear();
}
Type Provider::type() {
return Type::PhotoVideo;
}
bool Provider::hasSelectRestriction() {
if (_peer->session().frozen()) {
return true;
} else if (const auto channel = _peer->asChannel()) {
return !channel->canEditStories() && !channel->canDeleteStories();
}
return !_peer->isSelf();
}
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
return rpl::never<bool>();
}
bool Provider::sectionHasFloatingHeader() {
return false;
}
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
return QString();
}
bool Provider::sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) {
return true;
}
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
return true;
}
std::optional<int> Provider::fullCount() {
return _slice.fullCount();
}
void Provider::clear() {
for (const auto &[storyId, _] : _layouts) {
_peer->owner().stories().unregisterPolling(
{ _peer->id, storyId },
Data::Stories::Polling::Chat);
}
_layouts.clear();
_aroundId = kDefaultAroundId;
_idsLimit = kMinimalIdsLimit;
_slice = Data::StoriesIdsSlice();
}
void Provider::restart() {
clear();
refreshViewer();
}
void Provider::checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) {
const auto visibleWidth = viewport.width();
const auto visibleHeight = viewport.height();
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
const auto minItemHeight = MinStoryHeight(visibleWidth);
const auto preloadedCount = preloadedHeight / minItemHeight;
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
const auto preloadIdsLimit = preloadIdsLimitMin
+ (visibleHeight / minItemHeight);
const auto after = _slice.skippedAfter();
const auto topLoaded = after && (*after == 0);
const auto before = _slice.skippedBefore();
const auto bottomLoaded = before && (*before == 0);
const auto minScreenDelta = kPreloadedScreensCount
- kPreloadIfLessThanScreens;
const auto minIdDelta = (minScreenDelta * visibleHeight)
/ minItemHeight;
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
auto preloadRequired = false;
const auto id = StoryIdFromMsgId(layout->getItem()->id);
if (!preloadRequired) {
preloadRequired = (_idsLimit < preloadIdsLimitMin);
}
if (!preloadRequired) {
auto delta = _slice.distance(_aroundId, id);
Assert(delta != std::nullopt);
preloadRequired = (qAbs(*delta) >= minIdDelta);
}
if (preloadRequired) {
_idsLimit = preloadIdsLimit;
_aroundId = id;
refreshViewer();
}
};
if (preloadTop && !topLoaded) {
preloadAroundItem(topLayout);
} else if (preloadBottom && !bottomLoaded) {
preloadAroundItem(bottomLayout);
}
}
void Provider::setSearchQuery(QString query) {
}
void Provider::refreshViewer() {
_viewerLifetime.destroy();
const auto aroundId = _aroundId;
auto ids = Data::AlbumStoriesIds(_peer, _albumId, aroundId, _idsLimit);
std::move(
ids
) | rpl::on_next([=](Data::StoriesIdsSlice &&slice) {
if (!slice.fullCount()) {
// Don't display anything while full count is unknown.
return;
}
_slice = std::move(slice);
auto nearestId = std::optional<StoryId>();
for (auto i = 0; i != _slice.size(); ++i) {
if (!nearestId
|| std::abs(*nearestId - aroundId)
> std::abs(_slice[i] - aroundId)) {
nearestId = _slice[i];
}
}
if (nearestId) {
_aroundId = *nearestId;
}
//if (const auto nearest = _slice.nearest(idForViewer)) {
// _aroundId = *nearest;
//}
_refreshed.fire({});
}, _viewerLifetime);
}
rpl::producer<> Provider::refreshed() {
return _refreshed.events();
}
std::vector<ListSection> Provider::fillSections(
not_null<Overview::Layout::Delegate*> delegate) {
markLayoutsStale();
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
auto result = std::vector<ListSection>();
auto section = ListSection(Type::PhotoVideo, sectionDelegate());
auto count = _slice.size();
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();
result.push_back(std::move(section));
section = ListSection(Type::PhotoVideo, sectionDelegate());
section.addItem(layout);
}
}
}
if (!section.empty()) {
section.finishSection();
result.push_back(std::move(section));
}
return result;
}
void Provider::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_peer->owner().stories().unregisterPolling(
{ _peer->id, i->first },
Data::Stories::Polling::Chat);
_layoutRemoved.fire(i->second.item.get());
const auto taken = _items.take(i->first);
i = _layouts.erase(i);
} else {
++i;
}
}
}
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
return _layoutRemoved.events();
}
BaseLayout *Provider::lookupLayout(const HistoryItem *item) {
return nullptr;
}
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
return IsStoryMsgId(item->id) && (item->history()->peer == _peer);
}
bool Provider::isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) {
return (a->id < b->id);
}
void Provider::storyRemoved(not_null<Data::Story*> story) {
Expects(story->peer() == _peer);
if (const auto i = _layouts.find(story->id()); i != end(_layouts)) {
_peer->owner().stories().unregisterPolling(
story,
Data::Stories::Polling::Chat);
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
_items.remove(story->id());
}
BaseLayout *Provider::getLayout(
StoryId id,
not_null<Overview::Layout::Delegate*> delegate) {
auto it = _layouts.find(id);
if (it == _layouts.end()) {
if (auto layout = createLayout(id, delegate)) {
layout->initDimensions();
it = _layouts.emplace(id, std::move(layout)).first;
const auto ok = _peer->owner().stories().registerPolling(
{ _peer->id, id },
Data::Stories::Polling::Chat);
Assert(ok);
} else {
return nullptr;
}
}
it->second.stale = false;
return it->second.item.get();
}
HistoryItem *Provider::ensureItem(StoryId id) {
const auto i = _items.find(id);
if (i != end(_items)) {
return i->second.get();
}
auto item = _peer->owner().stories().resolveItem({ _peer->id, id });
if (!item) {
return nullptr;
}
return _items.emplace(id, std::move(item)).first->second.get();
}
std::unique_ptr<BaseLayout> Provider::createLayout(
StoryId id,
not_null<Overview::Layout::Delegate*> delegate) {
const auto item = ensureItem(id);
if (!item) {
return nullptr;
}
const auto getPhoto = [&]() -> PhotoData* {
if (const auto media = item->media()) {
return media->photo();
}
return nullptr;
};
const auto getFile = [&]() -> DocumentData* {
if (const auto media = item->media()) {
return media->document();
}
return nullptr;
};
const auto peer = item->history()->peer;
const auto channel = peer->asChannel();
const auto showPinned = (_albumId == Data::kStoriesAlbumIdSaved);
const auto showHidden = peer->isSelf()
|| (channel && channel->canEditStories());
using namespace Overview::Layout;
const auto options = MediaOptions{
.story = true,
.storyPinned = showPinned && item->isPinned(),
.storyShowPinned = showPinned,
.storyHidden = showHidden && !item->storyInProfile(),
.storyShowHidden = showHidden,
};
if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(delegate, item, photo, options);
} else if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, options);
} else {
return std::make_unique<Photo>(
delegate,
item,
Data::MediaStory::LoadingStoryPhoto(&item->history()->owner()),
options);
}
return nullptr;
}
ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) {
auto result = ListItemSelectionData(selection);
const auto id = item->id;
if (_addingToAlbumId || !IsStoryMsgId(id)) {
return result;
}
const auto peer = item->history()->peer;
const auto channel = peer->asChannel();
const auto maybeStory = peer->owner().stories().lookup(
{ peer->id, StoryIdFromMsgId(id) });
if (maybeStory) {
const auto story = *maybeStory;
result.canForward = peer->isSelf() && story->canShare();
result.canDelete = story->canDelete();
result.canUnpinStory = story->pinnedToTop();
result.storyInProfile = story->inProfile();
}
result.canToggleStoryPin = peer->isSelf()
|| (channel && channel->canEditStories());
return result;
}
void Provider::applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) {
const auto fromId = fromItem->id - (skipFrom ? 1 : 0);
const auto tillId = tillItem->id - (skipTill ? 0 : 1);
for (auto i = selected.begin(); i != selected.end();) {
const auto itemId = i->first->id;
if (itemId > fromId || itemId <= tillId) {
i = selected.erase(i);
} else {
++i;
}
}
for (auto &layoutItem : _layouts) {
const auto storyId = layoutItem.first;
const auto id = StoryIdToMsgId(storyId);
if (id <= fromId && id > tillId) {
const auto i = _items.find(storyId);
Assert(i != end(_items));
const auto item = i->second.get();
ChangeItemSelection(
selected,
item,
computeSelectionData(item, FullSelection));
}
}
}
bool Provider::allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return false;
}
QString Provider::showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return QString();
}
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
return StoryIdFromMsgId(item->id);
}
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));
// 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;
}
void Provider::saveState(
not_null<Media::Memento*> memento,
ListScrollTopState scrollState) {
if (_aroundId != kDefaultAroundId && scrollState.item) {
memento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) });
memento->setIdsLimit(_idsLimit);
memento->setScrollTopItem(scrollState.item->globalId());
memento->setScrollTopItemPosition(scrollState.position);
memento->setScrollTopShift(scrollState.shift);
}
}
void Provider::restoreState(
not_null<Media::Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) {
if (const auto limit = memento->idsLimit()) {
const auto wasAroundId = memento->aroundId();
if (wasAroundId.peer == _peer->id) {
_idsLimit = limit;
_aroundId = StoryIdFromMsgId(wasAroundId.msg);
restoreScrollState({
.position = memento->scrollTopItemPosition(),
.item = MessageByGlobalId(memento->scrollTopItem()),
.shift = memento->scrollTopShift(),
});
refreshViewer();
}
}
}
} // namespace Info::Stories

View File

@@ -0,0 +1,138 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "data/data_stories_ids.h"
#include "info/media/info_media_common.h"
#include "info/stories/info_stories_common.h"
class DocumentData;
class HistoryItem;
class PeerData;
class History;
namespace Data {
class Story;
} // namespace Data
namespace Info {
class AbstractController;
} // namespace Info
namespace Info::Stories {
class Provider final
: public Media::ListProvider
, private Media::ListSectionDelegate
, public base::has_weak_ptr {
public:
explicit Provider(not_null<AbstractController*> controller);
~Provider();
Media::Type type() override;
bool hasSelectRestriction() override;
rpl::producer<bool> hasSelectRestrictionChanges() override;
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
std::optional<int> fullCount() override;
void restart() override;
void checkPreload(
QSize viewport,
not_null<Media::BaseLayout*> topLayout,
not_null<Media::BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) override;
void refreshViewer() override;
rpl::producer<> refreshed() override;
void setSearchQuery(QString query) override;
std::vector<Media::ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) override;
rpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;
Media::BaseLayout *lookupLayout(const HistoryItem *item) override;
bool isMyItem(not_null<const HistoryItem*> item) override;
bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) override;
Media::ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) override;
void applyDragSelection(
Media::ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) override;
bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
HistoryItem *scrollTopStateItem(
Media::ListScrollTopState state) override;
void saveState(
not_null<Media::Memento*> memento,
Media::ListScrollTopState scrollState) override;
void restoreState(
not_null<Media::Memento*> memento,
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
private:
static constexpr auto kMinimalIdsLimit = 16;
static constexpr auto kDefaultAroundId = ServerMaxStoryId - 1;
bool sectionHasFloatingHeader() override;
QString sectionTitle(not_null<const Media::BaseLayout*> item) override;
bool sectionItemBelongsHere(
not_null<const Media::BaseLayout*> item,
not_null<const Media::BaseLayout*> previous) override;
void storyRemoved(not_null<Data::Story*> story);
void markLayoutsStale();
void clearStaleLayouts();
void clear();
[[nodiscard]] HistoryItem *ensureItem(StoryId id);
[[nodiscard]] Media::BaseLayout *getLayout(
StoryId id,
not_null<Overview::Layout::Delegate*> delegate);
[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(
StoryId id,
not_null<Overview::Layout::Delegate*> delegate);
const not_null<AbstractController*> _controller;
const not_null<PeerData*> _peer;
const not_null<History*> _history;
const int _albumId = 0;
const int _addingToAlbumId = 0;
StoryId _aroundId = kDefaultAroundId;
int _idsLimit = kMinimalIdsLimit;
Data::StoriesIdsSlice _slice;
base::flat_map<StoryId, std::shared_ptr<HistoryItem>> _items;
std::unordered_map<StoryId, Media::CachedItem> _layouts;
rpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;
rpl::event_stream<> _refreshed;
bool _started = false;
rpl::lifetime _lifetime;
rpl::lifetime _viewerLifetime;
};
} // namespace Info::Stories

View File

@@ -0,0 +1,306 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/stories/info_stories_widget.h"
#include "data/data_peer.h"
#include "data/data_stories.h"
#include "info/stories/info_stories_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h"
#include "lang/lang_keys.h"
#include "ui/ui_utility.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QScrollBar>
namespace Info::Stories {
int ArchiveId() {
return Data::kStoriesAlbumIdArchive;
}
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(Tag{
controller->storiesPeer(),
controller->storiesAlbumId(),
controller->storiesAddToAlbumId() })
, _media(controller) {
}
Memento::Memento(not_null<PeerData*> peer, int albumId, int addingToAlbumId)
: ContentMemento(Tag{ peer, albumId, addingToAlbumId })
, _media(peer, 0, Media::Type::PhotoVideo) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::Stories);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _albumId(controller->key().storiesAlbumId())
, _inner(
setupFlexibleInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
_albumId.value(),
controller->key().storiesAddToAlbumId()),
_flexibleScroll))
, _pinnedToTop(_inner->createPinnedToTop(this)) {
_emptyAlbumShown = _inner->albumEmptyValue();
_inner->albumIdChanges() | rpl::on_next([=](int id) {
controller->showSection(
Make(controller->storiesPeer(), id),
Window::SectionShow::Way::Backward);
}, _inner->lifetime());
_inner->setScrollHeightValue(scrollHeightValue());
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
if (request.ymin < 0) {
scrollTopRestore(
qMin(scrollTopSave(), request.ymax));
} else {
scrollTo(request);
}
}, lifetime());
if (_pinnedToTop) {
_inner->widthValue(
) | rpl::on_next([=](int w) {
_pinnedToTop->resizeToWidth(w);
setScrollTopSkip(_pinnedToTop->height());
}, _pinnedToTop->lifetime());
_pinnedToTop->heightValue(
) | rpl::on_next([=](int h) {
setScrollTopSkip(h);
}, _pinnedToTop->lifetime());
}
if (_pinnedToTop
&& _pinnedToTop->minimumHeight()
&& _inner->hasFlexibleTopBar()) {
_flexibleScrollHelper = std::make_unique<FlexibleScrollHelper>(
scroll(),
_inner,
_pinnedToTop.get(),
[=](QMargins margins) {
ContentWidget::setPaintPadding(std::move(margins));
},
[=](rpl::producer<not_null<QEvent*>> &&events) {
ContentWidget::setViewport(std::move(events));
},
_flexibleScroll);
}
rpl::combine(
_albumId.value(),
_emptyAlbumShown.value()
) | rpl::on_next([=] {
refreshBottom();
}, _inner->lifetime());
_inner->backRequest() | rpl::on_next([=] {
checkBeforeClose([=] { controller->showBackFromStack(); });
}, _inner->lifetime());
}
void Widget::setInnerFocus() {
_inner->setFocus();
}
void Widget::setIsStackBottom(bool isStackBottom) {
ContentWidget::setIsStackBottom(isStackBottom);
_inner->setIsStackBottom(isStackBottom);
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (auto storiesMemento = dynamic_cast<Memento*>(memento.get())) {
const auto myId = controller()->key().storiesAlbumId();
const auto hisId = storiesMemento->storiesAlbumId();
constexpr auto kArchive = Data::kStoriesAlbumIdArchive;
if (myId == hisId) {
restoreState(storiesMemento);
return true;
} else if (myId != kArchive && hisId != kArchive) {
_albumId = hisId;
return true;
}
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
void Widget::refreshBottom() {
const auto albumId = _albumId.current();
const auto withButton = (albumId != Data::kStoriesAlbumIdSaved)
&& (albumId != Data::kStoriesAlbumIdArchive)
&& controller()->storiesPeer()->canEditStories()
&& !_emptyAlbumShown.current();
const auto wasBottom = _pinnedToBottom ? _pinnedToBottom->height() : 0;
delete _pinnedToBottom.data();
if (!withButton) {
setScrollBottomSkip(0);
_hasPinnedToBottom = false;
} else {
setupBottomButton(wasBottom);
}
if (_pinnedToBottom) {
const auto processHeight = [=] {
setScrollBottomSkip(_pinnedToBottom->height());
_pinnedToBottom->moveToLeft(
_pinnedToBottom->x(),
height() - _pinnedToBottom->height());
};
_inner->sizeValue(
) | rpl::on_next([=](const QSize &s) {
_pinnedToBottom->resizeToWidth(s.width());
}, _pinnedToBottom->lifetime());
rpl::combine(
_pinnedToBottom->heightValue(),
heightValue()
) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());
}
}
void Widget::setupBottomButton(int wasBottomHeight) {
_pinnedToBottom = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(
this,
object_ptr<Ui::RpWidget>(this));
const auto wrap = _pinnedToBottom.data();
wrap->toggle(false, anim::type::instant);
const auto bottom = wrap->entity();
bottom->show();
const auto button = Ui::CreateChild<Ui::RoundButton>(
bottom,
rpl::single(QString()),
st::collectionEditBox.button);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->setText(tr::lng_stories_album_add_button(
) | rpl::map([](const QString &text) {
return Ui::Text::IconEmoji(&st::collectionAddIcon).append(text);
}));
button->show();
_hasPinnedToBottom = true;
button->setClickedCallback([=] {
if (const auto id = _albumId.current()) {
_inner->editAlbumStories(id);
} else {
refreshBottom();
}
});
const auto buttonTop = st::boxRadius;
bottom->widthValue() | rpl::on_next([=](int width) {
const auto normal = width - 2 * buttonTop;
button->resizeToWidth(normal);
const auto buttonLeft = (width - normal) / 2;
button->moveToLeft(buttonLeft, buttonTop);
}, button->lifetime());
button->heightValue() | rpl::on_next([=](int height) {
bottom->resize(bottom->width(), st::boxRadius + height);
}, button->lifetime());
if (_shown) {
wrap->toggle(
true,
wasBottomHeight ? anim::type::instant : anim::type::normal);
}
}
void Widget::showFinished() {
_shown = true;
if (const auto bottom = _pinnedToBottom.data()) {
bottom->toggle(true, anim::type::normal);
}
_inner->showFinished();
}
rpl::producer<SelectedItems> Widget::selectedListValue() const {
return _inner->selectedListValue();
}
void Widget::selectionAction(SelectionAction action) {
_inner->selectionAction(action);
}
rpl::producer<QString> Widget::title() {
const auto peer = controller()->key().storiesPeer();
return (controller()->key().storiesAlbumId() == ArchiveId())
? tr::lng_stories_archive_title()
: (peer && peer->isSelf())
? tr::lng_menu_my_profile()
: tr::lng_stories_my_title();
}
rpl::producer<bool> Widget::desiredBottomShadowVisibility() {
return _hasPinnedToBottom.value();
}
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer, int albumId) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(peer, albumId, 0)));
}
} // namespace Info::Stories

View File

@@ -0,0 +1,98 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "info/info_content_widget.h"
#include "info/media/info_media_widget.h"
#include "info/stories/info_stories_common.h"
#include "ui/effects/animations.h"
namespace Ui {
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Info::Stories {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer, int albumId, int addingToAlbumId);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
[[nodiscard]] Media::Memento &media() {
return _media;
}
[[nodiscard]] const Media::Memento &media() const {
return _media;
}
private:
Media::Memento _media;
int _addingToAlbumId = 0;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
void setInnerFocus() override;
void setIsStackBottom(bool isStackBottom) override;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<SelectedItems> selectedListValue() const override;
void selectionAction(SelectionAction action) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredBottomShadowVisibility() override;
void showFinished() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setupBottomButton(int wasBottomHeight);
void refreshBottom();
std::shared_ptr<ContentMemento> doCreateMemento() override;
FlexibleScrollData _flexibleScroll;
rpl::variable<int> _albumId;
InnerWidget *_inner = nullptr;
base::weak_qptr<Ui::RpWidget> _pinnedToTop;
QPointer<Ui::SlideWrap<Ui::RpWidget>> _pinnedToBottom;
rpl::variable<bool> _hasPinnedToBottom;
rpl::variable<bool> _emptyAlbumShown;
bool _shown = false;
std::unique_ptr<FlexibleScrollHelper> _flexibleScrollHelper;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(
not_null<PeerData*> peer,
int albumId = 0);
} // namespace Info::Stories