tdesktop/Telegram/SourceFiles/media/stories/media_stories_controller.cpp

1909 lines
50 KiB
C++

/*
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 "media/stories/media_stories_controller.h"
#include "base/power_save_blocker.h"
#include "base/qt_signal_producer.h"
#include "base/unixtime.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/update_checker.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "media/stories/media_stories_caption_full_view.h"
#include "media/stories/media_stories_delegate.h"
#include "media/stories/media_stories_header.h"
#include "media/stories/media_stories_sibling.h"
#include "media/stories/media_stories_slider.h"
#include "media/stories/media_stories_reactions.h"
#include "media/stories/media_stories_recent_views.h"
#include "media/stories/media_stories_reply.h"
#include "media/stories/media_stories_repost_view.h"
#include "media/stories/media_stories_share.h"
#include "media/stories/media_stories_stealth.h"
#include "media/stories/media_stories_view.h"
#include "media/audio/media_audio.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/report_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/round_rect.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h" // defaultReportBox
#include "styles/style_media_view.h"
#include "styles/style_boxes.h" // UserpicButton
#include <QtGui/QWindow>
namespace Media::Stories {
namespace {
constexpr auto kPhotoProgressInterval = crl::time(100);
constexpr auto kPhotoDuration = 5 * crl::time(1000);
constexpr auto kFullContentFade = 0.6;
constexpr auto kSiblingMultiplierDefault = 0.448;
constexpr auto kSiblingMultiplierMax = 0.72;
constexpr auto kSiblingOutsidePart = 0.24;
constexpr auto kSiblingUserpicSize = 0.3;
constexpr auto kInnerHeightMultiplier = 1.6;
constexpr auto kPreloadPeersCount = 3;
constexpr auto kPreloadStoriesCount = 5;
constexpr auto kPreloadNextMediaCount = 3;
constexpr auto kPreloadPreviousMediaCount = 1;
constexpr auto kMarkAsReadAfterSeconds = 0.2;
constexpr auto kMarkAsReadAfterProgress = 0.;
struct SameDayRange {
int from = 0;
int till = 0;
};
[[nodiscard]] SameDayRange ComputeSameDayRange(
not_null<Data::Story*> story,
const Data::StoriesIds &ids,
const std::vector<StoryId> &sorted,
int index) {
Expects(index >= 0 && index < ids.list.size());
Expects(index >= 0 && index < sorted.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());
for (auto i = index; i != 0;) {
const auto storyId = sorted[--i];
if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
const auto day = base::unixtime::parse((*maybeStory)->date());
if (day.date() != now.date()) {
break;
}
}
--result.from;
}
for (auto i = index + 1, c = int(sorted.size()); i != c; ++i) {
const auto storyId = sorted[i];
if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
const auto day = base::unixtime::parse((*maybeStory)->date());
if (day.date() != now.date()) {
break;
}
}
++result.till;
}
return result;
}
[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
if (std::abs(angle) < 1.) {
return point;
}
const auto alpha = angle / 180. * M_PI;
const auto acos = cos(alpha);
const auto asin = sin(alpha);
point -= origin;
return origin + QPoint(
int(base::SafeRound(acos * point.x() - asin * point.y())),
int(base::SafeRound(asin * point.x() + acos * point.y())));
}
} // namespace
class Controller::PhotoPlayback final {
public:
explicit PhotoPlayback(not_null<Controller*> controller);
[[nodiscard]] bool paused() const;
void togglePaused(bool paused);
private:
void callback();
const not_null<Controller*> _controller;
base::Timer _timer;
crl::time _started = 0;
crl::time _paused = 0;
};
class Controller::Unsupported final {
public:
Unsupported(not_null<Controller*> controller, not_null<PeerData*> peer);
private:
void setup(not_null<PeerData*> peer);
const not_null<Controller*> _controller;
std::unique_ptr<Ui::RpWidget> _bg;
std::unique_ptr<Ui::FlatLabel> _text;
std::unique_ptr<Ui::RoundButton> _button;
Ui::RoundRect _bgRound;
};
Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> controller)
: _controller(controller)
, _timer([=] { callback(); })
, _started(crl::now())
, _paused(_started) {
}
bool Controller::PhotoPlayback::paused() const {
return _paused != 0;
}
void Controller::PhotoPlayback::togglePaused(bool paused) {
if (!_paused == !paused) {
return;
} else if (paused) {
const auto now = crl::now();
if (now - _started >= kPhotoDuration) {
return;
}
_paused = now;
_timer.cancel();
} else {
_started += crl::now() - _paused;
_paused = 0;
_timer.callEach(kPhotoProgressInterval);
}
callback();
}
void Controller::PhotoPlayback::callback() {
const auto now = crl::now();
const auto elapsed = now - _started;
const auto finished = (now - _started >= kPhotoDuration);
if (finished) {
_timer.cancel();
}
using State = Player::State;
const auto state = finished
? State::StoppedAtEnd
: _paused
? State::Paused
: State::Playing;
_controller->updatePhotoPlayback({
.state = state,
.position = elapsed,
.receivedTill = kPhotoDuration,
.length = kPhotoDuration,
.frequency = 1000,
});
}
Controller::Unsupported::Unsupported(
not_null<Controller*> controller,
not_null<PeerData*> peer)
: _controller(controller)
, _bgRound(st::storiesRadius, st::storiesComposeBg) {
setup(peer);
}
void Controller::Unsupported::setup(not_null<PeerData*> peer) {
const auto wrap = _controller->wrap();
_bg = std::make_unique<Ui::RpWidget>(wrap);
_bg->show();
_bg->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(_bg.get());
_bgRound.paint(p, _bg->rect());
}, _bg->lifetime());
_controller->layoutValue(
) | rpl::start_with_next([=](const Layout &layout) {
_bg->setGeometry(layout.content);
}, _bg->lifetime());
_text = std::make_unique<Ui::FlatLabel>(
wrap,
tr::lng_stories_unsupported(),
st::storiesUnsupportedLabel);
_text->show();
_button = std::make_unique<Ui::RoundButton>(
wrap,
tr::lng_update_telegram(),
st::storiesUnsupportedUpdate);
_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
_button->show();
rpl::combine(
_controller->layoutValue(),
_text->sizeValue(),
_button->sizeValue()
) | rpl::start_with_next([=](
const Layout &layout,
QSize text,
QSize button) {
const auto wrap = layout.content;
const auto totalHeight = st::storiesUnsupportedTop
+ text.height()
+ st::storiesUnsupportedSkip
+ button.height();
const auto top = (wrap.height() - totalHeight) / 2;
_text->move(
wrap.x() + (wrap.width() - text.width()) / 2,
wrap.y() + top + st::storiesUnsupportedTop);
_button->move(
wrap.x() + (wrap.width() - button.width()) / 2,
wrap.y() + top + totalHeight - button.height());
}, _button->lifetime());
_button->setClickedCallback([=] {
Core::UpdateApplication();
});
}
Controller::Controller(not_null<Delegate*> delegate)
: _delegate(delegate)
, _wrap(_delegate->storiesWrap())
, _header(std::make_unique<Header>(this))
, _slider(std::make_unique<Slider>(this))
, _replyArea(std::make_unique<ReplyArea>(this))
, _reactions(std::make_unique<Reactions>(this))
, _recentViews(std::make_unique<RecentViews>(this)) {
initLayout();
using namespace rpl::mappers;
rpl::combine(
_replyArea->activeValue(),
_reactions->activeValue(),
_1 || _2
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool active) {
_replyActive = active;
updateContentFaded();
}, _lifetime);
_reactions->setReplyFieldState(
_replyArea->focusedValue(),
_replyArea->hasSendTextValue());
if (const auto like = _replyArea->likeAnimationTarget()) {
_reactions->attachToReactionButton(like);
}
_reactions->chosen(
) | rpl::start_with_next([=](Reactions::Chosen chosen) {
if (reactionChosen(chosen.mode, chosen.reaction)) {
_reactions->animateAndProcess(std::move(chosen));
}
}, _lifetime);
_delegate->storiesLayerShown(
) | rpl::start_with_next([=](bool shown) {
if (_layerShown != shown) {
_layerShown = shown;
updatePlayingAllowed();
}
}, _lifetime);
_header->tooltipShownValue(
) | rpl::start_with_next([=](bool shown) {
if (_tooltipShown != shown) {
_tooltipShown = shown;
updatePlayingAllowed();
}
}, _lifetime);
const auto window = _wrap->window()->windowHandle();
Assert(window != nullptr);
base::qt_signal_producer(
window,
&QWindow::activeChanged
) | rpl::start_with_next([=] {
_windowActive = window->isActive();
updatePlayingAllowed();
}, _lifetime);
_windowActive = window->isActive();
_contentFadeAnimation.stop();
}
Controller::~Controller() {
_captionFullView = nullptr;
_repostView = nullptr;
changeShown(nullptr);
}
void Controller::updateContentFaded() {
const auto faded = _replyActive
|| (_captionFullView && !_captionFullView->closing());
if (_contentFaded == faded) {
return;
}
_contentFaded = faded;
_contentFadeAnimation.start(
[=] { _delegate->storiesRepaint(); },
_contentFaded ? 0. : 1.,
_contentFaded ? 1. : 0.,
st::fadeWrapDuration);
updatePlayingAllowed();
}
void Controller::initLayout() {
const auto headerHeight = st::storiesHeaderMargin.top()
+ st::storiesHeaderPhoto.photoSize
+ st::storiesHeaderMargin.bottom();
const auto sliderHeight = st::storiesSliderMargin.top()
+ st::storiesSliderWidth
+ st::storiesSliderMargin.bottom();
const auto outsideHeaderHeight = headerHeight
+ sliderHeight
+ st::storiesSliderOutsideSkip;
const auto fieldMinHeight = st::storiesFieldMargin.top()
+ st::storiesAttach.height
+ st::storiesFieldMargin.bottom();
const auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom()
+ outsideHeaderHeight
+ st::storiesMaxSize.height()
+ fieldMinHeight;
_layout = _wrap->sizeValue(
) | rpl::map([=](QSize size) {
const auto topNotchSkip = _delegate->storiesTopNotchSkip();
size = QSize(
std::max(size.width(), st::mediaviewMinWidth),
std::max(size.height(), st::mediaviewMinHeight));
auto layout = Layout();
layout.headerLayout = (size.height() >= minHeightForOutsideHeader)
? HeaderLayout::Outside
: HeaderLayout::Normal;
const auto topSkip = topNotchSkip
+ st::storiesFieldMargin.bottom()
+ (layout.headerLayout == HeaderLayout::Outside
? outsideHeaderHeight
: 0);
const auto bottomSkip = fieldMinHeight;
const auto maxWidth = size.width() - 2 * st::storiesSideSkip;
const auto availableHeight = size.height() - topSkip - bottomSkip;
const auto maxContentHeight = std::min(
availableHeight,
st::storiesMaxSize.height());
const auto nowWidth = maxContentHeight * st::storiesMaxSize.width()
/ st::storiesMaxSize.height();
const auto contentWidth = std::min(nowWidth, maxWidth);
const auto contentHeight = (contentWidth < nowWidth)
? (contentWidth * st::storiesMaxSize.height()
/ st::storiesMaxSize.width())
: maxContentHeight;
const auto addedTopSkip = (availableHeight - contentHeight) / 2;
layout.content = QRect(
(size.width() - contentWidth) / 2,
addedTopSkip + topSkip,
contentWidth,
contentHeight);
const auto reactionsWidth = st::storiesReactionsWidth;
layout.reactions = QRect(
(size.width() - reactionsWidth) / 2,
layout.content.y(),
reactionsWidth,
contentHeight);
if (layout.headerLayout == HeaderLayout::Outside) {
layout.header = QRect(
layout.content.topLeft() - QPoint(0, outsideHeaderHeight),
QSize(contentWidth, outsideHeaderHeight));
layout.slider = QRect(
layout.header.topLeft() + QPoint(0, headerHeight),
QSize(contentWidth, sliderHeight));
} else {
layout.slider = QRect(
layout.content.topLeft(),
QSize(contentWidth, sliderHeight));
layout.header = QRect(
layout.slider.topLeft() + QPoint(0, sliderHeight),
QSize(contentWidth, headerHeight));
}
layout.controlsWidth = std::max(
layout.content.width(),
st::storiesControlsMinWidth);
layout.controlsBottomPosition = QPoint(
(size.width() - layout.controlsWidth) / 2,
(layout.content.y()
+ layout.content.height()
+ fieldMinHeight
- st::storiesFieldMargin.bottom()));
layout.views = QRect(
layout.controlsBottomPosition - QPoint(0, fieldMinHeight),
QSize(layout.controlsWidth, fieldMinHeight));
layout.autocompleteRect = QRect(
layout.controlsBottomPosition.x(),
0,
layout.controlsWidth,
layout.controlsBottomPosition.y());
const auto sidesAvailable = size.width() - layout.content.width();
const auto widthForSiblings = sidesAvailable
- 2 * st::storiesFieldMargin.bottom();
const auto siblingWidthMax = widthForSiblings
/ (2 * (1. - kSiblingOutsidePart));
const auto siblingMultiplierMax = std::max(
kSiblingMultiplierDefault,
st::storiesSiblingWidthMin / float64(layout.content.width()));
const auto siblingMultiplier = std::min({
siblingMultiplierMax,
kSiblingMultiplierMax,
siblingWidthMax / layout.content.width(),
});
const auto siblingSize = layout.content.size() * siblingMultiplier;
const auto siblingTop = (size.height() - siblingSize.height()) / 2;
const auto outsideMax = int(base::SafeRound(
siblingSize.width() * kSiblingOutsidePart));
const auto leftAvailable = layout.content.x() - siblingSize.width();
const auto xDesired = leftAvailable / 3;
const auto xPossible = std::min(
xDesired,
(leftAvailable - st::storiesControlSize));
const auto xLeft = std::max(xPossible, -outsideMax);
const auto xRight = size.width() - siblingSize.width() - xLeft;
const auto userpicSize = int(base::SafeRound(
siblingSize.width() * kSiblingUserpicSize));
const auto innerHeight = userpicSize * kInnerHeightMultiplier;
const auto userpic = [&](QRect geometry) {
return QRect(
(geometry.width() - userpicSize) / 2,
(geometry.height() - innerHeight) / 2,
userpicSize,
userpicSize
).translated(geometry.topLeft());
};
const auto nameFontSize = std::max(
(st::storiesMaxNameFontSize * contentHeight
/ st::storiesMaxSize.height()),
st::fsize);
const auto nameBoundingRect = [&](QRect geometry, bool left) {
const auto skipSmall = nameFontSize;
const auto skipBig = skipSmall - std::min(xLeft, 0);
return QRect(
left ? skipBig : skipSmall,
(geometry.height() - innerHeight) / 2,
geometry.width() - skipSmall - skipBig,
innerHeight
).translated(geometry.topLeft());
};
const auto left = QRect({ xLeft, siblingTop }, siblingSize);
const auto right = QRect({ xRight, siblingTop }, siblingSize);
layout.siblingLeft = {
.geometry = left,
.userpic = userpic(left),
.nameBoundingRect = nameBoundingRect(left, true),
.nameFontSize = nameFontSize,
};
layout.siblingRight = {
.geometry = right,
.userpic = userpic(right),
.nameBoundingRect = nameBoundingRect(right, false),
.nameFontSize = nameFontSize,
};
if (!_areas.empty()) {
rebuildActiveAreas(layout);
}
return layout;
});
}
void Controller::rebuildActiveAreas(const Layout &layout) const {
const auto origin = layout.content.topLeft();
const auto scale = layout.content.size();
for (auto &area : _areas) {
const auto &general = area.original;
area.geometry = QRect(
int(base::SafeRound(general.x() * scale.width())),
int(base::SafeRound(general.y() * scale.height())),
int(base::SafeRound(general.width() * scale.width())),
int(base::SafeRound(general.height() * scale.height()))
).translated(origin);
if (const auto reaction = area.reaction.get()) {
reaction->setAreaGeometry(area.geometry);
}
}
}
Data::Story *Controller::story() const {
if (!_session) {
return nullptr;
}
const auto maybeStory = _session->data().stories().lookup(_shown);
return maybeStory ? maybeStory->get() : nullptr;
}
not_null<Ui::RpWidget*> Controller::wrap() const {
return _wrap;
}
Layout Controller::layout() const {
Expects(_layout.current().has_value());
return *_layout.current();
}
rpl::producer<Layout> Controller::layoutValue() const {
return _layout.value() | rpl::filter_optional();
}
ContentLayout Controller::contentLayout() const {
const auto &current = _layout.current();
Assert(current.has_value());
return {
.geometry = current->content,
.fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.)
* kFullContentFade),
.radius = st::storiesRadius,
.headerOutside = (current->headerLayout == HeaderLayout::Outside),
};
}
bool Controller::closeByClickAt(QPoint position) const {
const auto &current = _layout.current();
Assert(current.has_value());
return (position.x() < current->content.x() - st::storiesControlSize)
|| (position.x() > current->content.x() + current->content.width()
+ st::storiesControlSize);
}
Data::FileOrigin Controller::fileOrigin() const {
return _shown;
}
TextWithEntities Controller::captionText() const {
return _captionText;
}
bool Controller::skipCaption() const {
return (_captionFullView != nullptr)
|| (_captionText.empty() && !repost());
}
bool Controller::repost() const {
return _repostView != nullptr;
}
int Controller::repostSkipTop() const {
return _repostView
? (_repostView->height()
+ (_captionText.empty() ? 0 : st::mediaviewTextSkip))
: 0;
}
QMargins Controller::repostCaptionPadding() const {
return { 0, repostSkipTop(), 0, 0 };
}
void Controller::drawRepostInfo(
Painter &p,
int x,
int y,
int availableWidth) const {
Expects(_repostView != nullptr);
_repostView->draw(p, x, y, availableWidth);
}
RepostClickHandler Controller::lookupRepostHandler(QPoint position) const {
return _repostView
? _repostView->lookupHandler(position)
: RepostClickHandler();
}
void Controller::toggleLiked() {
_reactions->toggleLiked();
}
bool Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
auto result = true;
if (mode == ReactionsMode::Message) {
result = _replyArea->sendReaction(chosen.id);
} else if (const auto peer = shownPeer()) {
peer->owner().stories().sendReaction(_shown, chosen.id);
}
unfocusReply();
return result;
}
void Controller::showFullCaption() {
if (_captionText.empty()) {
return;
}
_captionFullView = std::make_unique<CaptionFullView>(this);
updateContentFaded();
}
void Controller::captionClosing() {
updateContentFaded();
}
void Controller::captionClosed() {
if (!_captionFullView) {
return;
} else if (_captionFullView->focused()) {
_wrap->setFocus();
}
_captionFullView = nullptr;
}
std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
return _delegate->storiesShow();
}
auto Controller::stickerOrEmojiChosen() const
-> rpl::producer<ChatHelpers::FileChosen> {
return _delegate->storiesStickerOrEmojiChosen();
}
auto Controller::cachedReactionIconFactory() const
-> HistoryView::Reactions::CachedIconFactory & {
return _delegate->storiesCachedReactionIconFactory();
}
void Controller::rebuildFromContext(
not_null<PeerData*> peer,
FullStoryId storyId) {
using namespace Data;
auto &stories = peer->owner().stories();
auto list = std::optional<StoriesList>();
auto source = (const StoriesSource*)nullptr;
const auto peerId = storyId.peer;
const auto id = storyId.story;
v::match(_context.data, [&](StoriesContextSingle) {
hideSiblings();
}, [&](StoriesContextPeer) {
source = stories.source(peerId);
hideSiblings();
}, [&](StoriesContextSaved) {
if (stories.savedCountKnown(peerId)) {
const auto &saved = stories.saved(peerId);
auto sorted = RespectingPinned(saved);
const auto i = ranges::find(sorted, id);
const auto tillEnd = int(end(sorted) - i);
if (tillEnd > 0) {
_index = int(i - begin(sorted));
list = StoriesList{
.peer = peer,
.ids = saved,
.sorted = std::move(sorted),
.total = stories.savedCount(peerId),
};
if (saved.list.size() < list->total
&& tillEnd < kPreloadStoriesCount) {
stories.savedLoadMore(peerId);
}
}
}
hideSiblings();
}, [&](StoriesContextArchive) {
if (stories.archiveCountKnown(peerId)) {
const auto &archive = stories.archive(peerId);
auto sorted = RespectingPinned(archive);
const auto i = ranges::find(sorted, id);
const auto tillEnd = int(end(sorted) - i);
if (tillEnd > 0) {
_index = int(i - begin(sorted));
list = StoriesList{
.peer = peer,
.ids = archive,
.sorted = std::move(sorted),
.total = stories.archiveCount(peerId),
};
if (archive.list.size() < list->total
&& tillEnd < kPreloadStoriesCount) {
stories.archiveLoadMore(peerId);
}
}
}
hideSiblings();
}, [&](StorySourcesList list) {
source = stories.source(peerId);
const auto &sources = stories.sources(list);
const auto i = ranges::find(
sources,
storyId.peer,
&StoriesSourceInfo::id);
if (i != end(sources)) {
if (_cachedSourcesList.empty()) {
_showingUnreadSources = source && (source->readTill < id);
}
rebuildCachedSourcesList(sources, (i - begin(sources)));
_cachedSourcesList[_cachedSourceIndex].shownId = storyId.story;
showSiblings(&peer->session());
if (int(sources.end() - i) < kPreloadPeersCount) {
stories.loadMore(list);
}
}
});
_sliderIndex = 0;
_sliderCount = 0;
if (list) {
_source = std::nullopt;
if (_list != list) {
_list = std::move(list);
}
if (const auto maybe = peer->owner().stories().lookup(storyId)) {
const auto now = *maybe;
const auto range = ComputeSameDayRange(
now,
_list->ids,
_list->sorted,
_index);
_sliderCount = range.till - range.from + 1;
_sliderIndex = _index - range.from;
}
} else {
if (source) {
const auto i = source->ids.lower_bound(StoryIdDates{ id });
if (i != end(source->ids) && i->id == id) {
_index = int(i - begin(source->ids));
} else {
source = nullptr;
}
}
if (!source) {
_source = std::nullopt;
_list = StoriesList{
.peer = peer,
.ids = { { id } },
.sorted = { id },
.total = 1,
};
_index = 0;
} else {
_list = std::nullopt;
if (_source != *source) {
_source = *source;
}
}
}
preloadNext();
_slider->show({
.index = _sliderCount ? _sliderIndex : _index,
.total = _sliderCount ? _sliderCount : shownCount(),
});
}
void Controller::preloadNext() {
Expects(shown());
auto ids = std::vector<FullStoryId>();
ids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);
const auto peer = shownPeer();
const auto count = shownCount();
const auto till = std::min(_index + kPreloadNextMediaCount, count);
for (auto i = _index + 1; i != till; ++i) {
ids.push_back({ .peer = peer->id, .story = shownId(i) });
}
const auto from = std::max(_index - kPreloadPreviousMediaCount, 0);
for (auto i = _index; i != from;) {
ids.push_back({ .peer = peer->id, .story = shownId(--i) });
}
peer->owner().stories().setPreloadingInViewer(std::move(ids));
}
void Controller::checkMoveByDelta() {
const auto index = _index + _waitingForDelta;
if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
subjumpTo(index);
}
}
void Controller::show(
not_null<Data::Story*> story,
Data::StoriesContext context) {
auto &stories = story->owner().stories();
const auto storyId = story->fullId();
const auto peer = story->peer();
_context = context;
_waitingForId = {};
_waitingForDelta = 0;
rebuildFromContext(peer, storyId);
_contextLifetime.destroy();
const auto subscribeToSource = [&] {
stories.sourceChanged() | rpl::filter(
rpl::mappers::_1 == storyId.peer
) | rpl::start_with_next([=] {
rebuildFromContext(peer, storyId);
}, _contextLifetime);
};
v::match(_context.data, [&](Data::StoriesContextSingle) {
}, [&](Data::StoriesContextPeer) {
subscribeToSource();
}, [&](Data::StoriesContextSaved) {
stories.savedChanged() | rpl::filter(
rpl::mappers::_1 == storyId.peer
) | rpl::start_with_next([=] {
rebuildFromContext(peer, storyId);
checkMoveByDelta();
}, _contextLifetime);
}, [&](Data::StoriesContextArchive) {
stories.archiveChanged(
) | rpl::start_with_next([=] {
rebuildFromContext(peer, storyId);
checkMoveByDelta();
}, _contextLifetime);
}, [&](Data::StorySourcesList) {
subscribeToSource();
});
const auto guard = gsl::finally([&] {
_paused = false;
_started = false;
if (!story->document()) {
_photoPlayback = std::make_unique<PhotoPlayback>(this);
} else {
_photoPlayback = nullptr;
}
});
const auto unsupported = story->unsupported();
if (!unsupported) {
_unsupported = nullptr;
} else {
_unsupported = std::make_unique<Unsupported>(this, peer);
_header->raise();
_slider->raise();
}
captionClosed();
_repostView = validateRepostView(story);
_captionText = story->caption();
_contentFaded = false;
_contentFadeAnimation.stop();
const auto document = story->document();
_header->show({
.peer = peer,
.fromPeer = story->fromPeer(),
.repostPeer = _repostView ? _repostView->fromPeer() : nullptr,
.repostFrom = _repostView ? _repostView->fromName() : nullptr,
.date = story->date(),
.fullIndex = _sliderCount ? _index : 0,
.fullCount = _sliderCount ? shownCount() : 0,
.privacy = story->privacy(),
.edited = story->edited(),
.video = (document != nullptr),
.silent = (document && document->isSilentVideo()),
});
uiShow()->hideLayer(anim::type::instant);
if (!changeShown(story)) {
return;
}
_replyArea->show({
.peer = unsupported ? nullptr : peer.get(),
.id = story->id(),
}, _reactions->likedValue());
const auto wasLikeButton = QPointer(_recentViews->likeButton());
_recentViews->show({
.list = story->recentViewers(),
.reactions = story->reactions(),
.forwards = story->forwards(),
.views = story->views(),
.total = story->interactions(),
.type = RecentViewsTypeFor(peer),
.canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
}, _reactions->likedValue());
if (const auto nowLikeButton = _recentViews->likeButton()) {
if (wasLikeButton != nowLikeButton) {
_reactions->attachToReactionButton(nowLikeButton);
}
}
if (peer->isSelf() || peer->isBroadcast() || peer->isServiceUser()) {
_reactions->setReactionIconWidget(_recentViews->likeIconWidget());
} else if (const auto like = _replyArea->likeAnimationTarget()) {
_reactions->setReactionIconWidget(like);
}
_reactions->showLikeFrom(story);
stories.loadAround(storyId, context);
updatePlayingAllowed();
peer->updateFull();
}
void Controller::jumpTo(
not_null<Data::Story*> story,
Data::StoriesContext context) {
show(story, std::move(context));
_delegate->storiesRedisplay(story);
}
bool Controller::changeShown(Data::Story *story) {
const auto id = story ? story->fullId() : FullStoryId();
const auto session = story ? &story->session() : nullptr;
const auto sessionChanged = (_session != session);
updateAreas(story);
if (_shown == id && !sessionChanged) {
return false;
}
if (_shown) {
Assert(_session != nullptr);
_session->data().stories().unregisterPolling(
_shown,
Data::Stories::Polling::Viewer);
}
if (sessionChanged) {
_sessionLifetime.destroy();
}
_shown = id;
_session = session;
if (sessionChanged) {
subscribeToSession();
}
if (story) {
story->owner().stories().registerPolling(
story,
Data::Stories::Polling::Viewer);
}
_viewed = false;
invalidate_weak_ptrs(&_viewsLoadGuard);
_reactions->hide();
_reactions->setReactionIconWidget(nullptr);
if (_replyArea->focused()) {
unfocusReply();
}
return true;
}
void Controller::subscribeToSession() {
Expects(!_sessionLifetime);
if (!_session) {
return;
}
_session->changes().storyUpdates(
Data::StoryUpdate::Flag::Destroyed
) | rpl::start_with_next([=](Data::StoryUpdate update) {
if (update.story->fullId() == _shown) {
_delegate->storiesClose();
}
}, _sessionLifetime);
_session->data().stories().itemsChanged(
) | rpl::start_with_next([=](PeerId peerId) {
if (_waitingForId.peer == peerId) {
checkWaitingFor();
}
}, _sessionLifetime);
_session->changes().storyUpdates(
Data::StoryUpdate::Flag::Edited
| Data::StoryUpdate::Flag::ViewsChanged
| Data::StoryUpdate::Flag::Reaction
) | rpl::filter([=](const Data::StoryUpdate &update) {
return (update.story == this->story());
}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
if (update.flags & Data::StoryUpdate::Flag::Edited) {
show(update.story, _context);
_delegate->storiesRedisplay(update.story);
} else {
const auto peer = update.story->peer();
_recentViews->show({
.list = update.story->recentViewers(),
.reactions = update.story->reactions(),
.forwards = update.story->forwards(),
.views = update.story->views(),
.total = update.story->interactions(),
.type = RecentViewsTypeFor(peer),
.canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
});
updateAreas(update.story);
}
}, _sessionLifetime);
_sessionLifetime.add([=] {
_session->data().stories().setPreloadingInViewer({});
});
}
void Controller::updateAreas(Data::Story *story) {
const auto &locations = story
? story->locations()
: std::vector<Data::StoryLocation>();
const auto &suggestedReactions = story
? story->suggestedReactions()
: std::vector<Data::SuggestedReaction>();
const auto &channelPosts = story
? story->channelPosts()
: std::vector<Data::ChannelPost>();
if (_locations != locations) {
_locations = locations;
_areas.clear();
}
if (_channelPosts != channelPosts) {
_channelPosts = channelPosts;
_areas.clear();
}
const auto reactionsCount = int(suggestedReactions.size());
if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) {
for (auto i = 0; i != reactionsCount; ++i) {
const auto count = suggestedReactions[i].count;
if (_suggestedReactions[i].count != count) {
_suggestedReactions[i].count = count;
_areas[i + _locations.size()].reaction->updateCount(count);
}
if (_suggestedReactions[i] != suggestedReactions[i]) {
_suggestedReactions = suggestedReactions;
_areas.clear();
break;
}
}
} else if (_suggestedReactions != suggestedReactions) {
_suggestedReactions = suggestedReactions;
_areas.clear();
}
}
PauseState Controller::pauseState() const {
const auto inactive = !_windowActive
|| _replyActive
|| _layerShown
|| _menuShown;
const auto playing = !inactive && !_paused;
return playing
? PauseState::Playing
: !inactive
? PauseState::Paused
: _paused
? PauseState::InactivePaused
: PauseState::Inactive;
}
float64 Controller::currentVolume() const {
return Core::App().settings().videoVolume();
}
void Controller::toggleVolume() {
_delegate->storiesVolumeToggle();
}
void Controller::changeVolume(float64 volume) {
_delegate->storiesVolumeChanged(volume);
}
void Controller::volumeChangeFinished() {
_delegate->storiesVolumeChangeFinished();
}
void Controller::updatePlayingAllowed() {
if (!_shown) {
return;
}
_header->updatePauseState();
setPlayingAllowed(_started
&& _windowActive
&& !_paused
&& !_replyActive
&& (!_captionFullView || _captionFullView->closing())
&& !_layerShown
&& !_menuShown
&& !_tooltipShown);
}
void Controller::setPlayingAllowed(bool allowed) {
if (_photoPlayback) {
_photoPlayback->togglePaused(!allowed);
} else {
_delegate->storiesTogglePaused(!allowed);
}
}
void Controller::showSiblings(not_null<Main::Session*> session) {
showSibling(
_siblingLeft,
session,
(_cachedSourceIndex > 0
? _cachedSourcesList[_cachedSourceIndex - 1]
: CachedSource()));
showSibling(
_siblingRight,
session,
(_cachedSourceIndex + 1 < _cachedSourcesList.size()
? _cachedSourcesList[_cachedSourceIndex + 1]
: CachedSource()));
}
void Controller::hideSiblings() {
_siblingLeft = nullptr;
_siblingRight = nullptr;
}
void Controller::showSibling(
std::unique_ptr<Sibling> &sibling,
not_null<Main::Session*> session,
CachedSource cached) {
if (!cached) {
sibling = nullptr;
return;
}
const auto source = session->data().stories().source(cached.peerId);
if (!source) {
sibling = nullptr;
} else if (!sibling || !sibling->shows(*source, cached.shownId)) {
sibling = std::make_unique<Sibling>(this, *source, cached.shownId);
}
}
void Controller::ready() {
if (_started) {
return;
}
_started = true;
updatePlayingAllowed();
_reactions->ready();
}
void Controller::updateVideoPlayback(const Player::TrackState &state) {
updatePlayback(state);
}
void Controller::updatePhotoPlayback(const Player::TrackState &state) {
updatePlayback(state);
}
void Controller::updatePlayback(const Player::TrackState &state) {
_slider->updatePlayback(state);
updatePowerSaveBlocker(state);
maybeMarkAsRead(state);
if (Player::IsStoppedAtEnd(state.state)) {
if (!subjumpFor(1)) {
_delegate->storiesClose();
}
}
}
ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
const auto &layout = _layout.current();
if (!layout
|| (_locations.empty()
&& _suggestedReactions.empty()
&& _channelPosts.empty())) {
return nullptr;
} else if (_areas.empty()) {
const auto now = story();
_areas.reserve(_locations.size()
+ _suggestedReactions.size()
+ _channelPosts.size());
for (const auto &location : _locations) {
_areas.push_back({
.original = location.area.geometry,
.rotation = location.area.rotation,
.handler = std::make_shared<LocationClickHandler>(
location.point),
});
}
for (const auto &suggestedReaction : _suggestedReactions) {
const auto id = suggestedReaction.reaction;
auto widget = _reactions->makeSuggestedReactionWidget(
suggestedReaction);
const auto raw = widget.get();
_areas.push_back({
.original = suggestedReaction.area.geometry,
.rotation = suggestedReaction.area.rotation,
.handler = std::make_shared<LambdaClickHandler>([=] {
raw->playEffect();
if (const auto now = story()) {
if (now->sentReactionId() != id) {
now->owner().stories().sendReaction(
now->fullId(),
id);
}
}
}),
.reaction = std::move(widget),
});
}
if (const auto session = now ? &now->session() : nullptr) {
for (const auto &channelPost : _channelPosts) {
_areas.push_back({
.original = channelPost.area.geometry,
.rotation = channelPost.area.rotation,
.handler = MakeChannelPostHandler(
session,
channelPost.itemId),
});
}
}
rebuildActiveAreas(*layout);
}
const auto circleContains = [&](QRect circle) {
const auto radius = std::min(circle.width(), circle.height()) / 2;
const auto delta = circle.center() - point;
return QPoint::dotProduct(delta, delta) < (radius * radius);
};
for (const auto &area : _areas) {
const auto center = area.geometry.center();
const auto angle = -area.rotation;
const auto contains = area.reaction
? circleContains(area.geometry)
: area.geometry.contains(Rotated(point, center, angle));
if (contains) {
return area.handler;
}
}
return nullptr;
}
void Controller::maybeMarkAsRead(const Player::TrackState &state) {
const auto length = state.length;
const auto position = Player::IsStoppedAtEnd(state.state)
? state.length
: Player::IsStoppedOrStopping(state.state)
? 0
: state.position;
if (position > state.frequency * kMarkAsReadAfterSeconds) {
if (position > kMarkAsReadAfterProgress * length) {
markAsRead();
}
}
}
void Controller::markAsRead() {
Expects(shown());
if (_viewed) {
return;
}
_viewed = true;
shownPeer()->owner().stories().markAsRead(_shown, _started);
}
bool Controller::subjumpAvailable(int delta) const {
const auto index = _index + delta;
if (index < 0) {
return _siblingLeft && _siblingLeft->shownId().valid();
} else if (index >= shownCount()) {
return _siblingRight && _siblingRight->shownId().valid();
}
return index >= 0 && index < shownCount();
}
bool Controller::subjumpFor(int delta) {
if (delta > 0) {
markAsRead();
}
const auto index = _index + delta;
if (index < 0) {
if (_siblingLeft && _siblingLeft->shownId().valid()) {
return jumpFor(-1);
} else if (!shown() || !shownCount()) {
return false;
}
subjumpTo(0);
return true;
} else if (index >= shownCount()) {
return _siblingRight
&& _siblingRight->shownId().valid()
&& jumpFor(1);
} else {
subjumpTo(index);
}
return true;
}
void Controller::subjumpTo(int index) {
Expects(shown());
Expects(index >= 0 && index < shownCount());
const auto peer = shownPeer();
const auto id = FullStoryId{
.peer = peer->id,
.story = shownId(index),
};
auto &stories = peer->owner().stories();
if (!id.story) {
const auto delta = index - _index;
if (_waitingForDelta != delta) {
_waitingForDelta = delta;
_waitingForId = {};
loadMoreToList();
}
} else if (stories.lookup(id)) {
_delegate->storiesJumpTo(&peer->session(), id, _context);
} else if (_waitingForId != id) {
_waitingForId = id;
_waitingForDelta = 0;
stories.loadAround(id, _context);
}
}
void Controller::checkWaitingFor() {
Expects(_waitingForId.valid());
Expects(shown());
const auto peer = shownPeer();
auto &stories = peer->owner().stories();
const auto maybe = stories.lookup(_waitingForId);
if (!maybe) {
if (maybe.error() == Data::NoStory::Deleted) {
_waitingForId = {};
}
return;
}
_delegate->storiesJumpTo(
&peer->session(),
base::take(_waitingForId),
_context);
}
bool Controller::jumpFor(int delta) {
if (delta == -1) {
if (const auto left = _siblingLeft.get()) {
_delegate->storiesJumpTo(
&left->peer()->session(),
left->shownId(),
_context);
return true;
}
} else if (delta == 1) {
if (shown() && _index + 1 >= shownCount()) {
markAsRead();
}
if (const auto right = _siblingRight.get()) {
_delegate->storiesJumpTo(
&right->peer()->session(),
right->shownId(),
_context);
return true;
}
}
return false;
}
bool Controller::paused() const {
return _paused;
}
void Controller::togglePaused(bool paused) {
if (_paused != paused) {
_paused = paused;
updatePlayingAllowed();
}
}
void Controller::contentPressed(bool pressed) {
togglePaused(pressed);
if (_captionFullView) {
_captionFullView->close();
}
if (pressed) {
_reactions->outsidePressed();
}
}
void Controller::setMenuShown(bool shown) {
if (_menuShown != shown) {
_menuShown = shown;
updatePlayingAllowed();
}
}
void Controller::repaintSibling(not_null<Sibling*> sibling) {
if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) {
_delegate->storiesRepaint();
}
}
void Controller::repaint() {
if (_captionFullView) {
_captionFullView->repaint();
}
_delegate->storiesRepaint();
}
SiblingView Controller::sibling(SiblingType type) const {
const auto &pointer = (type == SiblingType::Left)
? _siblingLeft
: _siblingRight;
if (const auto value = pointer.get()) {
const auto over = _delegate->storiesSiblingOver(type);
const auto layout = (type == SiblingType::Left)
? _layout.current()->siblingLeft
: _layout.current()->siblingRight;
return value->view(layout, over);
}
return {};
}
const Data::StoryViews &Controller::views(int limit, bool initial) {
invalidate_weak_ptrs(&_viewsLoadGuard);
if (initial) {
refreshViewsFromData();
}
if (_viewsSlice.total > _viewsSlice.list.size()
&& _viewsSlice.list.size() < limit) {
const auto done = viewsGotMoreCallback();
const auto peer = shownPeer();
auto &stories = peer->owner().stories();
if (peer->isChannel()) {
stories.loadReactionsSlice(
peer,
_shown.story,
_viewsSlice.nextOffset,
done);
} else {
stories.loadViewsSlice(
peer,
_shown.story,
_viewsSlice.nextOffset,
done);
}
}
return _viewsSlice;
}
rpl::producer<> Controller::moreViewsLoaded() const {
return _moreViewsLoaded.events();
}
Fn<void(Data::StoryViews)> Controller::viewsGotMoreCallback() {
return crl::guard(&_viewsLoadGuard, [=](Data::StoryViews result) {
if (_viewsSlice.list.empty()) {
const auto peer = shownPeer();
auto &stories = peer->owner().stories();
if (const auto maybeStory = stories.lookup(_shown)) {
if (peer->isChannel()) {
_viewsSlice = (*maybeStory)->channelReactionsList();
} else {
_viewsSlice = (*maybeStory)->viewsList();
}
} else {
_viewsSlice = {};
}
} else {
_viewsSlice.list.insert(
end(_viewsSlice.list),
begin(result.list),
end(result.list));
_viewsSlice.total = result.nextOffset.isEmpty()
? int(_viewsSlice.list.size())
: std::max(result.total, int(_viewsSlice.list.size()));
_viewsSlice.nextOffset = result.nextOffset;
}
_moreViewsLoaded.fire({});
});
}
bool Controller::shown() const {
return _source || _list;
}
PeerData *Controller::shownPeer() const {
return _source
? _source->peer.get()
: _list
? _list->peer.get()
: nullptr;
}
int Controller::shownCount() const {
return _source ? int(_source->ids.size()) : _list ? _list->total : 0;
}
StoryId Controller::shownId(int index) const {
Expects(index >= 0 && index < shownCount());
return _source
? (_source->ids.begin() + index)->id
: (index < int(_list->sorted.size()))
? _list->sorted[index]
: StoryId();
}
std::unique_ptr<RepostView> Controller::validateRepostView(
not_null<Data::Story*> story) {
return (story->repost() || !story->channelPosts().empty())
? std::make_unique<RepostView>(this, story)
: nullptr;
}
void Controller::loadMoreToList() {
Expects(shown());
using namespace Data;
const auto peer = shownPeer();
const auto peerId = _shown.peer;
auto &stories = peer->owner().stories();
v::match(_context.data, [&](StoriesContextSaved) {
stories.savedLoadMore(peerId);
}, [&](StoriesContextArchive) {
stories.archiveLoadMore(peerId);
}, [](const auto &) {
});
}
void Controller::rebuildCachedSourcesList(
const std::vector<Data::StoriesSourceInfo> &lists,
int index) {
Expects(index >= 0 && index < lists.size());
const auto currentPeerId = lists[index].id;
// Remove removed.
_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
CachedSource source) {
return !ranges::contains(
lists,
source.peerId,
&Data::StoriesSourceInfo::id);
}), end(_cachedSourcesList));
// Find current, full rebuild if can't find.
const auto i = ranges::find(
_cachedSourcesList,
currentPeerId,
&CachedSource::peerId);
if (i == end(_cachedSourcesList)) {
_cachedSourcesList.clear();
} else {
_cachedSourceIndex = int(i - begin(_cachedSourcesList));
}
if (_cachedSourcesList.empty()) {
// Full rebuild.
const auto predicate = [&](const Data::StoriesSourceInfo &info) {
return !_showingUnreadSources
|| (info.unreadCount > 0)
|| (info.id == currentPeerId);
};
const auto mapper = [](const Data::StoriesSourceInfo &info) {
return CachedSource{ info.id };
};
_cachedSourcesList = lists
| ranges::views::filter(predicate)
| ranges::views::transform(mapper)
| ranges::to_vector;
_cachedSourceIndex = ranges::find(
_cachedSourcesList,
currentPeerId,
&CachedSource::peerId
) - begin(_cachedSourcesList);
} else if (ranges::equal(
lists,
_cachedSourcesList,
ranges::equal_to(),
&Data::StoriesSourceInfo::id,
&CachedSource::peerId)) {
// No rebuild needed.
} else {
// All that go before the current push to front.
for (auto before = index; before > 0;) {
const auto &info = lists[--before];
if (_showingUnreadSources && !info.unreadCount) {
continue;
} else if (!ranges::contains(
_cachedSourcesList,
info.id,
&CachedSource::peerId)) {
_cachedSourcesList.insert(
begin(_cachedSourcesList),
{ info.id });
++_cachedSourceIndex;
}
}
// All that go after the current push to back.
for (auto after = index + 1, count = int(lists.size())
; after != count
; ++after) {
const auto &info = lists[after];
if (_showingUnreadSources && !info.unreadCount) {
continue;
} else if (!ranges::contains(
_cachedSourcesList,
info.id,
&CachedSource::peerId)) {
_cachedSourcesList.push_back({ info.id });
}
}
}
Ensures(_cachedSourcesList.size() <= lists.size());
Ensures(_cachedSourceIndex >= 0
&& _cachedSourceIndex < _cachedSourcesList.size());
}
void Controller::refreshViewsFromData() {
Expects(shown());
const auto peer = shownPeer();
auto &stories = peer->owner().stories();
const auto maybeStory = stories.lookup(_shown);
const auto check = peer->isSelf()
|| CanViewReactionsFor(peer);
if (!maybeStory || !check) {
_viewsSlice = {};
} else if (peer->isChannel()) {
_viewsSlice = (*maybeStory)->channelReactionsList();
} else {
_viewsSlice = (*maybeStory)->viewsList();
}
}
void Controller::unfocusReply() {
_wrap->setFocus();
}
void Controller::shareRequested() {
const auto show = _delegate->storiesShow();
if (auto box = PrepareShareBox(show, _shown, true)) {
show->show(std::move(box));
}
}
void Controller::deleteRequested() {
const auto story = this->story();
if (!story) {
return;
}
const auto id = story->fullId();
const auto weak = base::make_weak(this);
const auto owner = &story->owner();
const auto confirmed = [=](Fn<void()> close) {
if (const auto strong = weak.get()) {
if (const auto story = strong->story()) {
if (story->fullId() == id) {
moveFromShown();
}
}
}
owner->stories().deleteList({ id });
close();
};
uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_stories_delete_one_sure(),
.confirmed = confirmed,
.confirmText = tr::lng_selected_delete(),
.labelStyle = &st::storiesBoxLabel,
}));
}
void Controller::reportRequested() {
ReportRequested(uiShow(), _shown, &st::storiesReportBox);
}
void Controller::toggleInProfileRequested(bool inProfile) {
const auto story = this->story();
if (!story || !story->peer()->isSelf()) {
return;
}
if (!inProfile && v::is<Data::StoriesContextSaved>(_context.data)) {
moveFromShown();
}
story->owner().stories().toggleInProfileList(
{ story->fullId() },
inProfile);
const auto channel = story->peer()->isChannel();
uiShow()->showToast(PrepareToggleInProfileToast(channel, 1, inProfile));
}
void Controller::moveFromShown() {
if (!subjumpFor(1)) {
[[maybe_unused]] const auto jumped = subjumpFor(-1);
}
}
bool Controller::ignoreWindowMove(QPoint position) const {
return _replyArea->ignoreWindowMove(position)
|| _header->ignoreWindowMove(position);
}
void Controller::tryProcessKeyInput(not_null<QKeyEvent*> e) {
_replyArea->tryProcessKeyInput(e);
}
bool Controller::allowStealthMode() const {
const auto story = this->story();
return story
&& !story->peer()->isSelf()
&& story->peer()->session().premiumPossible();
}
void Controller::setupStealthMode() {
SetupStealthMode(uiShow());
}
auto Controller::attachReactionsToMenu(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition)
-> AttachStripResult {
return _reactions->attachToMenu(menu, desiredPosition);
}
rpl::lifetime &Controller::lifetime() {
return _lifetime;
}
void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
const auto block = !Player::IsPausedOrPausing(state.state)
&& !Player::IsStoppedOrStopping(state.state);
base::UpdatePowerSaveBlocker(
_powerSaveBlocker,
block,
base::PowerSaveBlockType::PreventDisplaySleep,
[] { return u"Stories playback is active"_q; },
[=] { return _wrap->window()->windowHandle(); });
}
Ui::Toast::Config PrepareToggleInProfileToast(
bool channel,
int count,
bool inProfile) {
return {
.text = (inProfile
? (count == 1
? (channel
? tr::lng_stories_channel_save_done
: tr::lng_stories_save_done)(
tr::now,
Ui::Text::Bold)
: (channel
? tr::lng_stories_channel_save_done_many
: tr::lng_stories_save_done_many)(
tr::now,
lt_count,
count,
Ui::Text::Bold)).append(
'\n').append((channel
? tr::lng_stories_channel_save_done_about
: tr::lng_stories_save_done_about)(tr::now))
: (count == 1
? (channel
? tr::lng_stories_channel_archive_done
: tr::lng_stories_archive_done)(
tr::now,
Ui::Text::WithEntities)
: (channel
? tr::lng_stories_channel_archive_done_many
: tr::lng_stories_archive_done_many)(
tr::now,
lt_count,
count,
Ui::Text::WithEntities))),
.st = &st::storiesActionToast,
.duration = (inProfile
? Data::Stories::kInProfileToastDuration
: Ui::Toast::kDefaultDuration),
};
}
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,
const style::ReportBox *stOverride) {
const auto owner = &show->session().data();
const auto st = stOverride ? stOverride : &st::defaultReportBox;
show->show(Box(Ui::ReportReasonBox, *st, Ui::ReportSource::Story, [=](
Ui::ReportReason reason) {
const auto done = [=](const QString &text) {
owner->stories().report(show, id, reason, text);
show->hideLayer();
};
show->showBox(Box(Ui::ReportDetailsBox, *st, done));
}));
}
object_ptr<Ui::BoxContent> PrepareShortInfoBox(not_null<PeerData*> peer) {
const auto open = [=] {
if (const auto window = Core::App().windowFor(peer)) {
window->invokeForSessionController(
&peer->session().account(),
peer,
[&](not_null<Window::SessionController*> controller) {
Core::App().hideMediaView();
controller->showPeerHistory(peer);
});
}
};
return ::PrepareShortInfoBox(
peer,
open,
[] { return false; },
&st::storiesShortInfoBox);
}
ClickHandlerPtr MakeChannelPostHandler(
not_null<Main::Session*> session,
FullMsgId item) {
return std::make_shared<LambdaClickHandler>(crl::guard(session, [=] {
const auto peer = session->data().peer(item.peer);
if (const auto window = Core::App().windowFor(peer)) {
if (const auto controller = window->sessionController()) {
if (&controller->session() == &peer->session()) {
Core::App().hideMediaView();
controller->showPeerHistory(
item.peer,
Window::SectionShow::Way::ClearStack,
item.msg);
}
}
}
}));
}
} // namespace Media::Stories