media_stories_controller.cpp 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "media/stories/media_stories_controller.h"
  8. #include "base/platform/base_platform_info.h"
  9. #include "base/power_save_blocker.h"
  10. #include "base/qt_signal_producer.h"
  11. #include "base/unixtime.h"
  12. #include "boxes/peers/prepare_short_info_box.h"
  13. #include "boxes/report_messages_box.h"
  14. #include "chat_helpers/compose/compose_show.h"
  15. #include "core/application.h"
  16. #include "core/click_handler_types.h"
  17. #include "core/core_settings.h"
  18. #include "core/local_url_handlers.h"
  19. #include "core/update_checker.h"
  20. #include "data/data_changes.h"
  21. #include "data/data_document.h"
  22. #include "data/data_file_origin.h"
  23. #include "data/data_peer.h"
  24. #include "data/data_session.h"
  25. #include "data/data_stories.h"
  26. #include "history/view/reactions/history_view_reactions_strip.h"
  27. #include "lang/lang_keys.h"
  28. #include "main/main_session.h"
  29. #include "media/stories/media_stories_caption_full_view.h"
  30. #include "media/stories/media_stories_delegate.h"
  31. #include "media/stories/media_stories_header.h"
  32. #include "media/stories/media_stories_sibling.h"
  33. #include "media/stories/media_stories_slider.h"
  34. #include "media/stories/media_stories_reactions.h"
  35. #include "media/stories/media_stories_recent_views.h"
  36. #include "media/stories/media_stories_reply.h"
  37. #include "media/stories/media_stories_repost_view.h"
  38. #include "media/stories/media_stories_share.h"
  39. #include "media/stories/media_stories_stealth.h"
  40. #include "media/stories/media_stories_view.h"
  41. #include "media/audio/media_audio.h"
  42. #include "settings/settings_credits_graphics.h"
  43. #include "ui/boxes/confirm_box.h"
  44. #include "ui/boxes/report_box_graphics.h"
  45. #include "ui/text/text_utilities.h"
  46. #include "ui/toast/toast.h"
  47. #include "ui/widgets/buttons.h"
  48. #include "ui/widgets/labels.h"
  49. #include "ui/round_rect.h"
  50. #include "window/window_controller.h"
  51. #include "window/window_session_controller.h"
  52. #include "styles/style_chat_helpers.h" // defaultReportBox
  53. #include "styles/style_media_view.h"
  54. #include "styles/style_boxes.h" // UserpicButton
  55. #include <QtGui/QWindow>
  56. namespace Media::Stories {
  57. namespace {
  58. constexpr auto kPhotoProgressInterval = crl::time(100);
  59. constexpr auto kPhotoDuration = 5 * crl::time(1000);
  60. constexpr auto kFullContentFade = 0.6;
  61. constexpr auto kSiblingMultiplierDefault = 0.448;
  62. constexpr auto kSiblingMultiplierMax = 0.72;
  63. constexpr auto kSiblingOutsidePart = 0.24;
  64. constexpr auto kSiblingUserpicSize = 0.3;
  65. constexpr auto kInnerHeightMultiplier = 1.6;
  66. constexpr auto kPreloadPeersCount = 3;
  67. constexpr auto kPreloadStoriesCount = 5;
  68. constexpr auto kPreloadNextMediaCount = 3;
  69. constexpr auto kPreloadPreviousMediaCount = 1;
  70. constexpr auto kMarkAsReadAfterSeconds = 0.2;
  71. constexpr auto kMarkAsReadAfterProgress = 0.;
  72. struct SameDayRange {
  73. int from = 0;
  74. int till = 0;
  75. };
  76. [[nodiscard]] SameDayRange ComputeSameDayRange(
  77. not_null<Data::Story*> story,
  78. const Data::StoriesIds &ids,
  79. const std::vector<StoryId> &sorted,
  80. int index) {
  81. Expects(index >= 0 && index < ids.list.size());
  82. Expects(index >= 0 && index < sorted.size());
  83. const auto pinned = int(ids.pinnedToTop.size());
  84. if (index < pinned) {
  85. return SameDayRange{ .from = 0, .till = pinned - 1 };
  86. }
  87. auto result = SameDayRange{ .from = index, .till = index };
  88. const auto peerId = story->peer()->id;
  89. const auto stories = &story->owner().stories();
  90. const auto now = base::unixtime::parse(story->date());
  91. for (auto i = index; i != 0;) {
  92. const auto storyId = sorted[--i];
  93. if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
  94. const auto day = base::unixtime::parse((*maybeStory)->date());
  95. if (day.date() != now.date()) {
  96. break;
  97. }
  98. }
  99. --result.from;
  100. }
  101. for (auto i = index + 1, c = int(sorted.size()); i != c; ++i) {
  102. const auto storyId = sorted[i];
  103. if (const auto maybeStory = stories->lookup({ peerId, storyId })) {
  104. const auto day = base::unixtime::parse((*maybeStory)->date());
  105. if (day.date() != now.date()) {
  106. break;
  107. }
  108. }
  109. ++result.till;
  110. }
  111. return result;
  112. }
  113. [[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
  114. if (std::abs(angle) < 1.) {
  115. return point;
  116. }
  117. const auto alpha = angle / 180. * M_PI;
  118. const auto acos = cos(alpha);
  119. const auto asin = sin(alpha);
  120. point -= origin;
  121. return origin + QPoint(
  122. int(base::SafeRound(acos * point.x() - asin * point.y())),
  123. int(base::SafeRound(asin * point.x() + acos * point.y())));
  124. }
  125. [[nodiscard]] bool ResolveWeatherInCelsius() {
  126. const auto saved = Core::App().settings().weatherInCelsius();
  127. return saved.value_or(!ranges::contains(
  128. std::array{ u"US"_q, u"BS"_q, u"KY"_q, u"LR"_q, u"BZ"_q },
  129. Platform::SystemCountry().toUpper()));
  130. }
  131. } // namespace
  132. class Controller::PhotoPlayback final {
  133. public:
  134. explicit PhotoPlayback(not_null<Controller*> controller);
  135. [[nodiscard]] bool paused() const;
  136. void togglePaused(bool paused);
  137. private:
  138. void callback();
  139. const not_null<Controller*> _controller;
  140. base::Timer _timer;
  141. crl::time _started = 0;
  142. crl::time _paused = 0;
  143. };
  144. class Controller::Unsupported final {
  145. public:
  146. Unsupported(not_null<Controller*> controller, not_null<PeerData*> peer);
  147. private:
  148. void setup(not_null<PeerData*> peer);
  149. const not_null<Controller*> _controller;
  150. std::unique_ptr<Ui::RpWidget> _bg;
  151. std::unique_ptr<Ui::FlatLabel> _text;
  152. std::unique_ptr<Ui::RoundButton> _button;
  153. Ui::RoundRect _bgRound;
  154. };
  155. Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> controller)
  156. : _controller(controller)
  157. , _timer([=] { callback(); })
  158. , _started(crl::now())
  159. , _paused(_started) {
  160. }
  161. bool Controller::PhotoPlayback::paused() const {
  162. return _paused != 0;
  163. }
  164. void Controller::PhotoPlayback::togglePaused(bool paused) {
  165. if (!_paused == !paused) {
  166. return;
  167. } else if (paused) {
  168. const auto now = crl::now();
  169. if (now - _started >= kPhotoDuration) {
  170. return;
  171. }
  172. _paused = now;
  173. _timer.cancel();
  174. } else {
  175. _started += crl::now() - _paused;
  176. _paused = 0;
  177. _timer.callEach(kPhotoProgressInterval);
  178. }
  179. callback();
  180. }
  181. void Controller::PhotoPlayback::callback() {
  182. const auto now = crl::now();
  183. const auto elapsed = now - _started;
  184. const auto finished = (now - _started >= kPhotoDuration);
  185. if (finished) {
  186. _timer.cancel();
  187. }
  188. using State = Player::State;
  189. const auto state = finished
  190. ? State::StoppedAtEnd
  191. : _paused
  192. ? State::Paused
  193. : State::Playing;
  194. _controller->updatePhotoPlayback({
  195. .state = state,
  196. .position = elapsed,
  197. .receivedTill = kPhotoDuration,
  198. .length = kPhotoDuration,
  199. .frequency = 1000,
  200. });
  201. }
  202. Controller::Unsupported::Unsupported(
  203. not_null<Controller*> controller,
  204. not_null<PeerData*> peer)
  205. : _controller(controller)
  206. , _bgRound(st::storiesRadius, st::storiesComposeBg) {
  207. setup(peer);
  208. }
  209. void Controller::Unsupported::setup(not_null<PeerData*> peer) {
  210. const auto wrap = _controller->wrap();
  211. _bg = std::make_unique<Ui::RpWidget>(wrap);
  212. _bg->show();
  213. _bg->paintRequest() | rpl::start_with_next([=] {
  214. auto p = QPainter(_bg.get());
  215. _bgRound.paint(p, _bg->rect());
  216. }, _bg->lifetime());
  217. _controller->layoutValue(
  218. ) | rpl::start_with_next([=](const Layout &layout) {
  219. _bg->setGeometry(layout.content);
  220. }, _bg->lifetime());
  221. _text = std::make_unique<Ui::FlatLabel>(
  222. wrap,
  223. tr::lng_stories_unsupported(),
  224. st::storiesUnsupportedLabel);
  225. _text->show();
  226. _button = std::make_unique<Ui::RoundButton>(
  227. wrap,
  228. tr::lng_update_telegram(),
  229. st::storiesUnsupportedUpdate);
  230. _button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
  231. _button->show();
  232. rpl::combine(
  233. _controller->layoutValue(),
  234. _text->sizeValue(),
  235. _button->sizeValue()
  236. ) | rpl::start_with_next([=](
  237. const Layout &layout,
  238. QSize text,
  239. QSize button) {
  240. const auto wrap = layout.content;
  241. const auto totalHeight = st::storiesUnsupportedTop
  242. + text.height()
  243. + st::storiesUnsupportedSkip
  244. + button.height();
  245. const auto top = (wrap.height() - totalHeight) / 2;
  246. _text->move(
  247. wrap.x() + (wrap.width() - text.width()) / 2,
  248. wrap.y() + top + st::storiesUnsupportedTop);
  249. _button->move(
  250. wrap.x() + (wrap.width() - button.width()) / 2,
  251. wrap.y() + top + totalHeight - button.height());
  252. }, _button->lifetime());
  253. _button->setClickedCallback([=] {
  254. Core::UpdateApplication();
  255. });
  256. }
  257. Controller::Controller(not_null<Delegate*> delegate)
  258. : _delegate(delegate)
  259. , _wrap(_delegate->storiesWrap())
  260. , _header(std::make_unique<Header>(this))
  261. , _slider(std::make_unique<Slider>(this))
  262. , _replyArea(std::make_unique<ReplyArea>(this))
  263. , _reactions(std::make_unique<Reactions>(this))
  264. , _recentViews(std::make_unique<RecentViews>(this))
  265. , _weatherInCelsius(ResolveWeatherInCelsius()){
  266. initLayout();
  267. using namespace rpl::mappers;
  268. rpl::combine(
  269. _replyArea->activeValue(),
  270. _reactions->activeValue(),
  271. _1 || _2
  272. ) | rpl::distinct_until_changed(
  273. ) | rpl::start_with_next([=](bool active) {
  274. _replyActive = active;
  275. updateContentFaded();
  276. }, _lifetime);
  277. _reactions->setReplyFieldState(
  278. _replyArea->focusedValue(),
  279. _replyArea->hasSendTextValue());
  280. if (const auto like = _replyArea->likeAnimationTarget()) {
  281. _reactions->attachToReactionButton(like);
  282. }
  283. _reactions->chosen(
  284. ) | rpl::start_with_next([=](Reactions::Chosen chosen) {
  285. if (reactionChosen(chosen.mode, chosen.reaction)) {
  286. _reactions->animateAndProcess(std::move(chosen));
  287. }
  288. }, _lifetime);
  289. _delegate->storiesLayerShown(
  290. ) | rpl::start_with_next([=](bool shown) {
  291. if (_layerShown != shown) {
  292. _layerShown = shown;
  293. updatePlayingAllowed();
  294. }
  295. }, _lifetime);
  296. _header->tooltipShownValue(
  297. ) | rpl::start_with_next([=](bool shown) {
  298. if (_tooltipShown != shown) {
  299. _tooltipShown = shown;
  300. updatePlayingAllowed();
  301. }
  302. }, _lifetime);
  303. _wrap->windowActiveValue(
  304. ) | rpl::start_with_next([=](bool active) {
  305. _windowActive = active;
  306. updatePlayingAllowed();
  307. }, _lifetime);
  308. _contentFadeAnimation.stop();
  309. }
  310. Controller::~Controller() {
  311. _captionFullView = nullptr;
  312. _repostView = nullptr;
  313. changeShown(nullptr);
  314. }
  315. void Controller::updateContentFaded() {
  316. const auto faded = _replyActive
  317. || (_captionFullView && !_captionFullView->closing());
  318. if (_contentFaded == faded) {
  319. return;
  320. }
  321. _contentFaded = faded;
  322. _contentFadeAnimation.start(
  323. [=] { _delegate->storiesRepaint(); },
  324. _contentFaded ? 0. : 1.,
  325. _contentFaded ? 1. : 0.,
  326. st::fadeWrapDuration);
  327. updatePlayingAllowed();
  328. }
  329. void Controller::initLayout() {
  330. const auto headerHeight = st::storiesHeaderMargin.top()
  331. + st::storiesHeaderPhoto.photoSize
  332. + st::storiesHeaderMargin.bottom();
  333. const auto sliderHeight = st::storiesSliderMargin.top()
  334. + st::storiesSliderWidth
  335. + st::storiesSliderMargin.bottom();
  336. const auto outsideHeaderHeight = headerHeight
  337. + sliderHeight
  338. + st::storiesSliderOutsideSkip;
  339. const auto fieldMinHeight = st::storiesFieldMargin.top()
  340. + st::storiesAttach.height
  341. + st::storiesFieldMargin.bottom();
  342. const auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom()
  343. + outsideHeaderHeight
  344. + st::storiesMaxSize.height()
  345. + fieldMinHeight;
  346. _layout = _wrap->sizeValue(
  347. ) | rpl::map([=](QSize size) {
  348. const auto topNotchSkip = _delegate->storiesTopNotchSkip();
  349. size = QSize(
  350. std::max(size.width(), st::mediaviewMinWidth),
  351. std::max(size.height(), st::mediaviewMinHeight));
  352. auto layout = Layout();
  353. layout.headerLayout = (size.height() >= minHeightForOutsideHeader)
  354. ? HeaderLayout::Outside
  355. : HeaderLayout::Normal;
  356. const auto topSkip = topNotchSkip
  357. + st::storiesFieldMargin.bottom()
  358. + (layout.headerLayout == HeaderLayout::Outside
  359. ? outsideHeaderHeight
  360. : 0);
  361. const auto bottomSkip = fieldMinHeight;
  362. const auto maxWidth = size.width() - 2 * st::storiesSideSkip;
  363. const auto availableHeight = size.height() - topSkip - bottomSkip;
  364. const auto maxContentHeight = std::min(
  365. availableHeight,
  366. st::storiesMaxSize.height());
  367. const auto nowWidth = maxContentHeight * st::storiesMaxSize.width()
  368. / st::storiesMaxSize.height();
  369. const auto contentWidth = std::min(nowWidth, maxWidth);
  370. const auto contentHeight = (contentWidth < nowWidth)
  371. ? (contentWidth * st::storiesMaxSize.height()
  372. / st::storiesMaxSize.width())
  373. : maxContentHeight;
  374. const auto addedTopSkip = (availableHeight - contentHeight) / 2;
  375. layout.content = QRect(
  376. (size.width() - contentWidth) / 2,
  377. addedTopSkip + topSkip,
  378. contentWidth,
  379. contentHeight);
  380. const auto reactionsWidth = st::storiesReactionsWidth;
  381. layout.reactions = QRect(
  382. (size.width() - reactionsWidth) / 2,
  383. layout.content.y(),
  384. reactionsWidth,
  385. contentHeight);
  386. if (layout.headerLayout == HeaderLayout::Outside) {
  387. layout.header = QRect(
  388. layout.content.topLeft() - QPoint(0, outsideHeaderHeight),
  389. QSize(contentWidth, outsideHeaderHeight));
  390. layout.slider = QRect(
  391. layout.header.topLeft() + QPoint(0, headerHeight),
  392. QSize(contentWidth, sliderHeight));
  393. } else {
  394. layout.slider = QRect(
  395. layout.content.topLeft(),
  396. QSize(contentWidth, sliderHeight));
  397. layout.header = QRect(
  398. layout.slider.topLeft() + QPoint(0, sliderHeight),
  399. QSize(contentWidth, headerHeight));
  400. }
  401. layout.controlsWidth = std::max(
  402. layout.content.width(),
  403. st::storiesControlsMinWidth);
  404. layout.controlsBottomPosition = QPoint(
  405. (size.width() - layout.controlsWidth) / 2,
  406. (layout.content.y()
  407. + layout.content.height()
  408. + fieldMinHeight
  409. - st::storiesFieldMargin.bottom()));
  410. layout.views = QRect(
  411. layout.controlsBottomPosition - QPoint(0, fieldMinHeight),
  412. QSize(layout.controlsWidth, fieldMinHeight));
  413. layout.autocompleteRect = QRect(
  414. layout.controlsBottomPosition.x(),
  415. 0,
  416. layout.controlsWidth,
  417. layout.controlsBottomPosition.y());
  418. const auto sidesAvailable = size.width() - layout.content.width();
  419. const auto widthForSiblings = sidesAvailable
  420. - 2 * st::storiesFieldMargin.bottom();
  421. const auto siblingWidthMax = widthForSiblings
  422. / (2 * (1. - kSiblingOutsidePart));
  423. const auto siblingMultiplierMax = std::max(
  424. kSiblingMultiplierDefault,
  425. st::storiesSiblingWidthMin / float64(layout.content.width()));
  426. const auto siblingMultiplier = std::min({
  427. siblingMultiplierMax,
  428. kSiblingMultiplierMax,
  429. siblingWidthMax / layout.content.width(),
  430. });
  431. const auto siblingSize = layout.content.size() * siblingMultiplier;
  432. const auto siblingTop = (size.height() - siblingSize.height()) / 2;
  433. const auto outsideMax = int(base::SafeRound(
  434. siblingSize.width() * kSiblingOutsidePart));
  435. const auto leftAvailable = layout.content.x() - siblingSize.width();
  436. const auto xDesired = leftAvailable / 3;
  437. const auto xPossible = std::min(
  438. xDesired,
  439. (leftAvailable - st::storiesControlSize));
  440. const auto xLeft = std::max(xPossible, -outsideMax);
  441. const auto xRight = size.width() - siblingSize.width() - xLeft;
  442. const auto userpicSize = int(base::SafeRound(
  443. siblingSize.width() * kSiblingUserpicSize));
  444. const auto innerHeight = userpicSize * kInnerHeightMultiplier;
  445. const auto userpic = [&](QRect geometry) {
  446. return QRect(
  447. (geometry.width() - userpicSize) / 2,
  448. (geometry.height() - innerHeight) / 2,
  449. userpicSize,
  450. userpicSize
  451. ).translated(geometry.topLeft());
  452. };
  453. const auto nameFontSize = std::max(
  454. (st::storiesMaxNameFontSize * contentHeight
  455. / st::storiesMaxSize.height()),
  456. st::fsize);
  457. const auto nameBoundingRect = [&](QRect geometry, bool left) {
  458. const auto skipSmall = nameFontSize;
  459. const auto skipBig = skipSmall - std::min(xLeft, 0);
  460. return QRect(
  461. left ? skipBig : skipSmall,
  462. (geometry.height() - innerHeight) / 2,
  463. geometry.width() - skipSmall - skipBig,
  464. innerHeight
  465. ).translated(geometry.topLeft());
  466. };
  467. const auto left = QRect({ xLeft, siblingTop }, siblingSize);
  468. const auto right = QRect({ xRight, siblingTop }, siblingSize);
  469. layout.siblingLeft = {
  470. .geometry = left,
  471. .userpic = userpic(left),
  472. .nameBoundingRect = nameBoundingRect(left, true),
  473. .nameFontSize = nameFontSize,
  474. };
  475. layout.siblingRight = {
  476. .geometry = right,
  477. .userpic = userpic(right),
  478. .nameBoundingRect = nameBoundingRect(right, false),
  479. .nameFontSize = nameFontSize,
  480. };
  481. if (!_areas.empty()) {
  482. rebuildActiveAreas(layout);
  483. }
  484. return layout;
  485. });
  486. }
  487. void Controller::rebuildActiveAreas(const Layout &layout) const {
  488. const auto origin = layout.content.topLeft();
  489. const auto scale = layout.content.size();
  490. for (auto &area : _areas) {
  491. const auto &general = area.original;
  492. area.geometry = QRect(
  493. int(base::SafeRound(general.x() * scale.width())),
  494. int(base::SafeRound(general.y() * scale.height())),
  495. int(base::SafeRound(general.width() * scale.width())),
  496. int(base::SafeRound(general.height() * scale.height()))
  497. ).translated(origin);
  498. area.radius = scale.width() * area.radiusOriginal / 100.;
  499. if (const auto view = area.view.get()) {
  500. view->setAreaGeometry(area.geometry, area.radius);
  501. }
  502. }
  503. }
  504. Data::Story *Controller::story() const {
  505. if (!_session) {
  506. return nullptr;
  507. }
  508. const auto maybeStory = _session->data().stories().lookup(_shown);
  509. return maybeStory ? maybeStory->get() : nullptr;
  510. }
  511. not_null<Ui::RpWidget*> Controller::wrap() const {
  512. return _wrap;
  513. }
  514. Layout Controller::layout() const {
  515. Expects(_layout.current().has_value());
  516. return *_layout.current();
  517. }
  518. rpl::producer<Layout> Controller::layoutValue() const {
  519. return _layout.value() | rpl::filter_optional();
  520. }
  521. ContentLayout Controller::contentLayout() const {
  522. const auto &current = _layout.current();
  523. Assert(current.has_value());
  524. return {
  525. .geometry = current->content,
  526. .fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.)
  527. * kFullContentFade),
  528. .radius = st::storiesRadius,
  529. .headerOutside = (current->headerLayout == HeaderLayout::Outside),
  530. };
  531. }
  532. bool Controller::closeByClickAt(QPoint position) const {
  533. const auto &current = _layout.current();
  534. Assert(current.has_value());
  535. return (position.x() < current->content.x() - st::storiesControlSize)
  536. || (position.x() > current->content.x() + current->content.width()
  537. + st::storiesControlSize);
  538. }
  539. Data::FileOrigin Controller::fileOrigin() const {
  540. return _shown;
  541. }
  542. TextWithEntities Controller::captionText() const {
  543. return _captionText;
  544. }
  545. bool Controller::skipCaption() const {
  546. return (_captionFullView != nullptr)
  547. || (_captionText.empty() && !repost());
  548. }
  549. bool Controller::repost() const {
  550. return _repostView != nullptr;
  551. }
  552. int Controller::repostSkipTop() const {
  553. return _repostView
  554. ? (_repostView->height()
  555. + (_captionText.empty() ? 0 : st::mediaviewTextSkip))
  556. : 0;
  557. }
  558. QMargins Controller::repostCaptionPadding() const {
  559. return { 0, repostSkipTop(), 0, 0 };
  560. }
  561. void Controller::drawRepostInfo(
  562. Painter &p,
  563. int x,
  564. int y,
  565. int availableWidth) const {
  566. Expects(_repostView != nullptr);
  567. _repostView->draw(p, x, y, availableWidth);
  568. }
  569. RepostClickHandler Controller::lookupRepostHandler(QPoint position) const {
  570. return _repostView
  571. ? _repostView->lookupHandler(position)
  572. : RepostClickHandler();
  573. }
  574. void Controller::toggleLiked() {
  575. _reactions->toggleLiked();
  576. }
  577. bool Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
  578. auto result = true;
  579. if (mode == ReactionsMode::Message) {
  580. result = _replyArea->sendReaction(chosen.id);
  581. } else if (const auto peer = shownPeer()) {
  582. peer->owner().stories().sendReaction(_shown, chosen.id);
  583. }
  584. unfocusReply();
  585. return result;
  586. }
  587. void Controller::showFullCaption() {
  588. if (_captionText.empty()) {
  589. return;
  590. }
  591. _captionFullView = std::make_unique<CaptionFullView>(this);
  592. updateContentFaded();
  593. }
  594. void Controller::captionClosing() {
  595. updateContentFaded();
  596. }
  597. void Controller::captionClosed() {
  598. if (!_captionFullView) {
  599. return;
  600. } else if (_captionFullView->focused()) {
  601. _wrap->setFocus();
  602. }
  603. _captionFullView = nullptr;
  604. }
  605. std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
  606. return _delegate->storiesShow();
  607. }
  608. auto Controller::stickerOrEmojiChosen() const
  609. -> rpl::producer<ChatHelpers::FileChosen> {
  610. return _delegate->storiesStickerOrEmojiChosen();
  611. }
  612. void Controller::rebuildFromContext(
  613. not_null<PeerData*> peer,
  614. FullStoryId storyId) {
  615. using namespace Data;
  616. auto &stories = peer->owner().stories();
  617. auto list = std::optional<StoriesList>();
  618. auto source = (const StoriesSource*)nullptr;
  619. const auto peerId = storyId.peer;
  620. const auto id = storyId.story;
  621. v::match(_context.data, [&](StoriesContextSingle) {
  622. hideSiblings();
  623. }, [&](StoriesContextPeer) {
  624. source = stories.source(peerId);
  625. hideSiblings();
  626. }, [&](StoriesContextSaved) {
  627. if (stories.savedCountKnown(peerId)) {
  628. const auto &saved = stories.saved(peerId);
  629. auto sorted = RespectingPinned(saved);
  630. const auto i = ranges::find(sorted, id);
  631. const auto tillEnd = int(end(sorted) - i);
  632. if (tillEnd > 0) {
  633. _index = int(i - begin(sorted));
  634. list = StoriesList{
  635. .peer = peer,
  636. .ids = saved,
  637. .sorted = std::move(sorted),
  638. .total = stories.savedCount(peerId),
  639. };
  640. if (saved.list.size() < list->total
  641. && tillEnd < kPreloadStoriesCount) {
  642. stories.savedLoadMore(peerId);
  643. }
  644. }
  645. }
  646. hideSiblings();
  647. }, [&](StoriesContextArchive) {
  648. if (stories.archiveCountKnown(peerId)) {
  649. const auto &archive = stories.archive(peerId);
  650. auto sorted = RespectingPinned(archive);
  651. const auto i = ranges::find(sorted, id);
  652. const auto tillEnd = int(end(sorted) - i);
  653. if (tillEnd > 0) {
  654. _index = int(i - begin(sorted));
  655. list = StoriesList{
  656. .peer = peer,
  657. .ids = archive,
  658. .sorted = std::move(sorted),
  659. .total = stories.archiveCount(peerId),
  660. };
  661. if (archive.list.size() < list->total
  662. && tillEnd < kPreloadStoriesCount) {
  663. stories.archiveLoadMore(peerId);
  664. }
  665. }
  666. }
  667. hideSiblings();
  668. }, [&](StorySourcesList list) {
  669. source = stories.source(peerId);
  670. const auto &sources = stories.sources(list);
  671. const auto i = ranges::find(
  672. sources,
  673. storyId.peer,
  674. &StoriesSourceInfo::id);
  675. if (i != end(sources)) {
  676. if (_cachedSourcesList.empty()) {
  677. _showingUnreadSources = source && (source->readTill < id);
  678. }
  679. rebuildCachedSourcesList(sources, (i - begin(sources)));
  680. _cachedSourcesList[_cachedSourceIndex].shownId = storyId.story;
  681. showSiblings(&peer->session());
  682. if (int(sources.end() - i) < kPreloadPeersCount) {
  683. stories.loadMore(list);
  684. }
  685. }
  686. });
  687. _sliderIndex = 0;
  688. _sliderCount = 0;
  689. if (list) {
  690. _source = std::nullopt;
  691. if (_list != list) {
  692. _list = std::move(list);
  693. }
  694. if (const auto maybe = peer->owner().stories().lookup(storyId)) {
  695. const auto now = *maybe;
  696. const auto range = ComputeSameDayRange(
  697. now,
  698. _list->ids,
  699. _list->sorted,
  700. _index);
  701. _sliderCount = range.till - range.from + 1;
  702. _sliderIndex = _index - range.from;
  703. }
  704. } else {
  705. if (source) {
  706. const auto i = source->ids.lower_bound(StoryIdDates{ id });
  707. if (i != end(source->ids) && i->id == id) {
  708. _index = int(i - begin(source->ids));
  709. } else {
  710. source = nullptr;
  711. }
  712. }
  713. if (!source) {
  714. _source = std::nullopt;
  715. _list = StoriesList{
  716. .peer = peer,
  717. .ids = { { id } },
  718. .sorted = { id },
  719. .total = 1,
  720. };
  721. _index = 0;
  722. } else {
  723. _list = std::nullopt;
  724. if (_source != *source) {
  725. _source = *source;
  726. }
  727. }
  728. }
  729. preloadNext();
  730. _slider->show({
  731. .index = _sliderCount ? _sliderIndex : _index,
  732. .total = _sliderCount ? _sliderCount : shownCount(),
  733. });
  734. }
  735. void Controller::preloadNext() {
  736. Expects(shown());
  737. auto ids = std::vector<FullStoryId>();
  738. ids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);
  739. const auto peer = shownPeer();
  740. const auto count = shownCount();
  741. const auto till = std::min(_index + kPreloadNextMediaCount, count);
  742. for (auto i = _index + 1; i != till; ++i) {
  743. ids.push_back({ .peer = peer->id, .story = shownId(i) });
  744. }
  745. const auto from = std::max(_index - kPreloadPreviousMediaCount, 0);
  746. for (auto i = _index; i != from;) {
  747. ids.push_back({ .peer = peer->id, .story = shownId(--i) });
  748. }
  749. peer->owner().stories().setPreloadingInViewer(std::move(ids));
  750. }
  751. void Controller::checkMoveByDelta() {
  752. const auto index = _index + _waitingForDelta;
  753. if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
  754. subjumpTo(index);
  755. }
  756. }
  757. void Controller::show(
  758. not_null<Data::Story*> story,
  759. Data::StoriesContext context) {
  760. auto &stories = story->owner().stories();
  761. const auto storyId = story->fullId();
  762. const auto peer = story->peer();
  763. _context = context;
  764. _waitingForId = {};
  765. _waitingForDelta = 0;
  766. rebuildFromContext(peer, storyId);
  767. _contextLifetime.destroy();
  768. const auto subscribeToSource = [&] {
  769. stories.sourceChanged() | rpl::filter(
  770. rpl::mappers::_1 == storyId.peer
  771. ) | rpl::start_with_next([=] {
  772. rebuildFromContext(peer, storyId);
  773. }, _contextLifetime);
  774. };
  775. v::match(_context.data, [&](Data::StoriesContextSingle) {
  776. }, [&](Data::StoriesContextPeer) {
  777. subscribeToSource();
  778. }, [&](Data::StoriesContextSaved) {
  779. stories.savedChanged() | rpl::filter(
  780. rpl::mappers::_1 == storyId.peer
  781. ) | rpl::start_with_next([=] {
  782. rebuildFromContext(peer, storyId);
  783. checkMoveByDelta();
  784. }, _contextLifetime);
  785. }, [&](Data::StoriesContextArchive) {
  786. stories.archiveChanged(
  787. ) | rpl::start_with_next([=] {
  788. rebuildFromContext(peer, storyId);
  789. checkMoveByDelta();
  790. }, _contextLifetime);
  791. }, [&](Data::StorySourcesList) {
  792. subscribeToSource();
  793. });
  794. const auto guard = gsl::finally([&] {
  795. _paused = false;
  796. _started = false;
  797. if (!story->document()) {
  798. _photoPlayback = std::make_unique<PhotoPlayback>(this);
  799. } else {
  800. _photoPlayback = nullptr;
  801. }
  802. });
  803. const auto unsupported = story->unsupported();
  804. if (!unsupported) {
  805. _unsupported = nullptr;
  806. } else {
  807. _unsupported = std::make_unique<Unsupported>(this, peer);
  808. _header->raise();
  809. _slider->raise();
  810. }
  811. captionClosed();
  812. _repostView = validateRepostView(story);
  813. _captionText = story->caption();
  814. _contentFaded = false;
  815. _contentFadeAnimation.stop();
  816. const auto document = story->document();
  817. _header->show({
  818. .peer = peer,
  819. .fromPeer = story->fromPeer(),
  820. .repostPeer = _repostView ? _repostView->fromPeer() : nullptr,
  821. .repostFrom = _repostView ? _repostView->fromName() : nullptr,
  822. .date = story->date(),
  823. .fullIndex = _sliderCount ? _index : 0,
  824. .fullCount = _sliderCount ? shownCount() : 0,
  825. .privacy = story->privacy(),
  826. .edited = story->edited(),
  827. .video = (document != nullptr),
  828. .silent = (document && document->isSilentVideo()),
  829. });
  830. uiShow()->hideLayer(anim::type::instant);
  831. if (!changeShown(story)) {
  832. return;
  833. }
  834. _replyArea->show({
  835. .peer = unsupported ? nullptr : peer.get(),
  836. .id = story->id(),
  837. }, _reactions->likedValue());
  838. const auto wasLikeButton = QPointer(_recentViews->likeButton());
  839. _recentViews->show({
  840. .list = story->recentViewers(),
  841. .reactions = story->reactions(),
  842. .forwards = story->forwards(),
  843. .views = story->views(),
  844. .total = story->interactions(),
  845. .type = RecentViewsTypeFor(peer),
  846. .canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
  847. }, _reactions->likedValue());
  848. if (const auto nowLikeButton = _recentViews->likeButton()) {
  849. if (wasLikeButton != nowLikeButton) {
  850. _reactions->attachToReactionButton(nowLikeButton);
  851. }
  852. }
  853. if (peer->isSelf() || peer->isBroadcast() || peer->isServiceUser()) {
  854. _reactions->setReactionIconWidget(_recentViews->likeIconWidget());
  855. } else if (const auto like = _replyArea->likeAnimationTarget()) {
  856. _reactions->setReactionIconWidget(like);
  857. }
  858. _reactions->showLikeFrom(story);
  859. stories.loadAround(storyId, context);
  860. updatePlayingAllowed();
  861. peer->updateFull();
  862. }
  863. void Controller::jumpTo(
  864. not_null<Data::Story*> story,
  865. Data::StoriesContext context) {
  866. show(story, std::move(context));
  867. _delegate->storiesRedisplay(story);
  868. }
  869. bool Controller::changeShown(Data::Story *story) {
  870. const auto id = story ? story->fullId() : FullStoryId();
  871. const auto session = story ? &story->session() : nullptr;
  872. const auto sessionChanged = (_session != session);
  873. updateAreas(story);
  874. if (_shown == id && !sessionChanged) {
  875. return false;
  876. }
  877. if (_shown) {
  878. Assert(_session != nullptr);
  879. _session->data().stories().unregisterPolling(
  880. _shown,
  881. Data::Stories::Polling::Viewer);
  882. }
  883. if (sessionChanged) {
  884. _sessionLifetime.destroy();
  885. }
  886. _shown = id;
  887. _session = session;
  888. if (sessionChanged) {
  889. subscribeToSession();
  890. }
  891. if (story) {
  892. story->owner().stories().registerPolling(
  893. story,
  894. Data::Stories::Polling::Viewer);
  895. }
  896. _viewed = false;
  897. invalidate_weak_ptrs(&_viewsLoadGuard);
  898. _reactions->hide();
  899. _reactions->setReactionIconWidget(nullptr);
  900. if (_replyArea->focused()) {
  901. unfocusReply();
  902. }
  903. return true;
  904. }
  905. void Controller::subscribeToSession() {
  906. Expects(!_sessionLifetime);
  907. if (!_session) {
  908. return;
  909. }
  910. _session->changes().storyUpdates(
  911. Data::StoryUpdate::Flag::Destroyed
  912. ) | rpl::start_with_next([=](Data::StoryUpdate update) {
  913. if (update.story->fullId() == _shown) {
  914. _delegate->storiesClose();
  915. }
  916. }, _sessionLifetime);
  917. _session->data().stories().itemsChanged(
  918. ) | rpl::start_with_next([=](PeerId peerId) {
  919. if (_waitingForId.peer == peerId) {
  920. checkWaitingFor();
  921. }
  922. }, _sessionLifetime);
  923. _session->changes().storyUpdates(
  924. Data::StoryUpdate::Flag::Edited
  925. | Data::StoryUpdate::Flag::ViewsChanged
  926. | Data::StoryUpdate::Flag::Reaction
  927. ) | rpl::filter([=](const Data::StoryUpdate &update) {
  928. return (update.story == this->story());
  929. }) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
  930. if (update.flags & Data::StoryUpdate::Flag::Edited) {
  931. show(update.story, _context);
  932. _delegate->storiesRedisplay(update.story);
  933. } else {
  934. const auto peer = update.story->peer();
  935. _recentViews->show({
  936. .list = update.story->recentViewers(),
  937. .reactions = update.story->reactions(),
  938. .forwards = update.story->forwards(),
  939. .views = update.story->views(),
  940. .total = update.story->interactions(),
  941. .type = RecentViewsTypeFor(peer),
  942. .canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
  943. });
  944. updateAreas(update.story);
  945. }
  946. }, _sessionLifetime);
  947. _sessionLifetime.add([=] {
  948. _session->data().stories().setPreloadingInViewer({});
  949. });
  950. }
  951. void Controller::updateAreas(Data::Story *story) {
  952. const auto &locations = story
  953. ? story->locations()
  954. : std::vector<Data::StoryLocation>();
  955. const auto &suggestedReactions = story
  956. ? story->suggestedReactions()
  957. : std::vector<Data::SuggestedReaction>();
  958. const auto &channelPosts = story
  959. ? story->channelPosts()
  960. : std::vector<Data::ChannelPost>();
  961. const auto &urlAreas = story
  962. ? story->urlAreas()
  963. : std::vector<Data::UrlArea>();
  964. const auto &weatherAreas = story
  965. ? story->weatherAreas()
  966. : std::vector<Data::WeatherArea>();
  967. if (_locations != locations) {
  968. _locations = locations;
  969. _areas.clear();
  970. }
  971. if (_channelPosts != channelPosts) {
  972. _channelPosts = channelPosts;
  973. _areas.clear();
  974. }
  975. if (_urlAreas != urlAreas) {
  976. _urlAreas = urlAreas;
  977. _areas.clear();
  978. }
  979. if (_weatherAreas != weatherAreas) {
  980. _weatherAreas = weatherAreas;
  981. _areas.clear();
  982. }
  983. const auto reactionsCount = int(suggestedReactions.size());
  984. if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) {
  985. for (auto i = 0; i != reactionsCount; ++i) {
  986. const auto count = suggestedReactions[i].count;
  987. if (_suggestedReactions[i].count != count) {
  988. _suggestedReactions[i].count = count;
  989. const auto view = _areas[i + _locations.size()].view.get();
  990. view->updateReactionsCount(count);
  991. }
  992. if (_suggestedReactions[i] != suggestedReactions[i]) {
  993. _suggestedReactions = suggestedReactions;
  994. _areas.clear();
  995. break;
  996. }
  997. }
  998. } else if (_suggestedReactions != suggestedReactions) {
  999. _suggestedReactions = suggestedReactions;
  1000. _areas.clear();
  1001. }
  1002. }
  1003. PauseState Controller::pauseState() const {
  1004. const auto inactive = !_windowActive
  1005. || _replyActive
  1006. || _layerShown
  1007. || _menuShown;
  1008. const auto playing = !inactive && !_paused;
  1009. return playing
  1010. ? PauseState::Playing
  1011. : !inactive
  1012. ? PauseState::Paused
  1013. : _paused
  1014. ? PauseState::InactivePaused
  1015. : PauseState::Inactive;
  1016. }
  1017. float64 Controller::currentVolume() const {
  1018. return Core::App().settings().videoVolume();
  1019. }
  1020. void Controller::toggleVolume() {
  1021. _delegate->storiesVolumeToggle();
  1022. }
  1023. void Controller::changeVolume(float64 volume) {
  1024. _delegate->storiesVolumeChanged(volume);
  1025. }
  1026. void Controller::volumeChangeFinished() {
  1027. _delegate->storiesVolumeChangeFinished();
  1028. }
  1029. void Controller::updatePlayingAllowed() {
  1030. if (!_shown) {
  1031. return;
  1032. }
  1033. _header->updatePauseState();
  1034. setPlayingAllowed(_started
  1035. && _windowActive
  1036. && !_paused
  1037. && !_replyActive
  1038. && (!_captionFullView || _captionFullView->closing())
  1039. && !_layerShown
  1040. && !_menuShown
  1041. && !_tooltipShown);
  1042. }
  1043. void Controller::setPlayingAllowed(bool allowed) {
  1044. if (_photoPlayback) {
  1045. _photoPlayback->togglePaused(!allowed);
  1046. } else {
  1047. _delegate->storiesTogglePaused(!allowed);
  1048. }
  1049. }
  1050. void Controller::showSiblings(not_null<Main::Session*> session) {
  1051. showSibling(
  1052. _siblingLeft,
  1053. session,
  1054. (_cachedSourceIndex > 0
  1055. ? _cachedSourcesList[_cachedSourceIndex - 1]
  1056. : CachedSource()));
  1057. showSibling(
  1058. _siblingRight,
  1059. session,
  1060. (_cachedSourceIndex + 1 < _cachedSourcesList.size()
  1061. ? _cachedSourcesList[_cachedSourceIndex + 1]
  1062. : CachedSource()));
  1063. }
  1064. void Controller::hideSiblings() {
  1065. _siblingLeft = nullptr;
  1066. _siblingRight = nullptr;
  1067. }
  1068. void Controller::showSibling(
  1069. std::unique_ptr<Sibling> &sibling,
  1070. not_null<Main::Session*> session,
  1071. CachedSource cached) {
  1072. if (!cached) {
  1073. sibling = nullptr;
  1074. return;
  1075. }
  1076. const auto source = session->data().stories().source(cached.peerId);
  1077. if (!source) {
  1078. sibling = nullptr;
  1079. } else if (!sibling || !sibling->shows(*source, cached.shownId)) {
  1080. sibling = std::make_unique<Sibling>(this, *source, cached.shownId);
  1081. }
  1082. }
  1083. void Controller::ready() {
  1084. if (_started) {
  1085. return;
  1086. }
  1087. _started = true;
  1088. updatePlayingAllowed();
  1089. _reactions->ready();
  1090. }
  1091. void Controller::updateVideoPlayback(const Player::TrackState &state) {
  1092. updatePlayback(state);
  1093. }
  1094. void Controller::updatePhotoPlayback(const Player::TrackState &state) {
  1095. updatePlayback(state);
  1096. }
  1097. void Controller::updatePlayback(const Player::TrackState &state) {
  1098. _slider->updatePlayback(state);
  1099. updatePowerSaveBlocker(state);
  1100. maybeMarkAsRead(state);
  1101. if (Player::IsStoppedAtEnd(state.state)) {
  1102. if (!subjumpFor(1)) {
  1103. _delegate->storiesClose();
  1104. }
  1105. }
  1106. }
  1107. ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
  1108. const auto &layout = _layout.current();
  1109. if (!layout
  1110. || (_locations.empty()
  1111. && _suggestedReactions.empty()
  1112. && _channelPosts.empty()
  1113. && _urlAreas.empty()
  1114. && _weatherAreas.empty())) {
  1115. return nullptr;
  1116. } else if (_areas.empty()) {
  1117. const auto now = story();
  1118. _areas.reserve(_locations.size()
  1119. + _suggestedReactions.size()
  1120. + _channelPosts.size()
  1121. + _urlAreas.size());
  1122. for (const auto &location : _locations) {
  1123. _areas.push_back({
  1124. .original = location.area.geometry,
  1125. .rotation = location.area.rotation,
  1126. .handler = std::make_shared<LocationClickHandler>(
  1127. location.point),
  1128. });
  1129. }
  1130. for (const auto &suggestedReaction : _suggestedReactions) {
  1131. const auto id = suggestedReaction.reaction;
  1132. auto widget = _reactions->makeSuggestedReactionWidget(
  1133. suggestedReaction);
  1134. const auto raw = widget.get();
  1135. _areas.push_back({
  1136. .original = suggestedReaction.area.geometry,
  1137. .rotation = suggestedReaction.area.rotation,
  1138. .handler = std::make_shared<LambdaClickHandler>([=] {
  1139. raw->playEffect();
  1140. if (const auto now = story()) {
  1141. if (now->sentReactionId() != id) {
  1142. now->owner().stories().sendReaction(
  1143. now->fullId(),
  1144. id);
  1145. }
  1146. }
  1147. }),
  1148. .view = std::move(widget),
  1149. });
  1150. }
  1151. if (const auto session = now ? &now->session() : nullptr) {
  1152. for (const auto &channelPost : _channelPosts) {
  1153. _areas.push_back({
  1154. .original = channelPost.area.geometry,
  1155. .rotation = channelPost.area.rotation,
  1156. .handler = MakeChannelPostHandler(
  1157. session,
  1158. channelPost.itemId),
  1159. });
  1160. }
  1161. }
  1162. const auto weak = base::make_weak(this);
  1163. for (const auto &url : _urlAreas) {
  1164. _areas.push_back({
  1165. .original = url.area.geometry,
  1166. .rotation = url.area.rotation,
  1167. .handler = MakeUrlAreaHandler(weak, url.url),
  1168. });
  1169. }
  1170. for (const auto &weather : _weatherAreas) {
  1171. _areas.push_back({
  1172. .original = weather.area.geometry,
  1173. .radiusOriginal = weather.area.radius,
  1174. .rotation = weather.area.rotation,
  1175. .handler = std::make_shared<LambdaClickHandler>([=] {
  1176. toggleWeatherMode();
  1177. }),
  1178. .view = _reactions->makeWeatherAreaWidget(
  1179. weather,
  1180. _weatherInCelsius.value()),
  1181. });
  1182. }
  1183. rebuildActiveAreas(*layout);
  1184. }
  1185. for (const auto &area : _areas) {
  1186. const auto center = area.geometry.center();
  1187. const auto angle = -area.rotation;
  1188. const auto contains = area.view
  1189. ? area.view->contains(point)
  1190. : area.geometry.contains(Rotated(point, center, angle));
  1191. if (contains) {
  1192. return area.handler;
  1193. }
  1194. }
  1195. return nullptr;
  1196. }
  1197. void Controller::toggleWeatherMode() const {
  1198. const auto now = !_weatherInCelsius.current();
  1199. Core::App().settings().setWeatherInCelsius(now);
  1200. Core::App().saveSettingsDelayed();
  1201. _weatherInCelsius = now;
  1202. }
  1203. void Controller::maybeMarkAsRead(const Player::TrackState &state) {
  1204. const auto length = state.length;
  1205. const auto position = Player::IsStoppedAtEnd(state.state)
  1206. ? state.length
  1207. : Player::IsStoppedOrStopping(state.state)
  1208. ? 0
  1209. : state.position;
  1210. if (position > state.frequency * kMarkAsReadAfterSeconds) {
  1211. if (position > kMarkAsReadAfterProgress * length) {
  1212. markAsRead();
  1213. }
  1214. }
  1215. }
  1216. void Controller::markAsRead() {
  1217. Expects(shown());
  1218. if (_viewed) {
  1219. return;
  1220. }
  1221. _viewed = true;
  1222. shownPeer()->owner().stories().markAsRead(_shown, _started);
  1223. }
  1224. bool Controller::subjumpAvailable(int delta) const {
  1225. const auto index = _index + delta;
  1226. if (index < 0) {
  1227. return _siblingLeft && _siblingLeft->shownId().valid();
  1228. } else if (index >= shownCount()) {
  1229. return _siblingRight && _siblingRight->shownId().valid();
  1230. }
  1231. return index >= 0 && index < shownCount();
  1232. }
  1233. bool Controller::subjumpFor(int delta) {
  1234. if (delta > 0) {
  1235. markAsRead();
  1236. }
  1237. const auto index = _index + delta;
  1238. if (index < 0) {
  1239. if (_siblingLeft && _siblingLeft->shownId().valid()) {
  1240. return jumpFor(-1);
  1241. } else if (!shown() || !shownCount()) {
  1242. return false;
  1243. }
  1244. subjumpTo(0);
  1245. return true;
  1246. } else if (index >= shownCount()) {
  1247. return _siblingRight
  1248. && _siblingRight->shownId().valid()
  1249. && jumpFor(1);
  1250. } else {
  1251. subjumpTo(index);
  1252. }
  1253. return true;
  1254. }
  1255. void Controller::subjumpTo(int index) {
  1256. Expects(shown());
  1257. Expects(index >= 0 && index < shownCount());
  1258. const auto peer = shownPeer();
  1259. const auto id = FullStoryId{
  1260. .peer = peer->id,
  1261. .story = shownId(index),
  1262. };
  1263. auto &stories = peer->owner().stories();
  1264. if (!id.story) {
  1265. const auto delta = index - _index;
  1266. if (_waitingForDelta != delta) {
  1267. _waitingForDelta = delta;
  1268. _waitingForId = {};
  1269. loadMoreToList();
  1270. }
  1271. } else if (stories.lookup(id)) {
  1272. _delegate->storiesJumpTo(&peer->session(), id, _context);
  1273. } else if (_waitingForId != id) {
  1274. _waitingForId = id;
  1275. _waitingForDelta = 0;
  1276. stories.loadAround(id, _context);
  1277. }
  1278. }
  1279. void Controller::checkWaitingFor() {
  1280. Expects(_waitingForId.valid());
  1281. Expects(shown());
  1282. const auto peer = shownPeer();
  1283. auto &stories = peer->owner().stories();
  1284. const auto maybe = stories.lookup(_waitingForId);
  1285. if (!maybe) {
  1286. if (maybe.error() == Data::NoStory::Deleted) {
  1287. _waitingForId = {};
  1288. }
  1289. return;
  1290. }
  1291. _delegate->storiesJumpTo(
  1292. &peer->session(),
  1293. base::take(_waitingForId),
  1294. _context);
  1295. }
  1296. bool Controller::jumpFor(int delta) {
  1297. if (delta == -1) {
  1298. if (const auto left = _siblingLeft.get()) {
  1299. _delegate->storiesJumpTo(
  1300. &left->peer()->session(),
  1301. left->shownId(),
  1302. _context);
  1303. return true;
  1304. }
  1305. } else if (delta == 1) {
  1306. if (shown() && _index + 1 >= shownCount()) {
  1307. markAsRead();
  1308. }
  1309. if (const auto right = _siblingRight.get()) {
  1310. _delegate->storiesJumpTo(
  1311. &right->peer()->session(),
  1312. right->shownId(),
  1313. _context);
  1314. return true;
  1315. }
  1316. }
  1317. return false;
  1318. }
  1319. bool Controller::paused() const {
  1320. return _paused;
  1321. }
  1322. void Controller::togglePaused(bool paused) {
  1323. if (_paused != paused) {
  1324. _paused = paused;
  1325. updatePlayingAllowed();
  1326. }
  1327. }
  1328. void Controller::contentPressed(bool pressed) {
  1329. togglePaused(pressed);
  1330. if (_captionFullView) {
  1331. _captionFullView->close();
  1332. }
  1333. if (pressed) {
  1334. _reactions->outsidePressed();
  1335. }
  1336. }
  1337. void Controller::setMenuShown(bool shown) {
  1338. if (_menuShown != shown) {
  1339. _menuShown = shown;
  1340. updatePlayingAllowed();
  1341. }
  1342. }
  1343. void Controller::repaintSibling(not_null<Sibling*> sibling) {
  1344. if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) {
  1345. _delegate->storiesRepaint();
  1346. }
  1347. }
  1348. void Controller::repaint() {
  1349. if (_captionFullView) {
  1350. _captionFullView->repaint();
  1351. }
  1352. _delegate->storiesRepaint();
  1353. }
  1354. SiblingView Controller::sibling(SiblingType type) const {
  1355. const auto &pointer = (type == SiblingType::Left)
  1356. ? _siblingLeft
  1357. : _siblingRight;
  1358. if (const auto value = pointer.get()) {
  1359. const auto over = _delegate->storiesSiblingOver(type);
  1360. const auto layout = (type == SiblingType::Left)
  1361. ? _layout.current()->siblingLeft
  1362. : _layout.current()->siblingRight;
  1363. return value->view(layout, over);
  1364. }
  1365. return {};
  1366. }
  1367. const Data::StoryViews &Controller::views(int limit, bool initial) {
  1368. invalidate_weak_ptrs(&_viewsLoadGuard);
  1369. if (initial) {
  1370. refreshViewsFromData();
  1371. }
  1372. if (_viewsSlice.total > _viewsSlice.list.size()
  1373. && _viewsSlice.list.size() < limit) {
  1374. const auto done = viewsGotMoreCallback();
  1375. const auto peer = shownPeer();
  1376. auto &stories = peer->owner().stories();
  1377. if (peer->isChannel()) {
  1378. stories.loadReactionsSlice(
  1379. peer,
  1380. _shown.story,
  1381. _viewsSlice.nextOffset,
  1382. done);
  1383. } else {
  1384. stories.loadViewsSlice(
  1385. peer,
  1386. _shown.story,
  1387. _viewsSlice.nextOffset,
  1388. done);
  1389. }
  1390. }
  1391. return _viewsSlice;
  1392. }
  1393. rpl::producer<> Controller::moreViewsLoaded() const {
  1394. return _moreViewsLoaded.events();
  1395. }
  1396. Fn<void(Data::StoryViews)> Controller::viewsGotMoreCallback() {
  1397. return crl::guard(&_viewsLoadGuard, [=](Data::StoryViews result) {
  1398. if (_viewsSlice.list.empty()) {
  1399. const auto peer = shownPeer();
  1400. auto &stories = peer->owner().stories();
  1401. if (const auto maybeStory = stories.lookup(_shown)) {
  1402. if (peer->isChannel()) {
  1403. _viewsSlice = (*maybeStory)->channelReactionsList();
  1404. } else {
  1405. _viewsSlice = (*maybeStory)->viewsList();
  1406. }
  1407. } else {
  1408. _viewsSlice = {};
  1409. }
  1410. } else {
  1411. _viewsSlice.list.insert(
  1412. end(_viewsSlice.list),
  1413. begin(result.list),
  1414. end(result.list));
  1415. _viewsSlice.total = result.nextOffset.isEmpty()
  1416. ? int(_viewsSlice.list.size())
  1417. : std::max(result.total, int(_viewsSlice.list.size()));
  1418. _viewsSlice.nextOffset = result.nextOffset;
  1419. }
  1420. _moreViewsLoaded.fire({});
  1421. });
  1422. }
  1423. bool Controller::shown() const {
  1424. return _source || _list;
  1425. }
  1426. PeerData *Controller::shownPeer() const {
  1427. return _source
  1428. ? _source->peer.get()
  1429. : _list
  1430. ? _list->peer.get()
  1431. : nullptr;
  1432. }
  1433. int Controller::shownCount() const {
  1434. return _source ? int(_source->ids.size()) : _list ? _list->total : 0;
  1435. }
  1436. StoryId Controller::shownId(int index) const {
  1437. Expects(index >= 0 && index < shownCount());
  1438. return _source
  1439. ? (_source->ids.begin() + index)->id
  1440. : (index < int(_list->sorted.size()))
  1441. ? _list->sorted[index]
  1442. : StoryId();
  1443. }
  1444. std::unique_ptr<RepostView> Controller::validateRepostView(
  1445. not_null<Data::Story*> story) {
  1446. return (story->repost() || !story->channelPosts().empty())
  1447. ? std::make_unique<RepostView>(this, story)
  1448. : nullptr;
  1449. }
  1450. void Controller::loadMoreToList() {
  1451. Expects(shown());
  1452. using namespace Data;
  1453. const auto peer = shownPeer();
  1454. const auto peerId = _shown.peer;
  1455. auto &stories = peer->owner().stories();
  1456. v::match(_context.data, [&](StoriesContextSaved) {
  1457. stories.savedLoadMore(peerId);
  1458. }, [&](StoriesContextArchive) {
  1459. stories.archiveLoadMore(peerId);
  1460. }, [](const auto &) {
  1461. });
  1462. }
  1463. void Controller::rebuildCachedSourcesList(
  1464. const std::vector<Data::StoriesSourceInfo> &lists,
  1465. int index) {
  1466. Expects(index >= 0 && index < lists.size());
  1467. const auto currentPeerId = lists[index].id;
  1468. // Remove removed.
  1469. _cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
  1470. CachedSource source) {
  1471. return !ranges::contains(
  1472. lists,
  1473. source.peerId,
  1474. &Data::StoriesSourceInfo::id);
  1475. }), end(_cachedSourcesList));
  1476. // Find current, full rebuild if can't find.
  1477. const auto i = ranges::find(
  1478. _cachedSourcesList,
  1479. currentPeerId,
  1480. &CachedSource::peerId);
  1481. if (i == end(_cachedSourcesList)) {
  1482. _cachedSourcesList.clear();
  1483. } else {
  1484. _cachedSourceIndex = int(i - begin(_cachedSourcesList));
  1485. }
  1486. if (_cachedSourcesList.empty()) {
  1487. // Full rebuild.
  1488. const auto predicate = [&](const Data::StoriesSourceInfo &info) {
  1489. return !_showingUnreadSources
  1490. || (info.unreadCount > 0)
  1491. || (info.id == currentPeerId);
  1492. };
  1493. const auto mapper = [](const Data::StoriesSourceInfo &info) {
  1494. return CachedSource{ info.id };
  1495. };
  1496. _cachedSourcesList = lists
  1497. | ranges::views::filter(predicate)
  1498. | ranges::views::transform(mapper)
  1499. | ranges::to_vector;
  1500. _cachedSourceIndex = ranges::find(
  1501. _cachedSourcesList,
  1502. currentPeerId,
  1503. &CachedSource::peerId
  1504. ) - begin(_cachedSourcesList);
  1505. } else if (ranges::equal(
  1506. lists,
  1507. _cachedSourcesList,
  1508. ranges::equal_to(),
  1509. &Data::StoriesSourceInfo::id,
  1510. &CachedSource::peerId)) {
  1511. // No rebuild needed.
  1512. } else {
  1513. // All that go before the current push to front.
  1514. for (auto before = index; before > 0;) {
  1515. const auto &info = lists[--before];
  1516. if (_showingUnreadSources && !info.unreadCount) {
  1517. continue;
  1518. } else if (!ranges::contains(
  1519. _cachedSourcesList,
  1520. info.id,
  1521. &CachedSource::peerId)) {
  1522. _cachedSourcesList.insert(
  1523. begin(_cachedSourcesList),
  1524. { info.id });
  1525. ++_cachedSourceIndex;
  1526. }
  1527. }
  1528. // All that go after the current push to back.
  1529. for (auto after = index + 1, count = int(lists.size())
  1530. ; after != count
  1531. ; ++after) {
  1532. const auto &info = lists[after];
  1533. if (_showingUnreadSources && !info.unreadCount) {
  1534. continue;
  1535. } else if (!ranges::contains(
  1536. _cachedSourcesList,
  1537. info.id,
  1538. &CachedSource::peerId)) {
  1539. _cachedSourcesList.push_back({ info.id });
  1540. }
  1541. }
  1542. }
  1543. Ensures(_cachedSourcesList.size() <= lists.size());
  1544. Ensures(_cachedSourceIndex >= 0
  1545. && _cachedSourceIndex < _cachedSourcesList.size());
  1546. }
  1547. void Controller::refreshViewsFromData() {
  1548. Expects(shown());
  1549. const auto peer = shownPeer();
  1550. auto &stories = peer->owner().stories();
  1551. const auto maybeStory = stories.lookup(_shown);
  1552. const auto check = peer->isSelf()
  1553. || CanViewReactionsFor(peer);
  1554. if (!maybeStory || !check) {
  1555. _viewsSlice = {};
  1556. } else if (peer->isChannel()) {
  1557. _viewsSlice = (*maybeStory)->channelReactionsList();
  1558. } else {
  1559. _viewsSlice = (*maybeStory)->viewsList();
  1560. }
  1561. }
  1562. void Controller::unfocusReply() {
  1563. _wrap->setFocus();
  1564. }
  1565. void Controller::shareRequested() {
  1566. const auto show = _delegate->storiesShow();
  1567. if (auto box = PrepareShareBox(show, _shown, true)) {
  1568. show->show(std::move(box));
  1569. }
  1570. }
  1571. void Controller::deleteRequested() {
  1572. const auto story = this->story();
  1573. if (!story) {
  1574. return;
  1575. }
  1576. const auto id = story->fullId();
  1577. const auto weak = base::make_weak(this);
  1578. const auto owner = &story->owner();
  1579. const auto confirmed = [=](Fn<void()> close) {
  1580. if (const auto strong = weak.get()) {
  1581. if (const auto story = strong->story()) {
  1582. if (story->fullId() == id) {
  1583. moveFromShown();
  1584. }
  1585. }
  1586. }
  1587. owner->stories().deleteList({ id });
  1588. close();
  1589. };
  1590. uiShow()->show(Ui::MakeConfirmBox({
  1591. .text = tr::lng_stories_delete_one_sure(),
  1592. .confirmed = confirmed,
  1593. .confirmText = tr::lng_selected_delete(),
  1594. .labelStyle = &st::storiesBoxLabel,
  1595. }));
  1596. }
  1597. void Controller::reportRequested() {
  1598. ReportRequested(uiShow(), _shown, &st::storiesReportBox);
  1599. }
  1600. void Controller::toggleInProfileRequested(bool inProfile) {
  1601. const auto story = this->story();
  1602. if (!story || !story->peer()->isSelf()) {
  1603. return;
  1604. }
  1605. if (!inProfile && v::is<Data::StoriesContextSaved>(_context.data)) {
  1606. moveFromShown();
  1607. }
  1608. story->owner().stories().toggleInProfileList(
  1609. { story->fullId() },
  1610. inProfile);
  1611. const auto channel = story->peer()->isChannel();
  1612. uiShow()->showToast(PrepareToggleInProfileToast(channel, 1, inProfile));
  1613. }
  1614. void Controller::moveFromShown() {
  1615. if (!subjumpFor(1)) {
  1616. [[maybe_unused]] const auto jumped = subjumpFor(-1);
  1617. }
  1618. }
  1619. bool Controller::ignoreWindowMove(QPoint position) const {
  1620. return _replyArea->ignoreWindowMove(position)
  1621. || _header->ignoreWindowMove(position);
  1622. }
  1623. void Controller::tryProcessKeyInput(not_null<QKeyEvent*> e) {
  1624. _replyArea->tryProcessKeyInput(e);
  1625. }
  1626. bool Controller::allowStealthMode() const {
  1627. const auto story = this->story();
  1628. return story
  1629. && !story->peer()->isSelf()
  1630. && story->peer()->session().premiumPossible();
  1631. }
  1632. void Controller::setupStealthMode() {
  1633. SetupStealthMode(uiShow());
  1634. }
  1635. auto Controller::attachReactionsToMenu(
  1636. not_null<Ui::PopupMenu*> menu,
  1637. QPoint desiredPosition)
  1638. -> AttachStripResult {
  1639. return _reactions->attachToMenu(menu, desiredPosition);
  1640. }
  1641. rpl::lifetime &Controller::lifetime() {
  1642. return _lifetime;
  1643. }
  1644. void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
  1645. const auto block = !Player::IsPausedOrPausing(state.state)
  1646. && !Player::IsStoppedOrStopping(state.state);
  1647. base::UpdatePowerSaveBlocker(
  1648. _powerSaveBlocker,
  1649. block,
  1650. base::PowerSaveBlockType::PreventDisplaySleep,
  1651. [] { return u"Stories playback is active"_q; },
  1652. [=] { return _wrap->window()->windowHandle(); });
  1653. }
  1654. Ui::Toast::Config PrepareToggleInProfileToast(
  1655. bool channel,
  1656. int count,
  1657. bool inProfile) {
  1658. return {
  1659. .text = (inProfile
  1660. ? (count == 1
  1661. ? (channel
  1662. ? tr::lng_stories_channel_save_done
  1663. : tr::lng_stories_save_done)(
  1664. tr::now,
  1665. Ui::Text::Bold)
  1666. : (channel
  1667. ? tr::lng_stories_channel_save_done_many
  1668. : tr::lng_stories_save_done_many)(
  1669. tr::now,
  1670. lt_count,
  1671. count,
  1672. Ui::Text::Bold)).append(
  1673. '\n').append((channel
  1674. ? tr::lng_stories_channel_save_done_about
  1675. : tr::lng_stories_save_done_about)(tr::now))
  1676. : (count == 1
  1677. ? (channel
  1678. ? tr::lng_stories_channel_archive_done
  1679. : tr::lng_stories_archive_done)(
  1680. tr::now,
  1681. Ui::Text::WithEntities)
  1682. : (channel
  1683. ? tr::lng_stories_channel_archive_done_many
  1684. : tr::lng_stories_archive_done_many)(
  1685. tr::now,
  1686. lt_count,
  1687. count,
  1688. Ui::Text::WithEntities))),
  1689. .st = &st::storiesActionToast,
  1690. .duration = (inProfile
  1691. ? Data::Stories::kInProfileToastDuration
  1692. : Ui::Toast::kDefaultDuration),
  1693. };
  1694. }
  1695. Ui::Toast::Config PrepareTogglePinToast(
  1696. bool channel,
  1697. int count,
  1698. bool pin) {
  1699. return {
  1700. .title = (pin
  1701. ? (count == 1
  1702. ? tr::lng_mediaview_pin_story_done(tr::now)
  1703. : tr::lng_mediaview_pin_stories_done(
  1704. tr::now,
  1705. lt_count,
  1706. count))
  1707. : QString()),
  1708. .text = { (pin
  1709. ? (count == 1
  1710. ? tr::lng_mediaview_pin_story_about(tr::now)
  1711. : tr::lng_mediaview_pin_stories_about(
  1712. tr::now,
  1713. lt_count,
  1714. count))
  1715. : (count == 1
  1716. ? tr::lng_mediaview_unpin_story_done(tr::now)
  1717. : tr::lng_mediaview_unpin_stories_done(
  1718. tr::now,
  1719. lt_count,
  1720. count))) },
  1721. .st = &st::storiesActionToast,
  1722. .duration = (pin
  1723. ? Data::Stories::kInProfileToastDuration
  1724. : Ui::Toast::kDefaultDuration),
  1725. };
  1726. }
  1727. void ReportRequested(
  1728. std::shared_ptr<Main::SessionShow> show,
  1729. FullStoryId id,
  1730. const style::ReportBox *stOverride) {
  1731. if (const auto maybeStory = show->session().data().stories().lookup(id)) {
  1732. const auto story = *maybeStory;
  1733. const auto st = stOverride ? stOverride : &st::defaultReportBox;
  1734. // show->hideLayer();
  1735. ShowReportMessageBox(show, story->peer(), {}, { story->id() }, st);
  1736. }
  1737. }
  1738. object_ptr<Ui::BoxContent> PrepareShortInfoBox(not_null<PeerData*> peer) {
  1739. const auto open = [=] {
  1740. if (const auto window = Core::App().windowFor(peer)) {
  1741. window->invokeForSessionController(
  1742. &peer->session().account(),
  1743. peer,
  1744. [&](not_null<Window::SessionController*> controller) {
  1745. Core::App().hideMediaView();
  1746. controller->showPeerHistory(peer);
  1747. });
  1748. }
  1749. };
  1750. return ::PrepareShortInfoBox(
  1751. peer,
  1752. open,
  1753. [] { return false; },
  1754. nullptr,
  1755. &st::storiesShortInfoBox);
  1756. }
  1757. ClickHandlerPtr MakeChannelPostHandler(
  1758. not_null<Main::Session*> session,
  1759. FullMsgId item) {
  1760. return std::make_shared<LambdaClickHandler>(crl::guard(session, [=] {
  1761. const auto peer = session->data().peer(item.peer);
  1762. if (const auto controller = session->tryResolveWindow(peer)) {
  1763. Core::App().hideMediaView();
  1764. controller->showPeerHistory(
  1765. peer,
  1766. Window::SectionShow::Way::ClearStack,
  1767. item.msg);
  1768. }
  1769. }));
  1770. }
  1771. ClickHandlerPtr MakeUrlAreaHandler(
  1772. base::weak_ptr<Controller> weak,
  1773. const QString &url) {
  1774. class Handler final : public HiddenUrlClickHandler {
  1775. public:
  1776. Handler(const QString &url, base::weak_ptr<Controller> weak)
  1777. : HiddenUrlClickHandler(url), _weak(weak) {
  1778. }
  1779. void onClick(ClickContext context) const override {
  1780. const auto raw = url();
  1781. const auto strong = _weak.get();
  1782. const auto prefix = u"tg://nft?slug="_q;
  1783. if (raw.startsWith(prefix) && strong) {
  1784. const auto slug = raw.mid(
  1785. prefix.size()
  1786. ).split('&').front().split('#').front();
  1787. Core::ResolveAndShowUniqueGift(
  1788. strong->uiShow(),
  1789. slug,
  1790. ::Settings::DarkCreditsEntryBoxStyle());
  1791. } else {
  1792. HiddenUrlClickHandler::onClick(context);
  1793. }
  1794. }
  1795. private:
  1796. base::weak_ptr<Controller> _weak;
  1797. };
  1798. return std::make_shared<Handler>(url, weak);
  1799. }
  1800. } // namespace Media::Stories