1844 lines
		
	
	
	
		
			49 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1844 lines
		
	
	
	
		
			49 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,
 | 
						|
		int index) {
 | 
						|
	Expects(index >= 0 && index < ids.list.size());
 | 
						|
 | 
						|
	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 })) {
 | 
						|
			const auto day = base::unixtime::parse((*maybeStory)->date());
 | 
						|
			if (day.date() != now.date()) {
 | 
						|
				break;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		--result.from;
 | 
						|
	}
 | 
						|
	for (auto i = b + index + 1, e = end(ids.list); i != e; ++i) {
 | 
						|
		if (const auto maybeStory = stories->lookup({ peerId, *i })) {
 | 
						|
			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) {
 | 
						|
		reactionChosen(chosen.mode, chosen.reaction);
 | 
						|
	}, _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 ¤t = _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 ¤t = _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();
 | 
						|
}
 | 
						|
 | 
						|
void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
 | 
						|
	if (mode == ReactionsMode::Message) {
 | 
						|
		_replyArea->sendReaction(chosen.id);
 | 
						|
	} else if (const auto peer = shownPeer()) {
 | 
						|
		peer->owner().stories().sendReaction(_shown, chosen.id);
 | 
						|
	}
 | 
						|
	unfocusReply();
 | 
						|
}
 | 
						|
 | 
						|
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);
 | 
						|
			const auto &ids = saved.list;
 | 
						|
			const auto i = ids.find(id);
 | 
						|
			if (i != end(ids)) {
 | 
						|
				list = StoriesList{
 | 
						|
					.peer = peer,
 | 
						|
					.ids = saved,
 | 
						|
					.total = stories.savedCount(peerId),
 | 
						|
				};
 | 
						|
				_index = int(i - begin(ids));
 | 
						|
				if (ids.size() < list->total
 | 
						|
					&& (end(ids) - i) < kPreloadStoriesCount) {
 | 
						|
					stories.savedLoadMore(peerId);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		hideSiblings();
 | 
						|
	}, [&](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)) {
 | 
						|
				list = StoriesList{
 | 
						|
					.peer = peer,
 | 
						|
					.ids = archive,
 | 
						|
					.total = stories.archiveCount(peerId),
 | 
						|
				};
 | 
						|
				_index = int(i - begin(ids));
 | 
						|
				if (ids.size() < list->total
 | 
						|
					&& (end(ids) - i) < 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, _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 } },
 | 
						|
				.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,
 | 
						|
		.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),
 | 
						|
	}, _reactions->likedValue());
 | 
						|
	if (const auto nowLikeButton = _recentViews->likeButton()) {
 | 
						|
		if (wasLikeButton != nowLikeButton) {
 | 
						|
			_reactions->attachToReactionButton(nowLikeButton);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (peer->isSelf() || peer->isChannel() || 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();
 | 
						|
}
 | 
						|
 | 
						|
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),
 | 
						|
			});
 | 
						|
			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->ids.list.size()))
 | 
						|
		? *(_list->ids.list.begin() + 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::togglePinnedRequested(bool pinned) {
 | 
						|
	const auto story = this->story();
 | 
						|
	if (!story || !story->peer()->isSelf()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (!pinned && v::is<Data::StoriesContextSaved>(_context.data)) {
 | 
						|
		moveFromShown();
 | 
						|
	}
 | 
						|
	story->owner().stories().togglePinnedList({ story->fullId() }, pinned);
 | 
						|
	const auto channel = story->peer()->isChannel();
 | 
						|
	uiShow()->showToast(PrepareTogglePinnedToast(channel, 1, pinned));
 | 
						|
}
 | 
						|
 | 
						|
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 PrepareTogglePinnedToast(
 | 
						|
		bool channel,
 | 
						|
		int count,
 | 
						|
		bool pinned) {
 | 
						|
	return {
 | 
						|
		.text = (pinned
 | 
						|
			? (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 = (pinned
 | 
						|
			? Data::Stories::kPinnedToastDuration
 | 
						|
			: 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
 |