data_story.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  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 "data/data_story.h"
  8. #include "base/unixtime.h"
  9. #include "api/api_text_entities.h"
  10. #include "data/data_document.h"
  11. #include "data/data_changes.h"
  12. #include "data/data_channel.h"
  13. #include "data/data_file_origin.h"
  14. #include "data/data_media_preload.h"
  15. #include "data/data_photo.h"
  16. #include "data/data_photo_media.h"
  17. #include "data/data_user.h"
  18. #include "data/data_session.h"
  19. #include "data/data_stories.h"
  20. #include "data/data_thread.h"
  21. #include "history/history_item.h"
  22. #include "lang/lang_keys.h"
  23. #include "main/main_session.h"
  24. #include "media/streaming/media_streaming_reader.h"
  25. #include "storage/download_manager_mtproto.h"
  26. #include "storage/file_download.h" // kMaxFileInMemory
  27. #include "ui/text/text_utilities.h"
  28. #include "ui/color_int_conversion.h"
  29. namespace Data {
  30. namespace {
  31. using UpdateFlag = StoryUpdate::Flag;
  32. [[nodiscard]] StoryArea ParseArea(const MTPMediaAreaCoordinates &area) {
  33. const auto &data = area.data();
  34. const auto center = QPointF(data.vx().v, data.vy().v);
  35. const auto size = QSizeF(data.vw().v, data.vh().v);
  36. const auto corner = center - QPointF(size.width(), size.height()) / 2.;
  37. return {
  38. .geometry = { corner / 100., size / 100. },
  39. .rotation = data.vrotation().v,
  40. .radius = data.vradius().value_or_empty(),
  41. };
  42. }
  43. [[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {
  44. const auto link = [&](const EntityInText &entity) {
  45. return (entity.type() == EntityType::CustomUrl)
  46. || (entity.type() == EntityType::Url)
  47. || (entity.type() == EntityType::Mention)
  48. || (entity.type() == EntityType::Hashtag);
  49. };
  50. text.entities.erase(
  51. ranges::remove_if(text.entities, link),
  52. text.entities.end());
  53. return text;
  54. }
  55. [[nodiscard]] auto ParseLocation(const MTPMediaArea &area)
  56. -> std::optional<StoryLocation> {
  57. auto result = std::optional<StoryLocation>();
  58. area.match([&](const MTPDmediaAreaVenue &data) {
  59. data.vgeo().match([&](const MTPDgeoPoint &geo) {
  60. result.emplace(StoryLocation{
  61. .area = ParseArea(data.vcoordinates()),
  62. .point = Data::LocationPoint(geo),
  63. .title = qs(data.vtitle()),
  64. .address = qs(data.vaddress()),
  65. .provider = qs(data.vprovider()),
  66. .venueId = qs(data.vvenue_id()),
  67. .venueType = qs(data.vvenue_type()),
  68. });
  69. }, [](const MTPDgeoPointEmpty &) {
  70. });
  71. }, [&](const MTPDmediaAreaGeoPoint &data) {
  72. data.vgeo().match([&](const MTPDgeoPoint &geo) {
  73. result.emplace(StoryLocation{
  74. .area = ParseArea(data.vcoordinates()),
  75. .point = Data::LocationPoint(geo),
  76. });
  77. }, [](const MTPDgeoPointEmpty &) {
  78. });
  79. }, [&](const MTPDmediaAreaSuggestedReaction &data) {
  80. }, [&](const MTPDmediaAreaChannelPost &data) {
  81. }, [&](const MTPDmediaAreaUrl &data) {
  82. }, [&](const MTPDmediaAreaWeather &data) {
  83. }, [&](const MTPDmediaAreaStarGift &data) {
  84. }, [&](const MTPDinputMediaAreaChannelPost &data) {
  85. LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
  86. }, [&](const MTPDinputMediaAreaVenue &data) {
  87. LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
  88. });
  89. return result;
  90. }
  91. [[nodiscard]] auto ParseSuggestedReaction(const MTPMediaArea &area)
  92. -> std::optional<SuggestedReaction> {
  93. auto result = std::optional<SuggestedReaction>();
  94. area.match([&](const MTPDmediaAreaVenue &data) {
  95. }, [&](const MTPDmediaAreaGeoPoint &data) {
  96. }, [&](const MTPDmediaAreaSuggestedReaction &data) {
  97. result.emplace(SuggestedReaction{
  98. .area = ParseArea(data.vcoordinates()),
  99. .reaction = Data::ReactionFromMTP(data.vreaction()),
  100. .flipped = data.is_flipped(),
  101. .dark = data.is_dark(),
  102. });
  103. }, [&](const MTPDmediaAreaChannelPost &data) {
  104. }, [&](const MTPDmediaAreaUrl &data) {
  105. }, [&](const MTPDmediaAreaWeather &data) {
  106. }, [&](const MTPDmediaAreaStarGift &data) {
  107. }, [&](const MTPDinputMediaAreaChannelPost &data) {
  108. LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
  109. }, [&](const MTPDinputMediaAreaVenue &data) {
  110. LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
  111. });
  112. return result;
  113. }
  114. [[nodiscard]] auto ParseChannelPost(const MTPMediaArea &area)
  115. -> std::optional<ChannelPost> {
  116. auto result = std::optional<ChannelPost>();
  117. area.match([&](const MTPDmediaAreaVenue &data) {
  118. }, [&](const MTPDmediaAreaGeoPoint &data) {
  119. }, [&](const MTPDmediaAreaSuggestedReaction &data) {
  120. }, [&](const MTPDmediaAreaChannelPost &data) {
  121. result.emplace(ChannelPost{
  122. .area = ParseArea(data.vcoordinates()),
  123. .itemId = FullMsgId(
  124. peerFromChannel(data.vchannel_id()),
  125. data.vmsg_id().v),
  126. });
  127. }, [&](const MTPDmediaAreaUrl &data) {
  128. }, [&](const MTPDmediaAreaWeather &data) {
  129. }, [&](const MTPDmediaAreaStarGift &data) {
  130. }, [&](const MTPDinputMediaAreaChannelPost &data) {
  131. LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
  132. }, [&](const MTPDinputMediaAreaVenue &data) {
  133. LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
  134. });
  135. return result;
  136. }
  137. [[nodiscard]] auto ParseUrlArea(const MTPMediaArea &area)
  138. -> std::optional<UrlArea> {
  139. auto result = std::optional<UrlArea>();
  140. area.match([&](const MTPDmediaAreaVenue &data) {
  141. }, [&](const MTPDmediaAreaGeoPoint &data) {
  142. }, [&](const MTPDmediaAreaSuggestedReaction &data) {
  143. }, [&](const MTPDmediaAreaChannelPost &data) {
  144. }, [&](const MTPDmediaAreaUrl &data) {
  145. result.emplace(UrlArea{
  146. .area = ParseArea(data.vcoordinates()),
  147. .url = qs(data.vurl()),
  148. });
  149. }, [&](const MTPDmediaAreaWeather &data) {
  150. }, [&](const MTPDmediaAreaStarGift &data) {
  151. result.emplace(UrlArea{
  152. .area = ParseArea(data.vcoordinates()),
  153. .url = u"tg://nft?slug="_q + qs(data.vslug()),
  154. });
  155. }, [&](const MTPDinputMediaAreaChannelPost &data) {
  156. LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
  157. }, [&](const MTPDinputMediaAreaVenue &data) {
  158. LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
  159. });
  160. return result;
  161. }
  162. [[nodiscard]] auto ParseWeatherArea(const MTPMediaArea &area)
  163. -> std::optional<WeatherArea> {
  164. auto result = std::optional<WeatherArea>();
  165. area.match([&](const MTPDmediaAreaVenue &data) {
  166. }, [&](const MTPDmediaAreaGeoPoint &data) {
  167. }, [&](const MTPDmediaAreaSuggestedReaction &data) {
  168. }, [&](const MTPDmediaAreaChannelPost &data) {
  169. }, [&](const MTPDmediaAreaUrl &data) {
  170. }, [&](const MTPDmediaAreaWeather &data) {
  171. result.emplace(WeatherArea{
  172. .area = ParseArea(data.vcoordinates()),
  173. .emoji = qs(data.vemoji()),
  174. .color = Ui::Color32FromSerialized(data.vcolor().v),
  175. .millicelsius = int(1000. * std::clamp(
  176. data.vtemperature_c().v,
  177. -274.,
  178. 1'000'000.)),
  179. });
  180. }, [&](const MTPDmediaAreaStarGift &data) {
  181. }, [&](const MTPDinputMediaAreaChannelPost &data) {
  182. LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
  183. }, [&](const MTPDinputMediaAreaVenue &data) {
  184. LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
  185. });
  186. return result;
  187. }
  188. [[nodiscard]] PeerData *RepostSourcePeer(
  189. not_null<Session*> owner,
  190. const MTPDstoryItem &data) {
  191. if (const auto forwarded = data.vfwd_from()) {
  192. if (const auto from = forwarded->data().vfrom()) {
  193. return owner->peer(peerFromMTP(*from));
  194. }
  195. }
  196. return nullptr;
  197. }
  198. [[nodiscard]] QString RepostSourceName(const MTPDstoryItem &data) {
  199. if (const auto forwarded = data.vfwd_from()) {
  200. return qs(forwarded->data().vfrom_name().value_or_empty());
  201. }
  202. return {};
  203. }
  204. [[nodiscard]] StoryId RepostSourceId(const MTPDstoryItem &data) {
  205. if (const auto forwarded = data.vfwd_from()) {
  206. return forwarded->data().vstory_id().value_or_empty();
  207. }
  208. return {};
  209. }
  210. [[nodiscard]] bool RepostModified(const MTPDstoryItem &data) {
  211. if (const auto forwarded = data.vfwd_from()) {
  212. return forwarded->data().is_modified();
  213. }
  214. return false;
  215. }
  216. [[nodiscard]] PeerData *FromPeer(
  217. not_null<Session*> owner,
  218. const MTPDstoryItem &data) {
  219. if (const auto from = data.vfrom_id()) {
  220. return owner->peer(peerFromMTP(*from));
  221. }
  222. return nullptr;
  223. }
  224. } // namespace
  225. Story::Story(
  226. StoryId id,
  227. not_null<PeerData*> peer,
  228. StoryMedia media,
  229. const MTPDstoryItem &data,
  230. TimeId now)
  231. : _id(id)
  232. , _peer(peer)
  233. , _repostSourcePeer(RepostSourcePeer(&peer->owner(), data))
  234. , _repostSourceName(RepostSourceName(data))
  235. , _repostSourceId(RepostSourceId(data))
  236. , _fromPeer(FromPeer(&peer->owner(), data))
  237. , _date(data.vdate().v)
  238. , _expires(data.vexpire_date().v)
  239. , _repostModified(RepostModified(data)) {
  240. applyFields(std::move(media), data, now, true);
  241. }
  242. Session &Story::owner() const {
  243. return _peer->owner();
  244. }
  245. Main::Session &Story::session() const {
  246. return _peer->session();
  247. }
  248. not_null<PeerData*> Story::peer() const {
  249. return _peer;
  250. }
  251. StoryId Story::id() const {
  252. return _id;
  253. }
  254. bool Story::mine() const {
  255. return _peer->isSelf();
  256. }
  257. StoryIdDates Story::idDates() const {
  258. return { _id, _date, _expires };
  259. }
  260. FullStoryId Story::fullId() const {
  261. return { _peer->id, _id };
  262. }
  263. TimeId Story::date() const {
  264. return _date;
  265. }
  266. TimeId Story::expires() const {
  267. return _expires;
  268. }
  269. bool Story::expired(TimeId now) const {
  270. return _expires <= (now ? now : base::unixtime::now());
  271. }
  272. bool Story::unsupported() const {
  273. return v::is_null(_media.data);
  274. }
  275. const StoryMedia &Story::media() const {
  276. return _media;
  277. }
  278. PhotoData *Story::photo() const {
  279. const auto result = std::get_if<not_null<PhotoData*>>(&_media.data);
  280. return result ? result->get() : nullptr;
  281. }
  282. DocumentData *Story::document() const {
  283. const auto result = std::get_if<not_null<DocumentData*>>(&_media.data);
  284. return result ? result->get() : nullptr;
  285. }
  286. bool Story::hasReplyPreview() const {
  287. return v::match(_media.data, [](not_null<PhotoData*> photo) {
  288. return !photo->isNull();
  289. }, [](not_null<DocumentData*> document) {
  290. return document->hasThumbnail();
  291. }, [](v::null_t) {
  292. return false;
  293. });
  294. }
  295. Image *Story::replyPreview() const {
  296. return v::match(_media.data, [&](not_null<PhotoData*> photo) {
  297. return photo->getReplyPreview(fullId(), _peer, false);
  298. }, [&](not_null<DocumentData*> document) {
  299. return document->getReplyPreview(fullId(), _peer, false);
  300. }, [](v::null_t) {
  301. return (Image*)nullptr;
  302. });
  303. }
  304. TextWithEntities Story::inReplyText() const {
  305. const auto type = tr::lng_in_dlg_story(tr::now);
  306. return _caption.text.isEmpty()
  307. ? Ui::Text::Colorized(type)
  308. : tr::lng_dialogs_text_media(
  309. tr::now,
  310. lt_media_part,
  311. tr::lng_dialogs_text_media_wrapped(
  312. tr::now,
  313. lt_media,
  314. Ui::Text::Colorized(type),
  315. Ui::Text::WithEntities),
  316. lt_caption,
  317. _caption,
  318. Ui::Text::WithEntities);
  319. }
  320. void Story::setPinnedToTop(bool pinned) {
  321. if (_pinnedToTop != pinned) {
  322. _pinnedToTop = pinned;
  323. if (const auto item = _peer->owner().stories().lookupItem(this)) {
  324. item->setIsPinned(pinned);
  325. }
  326. }
  327. }
  328. bool Story::pinnedToTop() const {
  329. return _pinnedToTop;
  330. }
  331. void Story::setInProfile(bool value) {
  332. _inProfile = value;
  333. }
  334. bool Story::inProfile() const {
  335. return _inProfile;
  336. }
  337. StoryPrivacy Story::privacy() const {
  338. return _privacyPublic
  339. ? StoryPrivacy::Public
  340. : _privacyCloseFriends
  341. ? StoryPrivacy::CloseFriends
  342. : _privacyContacts
  343. ? StoryPrivacy::Contacts
  344. : _privacySelectedContacts
  345. ? StoryPrivacy::SelectedContacts
  346. : StoryPrivacy::Other;
  347. }
  348. bool Story::forbidsForward() const {
  349. return _noForwards;
  350. }
  351. bool Story::edited() const {
  352. return _edited;
  353. }
  354. bool Story::out() const {
  355. return _out;
  356. }
  357. bool Story::canDownloadIfPremium() const {
  358. return !forbidsForward() || _peer->isSelf();
  359. }
  360. bool Story::canDownloadChecked() const {
  361. return _peer->isSelf()
  362. || (canDownloadIfPremium() && _peer->session().premium());
  363. }
  364. bool Story::canShare() const {
  365. return _privacyPublic
  366. && !forbidsForward()
  367. && (inProfile() || !expired());
  368. }
  369. bool Story::canDelete() const {
  370. if (const auto channel = _peer->asChannel()) {
  371. return channel->canDeleteStories()
  372. || (out() && channel->canPostStories());
  373. }
  374. return _peer->isSelf();
  375. }
  376. bool Story::canReport() const {
  377. return !_peer->isSelf();
  378. }
  379. bool Story::hasDirectLink() const {
  380. if (!_privacyPublic || (!_inProfile && expired())) {
  381. return false;
  382. }
  383. return !_peer->username().isEmpty();
  384. }
  385. Data::SendError Story::errorTextForForward(
  386. not_null<Thread*> to) const {
  387. const auto peer = to->peer();
  388. const auto holdsPhoto = v::is<not_null<PhotoData*>>(_media.data);
  389. const auto first = holdsPhoto
  390. ? ChatRestriction::SendPhotos
  391. : ChatRestriction::SendVideos;
  392. const auto second = holdsPhoto
  393. ? ChatRestriction::SendVideos
  394. : ChatRestriction::SendPhotos;
  395. if (const auto one = Data::RestrictionError(peer, first)) {
  396. return one;
  397. } else if (const auto two = Data::RestrictionError(peer, second)) {
  398. return two;
  399. } else if (!Data::CanSend(to, first, false)
  400. || !Data::CanSend(to, second, false)) {
  401. return tr::lng_forward_cant(tr::now);
  402. }
  403. return {};
  404. }
  405. void Story::setCaption(TextWithEntities &&caption) {
  406. _caption = std::move(caption);
  407. }
  408. const TextWithEntities &Story::caption() const {
  409. static const auto empty = TextWithEntities();
  410. return unsupported() ? empty : _caption;
  411. }
  412. Data::ReactionId Story::sentReactionId() const {
  413. return _sentReactionId;
  414. }
  415. void Story::setReactionId(Data::ReactionId id) {
  416. if (_sentReactionId != id) {
  417. const auto wasEmpty = _sentReactionId.empty();
  418. changeSuggestedReactionCount(_sentReactionId, -1);
  419. _sentReactionId = id;
  420. changeSuggestedReactionCount(id, 1);
  421. if (_views.known && _sentReactionId.empty() != wasEmpty) {
  422. const auto delta = wasEmpty ? 1 : -1;
  423. if (_views.reactions + delta >= 0) {
  424. _views.reactions += delta;
  425. }
  426. }
  427. session().changes().storyUpdated(this, UpdateFlag::Reaction);
  428. }
  429. }
  430. void Story::changeSuggestedReactionCount(Data::ReactionId id, int delta) {
  431. if (id.empty() || !_peer->isChannel()) {
  432. return;
  433. }
  434. for (auto &suggested : _suggestedReactions) {
  435. if (suggested.reaction == id && suggested.count + delta >= 0) {
  436. suggested.count += delta;
  437. }
  438. }
  439. }
  440. const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
  441. return _recentViewers;
  442. }
  443. const StoryViews &Story::viewsList() const {
  444. return _views;
  445. }
  446. const StoryViews &Story::channelReactionsList() const {
  447. return _channelReactions;
  448. }
  449. int Story::interactions() const {
  450. return _views.total;
  451. }
  452. int Story::views() const {
  453. return _views.views;
  454. }
  455. int Story::forwards() const {
  456. return _views.forwards;
  457. }
  458. int Story::reactions() const {
  459. return _views.reactions;
  460. }
  461. void Story::applyViewsSlice(
  462. const QString &offset,
  463. const StoryViews &slice) {
  464. const auto changed = (_views.reactions != slice.reactions)
  465. || (_views.views != slice.views)
  466. || (_views.forwards != slice.forwards)
  467. || (_views.total != slice.total);
  468. _views.reactions = slice.reactions;
  469. _views.forwards = slice.forwards;
  470. _views.views = slice.views;
  471. _views.total = slice.total;
  472. _views.known = true;
  473. if (offset.isEmpty()) {
  474. _views = slice;
  475. if (!_channelReactions.total) {
  476. _channelReactions.total = _views.reactions + _views.forwards;
  477. }
  478. } else if (_views.nextOffset == offset) {
  479. _views.list.insert(
  480. end(_views.list),
  481. begin(slice.list),
  482. end(slice.list));
  483. _views.nextOffset = slice.nextOffset;
  484. if (_views.nextOffset.isEmpty()) {
  485. _views.total = int(_views.list.size());
  486. _views.reactions = _views.total
  487. - ranges::count(
  488. _views.list,
  489. Data::ReactionId(),
  490. &StoryView::reaction);
  491. _views.forwards = _views.total
  492. - ranges::count(
  493. _views.list,
  494. 0,
  495. [](const StoryView &view) {
  496. return view.repostId
  497. ? view.repostId
  498. : view.forwardId.bare;
  499. });
  500. }
  501. }
  502. const auto known = int(_views.list.size());
  503. if (known >= _recentViewers.size()) {
  504. const auto take = std::min(known, kRecentViewersMax);
  505. auto viewers = _views.list
  506. | ranges::views::take(take)
  507. | ranges::views::transform(&StoryView::peer)
  508. | ranges::to_vector;
  509. if (_recentViewers != viewers) {
  510. _recentViewers = std::move(viewers);
  511. if (!changed) {
  512. // Count not changed, but list of recent viewers changed.
  513. _peer->session().changes().storyUpdated(
  514. this,
  515. UpdateFlag::ViewsChanged);
  516. }
  517. }
  518. }
  519. if (changed) {
  520. _peer->session().changes().storyUpdated(
  521. this,
  522. UpdateFlag::ViewsChanged);
  523. }
  524. }
  525. void Story::applyChannelReactionsSlice(
  526. const QString &offset,
  527. const StoryViews &slice) {
  528. const auto changed = (_channelReactions.reactions != slice.reactions)
  529. || (_channelReactions.total != slice.total);
  530. _channelReactions.reactions = slice.reactions;
  531. _channelReactions.total = slice.total;
  532. _channelReactions.known = true;
  533. if (offset.isEmpty()) {
  534. _channelReactions = slice;
  535. } else if (_channelReactions.nextOffset == offset) {
  536. _channelReactions.list.insert(
  537. end(_channelReactions.list),
  538. begin(slice.list),
  539. end(slice.list));
  540. _channelReactions.nextOffset = slice.nextOffset;
  541. if (_channelReactions.nextOffset.isEmpty()) {
  542. _channelReactions.total = int(_channelReactions.list.size());
  543. }
  544. }
  545. if (changed) {
  546. _peer->session().changes().storyUpdated(
  547. this,
  548. UpdateFlag::ViewsChanged);
  549. }
  550. }
  551. const std::vector<StoryLocation> &Story::locations() const {
  552. return _locations;
  553. }
  554. const std::vector<SuggestedReaction> &Story::suggestedReactions() const {
  555. return _suggestedReactions;
  556. }
  557. const std::vector<ChannelPost> &Story::channelPosts() const {
  558. return _channelPosts;
  559. }
  560. const std::vector<UrlArea> &Story::urlAreas() const {
  561. return _urlAreas;
  562. }
  563. const std::vector<WeatherArea> &Story::weatherAreas() const {
  564. return _weatherAreas;
  565. }
  566. void Story::applyChanges(
  567. StoryMedia media,
  568. const MTPDstoryItem &data,
  569. TimeId now) {
  570. applyFields(std::move(media), data, now, false);
  571. }
  572. Story::ViewsCounts Story::parseViewsCounts(
  573. const MTPDstoryViews &data,
  574. const Data::ReactionId &mine) {
  575. auto result = ViewsCounts{
  576. .views = data.vviews_count().v,
  577. .forwards = data.vforwards_count().value_or_empty(),
  578. .reactions = data.vreactions_count().value_or_empty(),
  579. };
  580. if (const auto list = data.vrecent_viewers()) {
  581. result.viewers.reserve(list->v.size());
  582. auto &owner = _peer->owner();
  583. auto &&cut = list->v
  584. | ranges::views::take(kRecentViewersMax);
  585. for (const auto &id : cut) {
  586. result.viewers.push_back(owner.peer(peerFromUser(id)));
  587. }
  588. }
  589. auto total = 0;
  590. if (const auto list = data.vreactions()
  591. ; list && _peer->isChannel()) {
  592. result.reactionsCounts.reserve(list->v.size());
  593. for (const auto &reaction : list->v) {
  594. const auto &data = reaction.data();
  595. const auto id = Data::ReactionFromMTP(data.vreaction());
  596. const auto count = data.vcount().v;
  597. result.reactionsCounts[id] = count;
  598. total += count;
  599. }
  600. }
  601. if (!mine.empty()) {
  602. if (auto &count = result.reactionsCounts[mine]; !count) {
  603. count = 1;
  604. ++total;
  605. }
  606. }
  607. if (result.reactions < total) {
  608. result.reactions = total;
  609. }
  610. return result;
  611. }
  612. void Story::applyFields(
  613. StoryMedia media,
  614. const MTPDstoryItem &data,
  615. TimeId now,
  616. bool initial) {
  617. _lastUpdateTime = now;
  618. const auto reaction = data.is_min()
  619. ? _sentReactionId
  620. : data.vsent_reaction()
  621. ? Data::ReactionFromMTP(*data.vsent_reaction())
  622. : Data::ReactionId();
  623. const auto inProfile = data.is_pinned();
  624. const auto edited = data.is_edited();
  625. const auto privacy = data.is_public()
  626. ? StoryPrivacy::Public
  627. : data.is_close_friends()
  628. ? StoryPrivacy::CloseFriends
  629. : data.is_contacts()
  630. ? StoryPrivacy::Contacts
  631. : data.is_selected_contacts()
  632. ? StoryPrivacy::SelectedContacts
  633. : StoryPrivacy::Other;
  634. const auto noForwards = data.is_noforwards();
  635. const auto out = data.is_min() ? _out : data.is_out();
  636. auto caption = TextWithEntities{
  637. data.vcaption().value_or_empty(),
  638. Api::EntitiesFromMTP(
  639. &owner().session(),
  640. data.ventities().value_or_empty()),
  641. };
  642. if (const auto user = _peer->asUser()) {
  643. if (!user->isVerified() && !user->isPremium()) {
  644. caption = StripLinks(std::move(caption));
  645. }
  646. }
  647. auto counts = ViewsCounts();
  648. auto viewsKnown = _views.known;
  649. if (const auto info = data.vviews()) {
  650. counts = parseViewsCounts(info->data(), reaction);
  651. viewsKnown = true;
  652. } else {
  653. counts.views = _views.total;
  654. counts.forwards = _views.forwards;
  655. counts.reactions = _views.reactions;
  656. counts.viewers = _recentViewers;
  657. for (const auto &suggested : _suggestedReactions) {
  658. if (const auto count = suggested.count) {
  659. counts.reactionsCounts[suggested.reaction] = count;
  660. }
  661. }
  662. }
  663. auto locations = std::vector<StoryLocation>();
  664. auto suggestedReactions = std::vector<SuggestedReaction>();
  665. auto channelPosts = std::vector<ChannelPost>();
  666. auto urlAreas = std::vector<UrlArea>();
  667. auto weatherAreas = std::vector<WeatherArea>();
  668. if (const auto areas = data.vmedia_areas()) {
  669. for (const auto &area : areas->v) {
  670. if (const auto location = ParseLocation(area)) {
  671. locations.push_back(*location);
  672. } else if (auto reaction = ParseSuggestedReaction(area)) {
  673. const auto i = counts.reactionsCounts.find(
  674. reaction->reaction);
  675. if (i != end(counts.reactionsCounts)) {
  676. reaction->count = i->second;
  677. }
  678. suggestedReactions.push_back(*reaction);
  679. } else if (auto post = ParseChannelPost(area)) {
  680. channelPosts.push_back(*post);
  681. } else if (auto url = ParseUrlArea(area)) {
  682. urlAreas.push_back(*url);
  683. } else if (auto weather = ParseWeatherArea(area)) {
  684. weatherAreas.push_back(*weather);
  685. }
  686. }
  687. }
  688. const auto inProfileChanged = (_inProfile != inProfile);
  689. const auto editedChanged = (_edited != edited);
  690. const auto mediaChanged = (_media != media);
  691. const auto captionChanged = (_caption != caption);
  692. const auto locationsChanged = (_locations != locations);
  693. const auto suggestedReactionsChanged
  694. = (_suggestedReactions != suggestedReactions);
  695. const auto channelPostsChanged = (_channelPosts != channelPosts);
  696. const auto urlAreasChanged = (_urlAreas != urlAreas);
  697. const auto weatherAreasChanged = (_weatherAreas != weatherAreas);
  698. const auto reactionChanged = (_sentReactionId != reaction);
  699. _out = out;
  700. _privacyPublic = (privacy == StoryPrivacy::Public);
  701. _privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends);
  702. _privacyContacts = (privacy == StoryPrivacy::Contacts);
  703. _privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts);
  704. _edited = edited;
  705. _inProfile = inProfile;
  706. _noForwards = noForwards;
  707. if (mediaChanged) {
  708. _media = std::move(media);
  709. }
  710. if (captionChanged) {
  711. _caption = std::move(caption);
  712. }
  713. if (locationsChanged) {
  714. _locations = std::move(locations);
  715. }
  716. if (suggestedReactionsChanged) {
  717. _suggestedReactions = std::move(suggestedReactions);
  718. }
  719. if (channelPostsChanged) {
  720. _channelPosts = std::move(channelPosts);
  721. }
  722. if (urlAreasChanged) {
  723. _urlAreas = std::move(urlAreas);
  724. }
  725. if (weatherAreasChanged) {
  726. _weatherAreas = std::move(weatherAreas);
  727. }
  728. if (reactionChanged) {
  729. _sentReactionId = reaction;
  730. }
  731. updateViewsCounts(std::move(counts), viewsKnown, initial);
  732. const auto changed = editedChanged
  733. || captionChanged
  734. || mediaChanged
  735. || locationsChanged
  736. || channelPostsChanged
  737. || urlAreasChanged
  738. || weatherAreasChanged;
  739. const auto reactionsChanged = reactionChanged
  740. || suggestedReactionsChanged;
  741. if (!initial && (changed || reactionsChanged)) {
  742. _peer->session().changes().storyUpdated(this, UpdateFlag()
  743. | (changed ? UpdateFlag::Edited : UpdateFlag())
  744. | (reactionsChanged ? UpdateFlag::Reaction : UpdateFlag()));
  745. }
  746. if (!initial && (captionChanged || mediaChanged)) {
  747. if (const auto item = _peer->owner().stories().lookupItem(this)) {
  748. item->applyChanges(this);
  749. }
  750. _peer->owner().refreshStoryItemViews(fullId());
  751. }
  752. if (inProfileChanged) {
  753. _peer->owner().stories().savedStateChanged(this);
  754. }
  755. }
  756. void Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) {
  757. const auto total = _views.total
  758. ? _views.total
  759. : (counts.views + counts.forwards);
  760. const auto viewsChanged = (_views.total != total)
  761. || (_views.forwards != counts.forwards)
  762. || (_views.reactions != counts.reactions)
  763. || (_recentViewers != counts.viewers);
  764. if (_views.reactions != counts.reactions
  765. || _views.forwards != counts.forwards
  766. || _views.total != total
  767. || _views.known != known) {
  768. _views = StoryViews{
  769. .reactions = counts.reactions,
  770. .forwards = counts.forwards,
  771. .views = counts.views,
  772. .total = total,
  773. .known = known,
  774. };
  775. if (!_channelReactions.total) {
  776. _channelReactions.total = _views.reactions + _views.forwards;
  777. }
  778. }
  779. if (viewsChanged) {
  780. _recentViewers = std::move(counts.viewers);
  781. _peer->session().changes().storyUpdated(
  782. this,
  783. UpdateFlag::ViewsChanged);
  784. }
  785. }
  786. void Story::applyViewsCounts(const MTPDstoryViews &data) {
  787. auto counts = parseViewsCounts(data, _sentReactionId);
  788. auto suggestedCountsChanged = false;
  789. for (auto &suggested : _suggestedReactions) {
  790. const auto i = counts.reactionsCounts.find(suggested.reaction);
  791. const auto v = (i != end(counts.reactionsCounts)) ? i->second : 0;
  792. if (suggested.count != v) {
  793. suggested.count = v;
  794. suggestedCountsChanged = true;
  795. }
  796. }
  797. updateViewsCounts(std::move(counts), true, false);
  798. if (suggestedCountsChanged) {
  799. _peer->session().changes().storyUpdated(this, UpdateFlag::Reaction);
  800. }
  801. }
  802. TimeId Story::lastUpdateTime() const {
  803. return _lastUpdateTime;
  804. }
  805. bool Story::repost() const {
  806. return _repostSourcePeer || !_repostSourceName.isEmpty();
  807. }
  808. bool Story::repostModified() const {
  809. return _repostModified;
  810. }
  811. PeerData *Story::repostSourcePeer() const {
  812. return _repostSourcePeer;
  813. }
  814. QString Story::repostSourceName() const {
  815. return _repostSourceName;
  816. }
  817. StoryId Story::repostSourceId() const {
  818. return _repostSourceId;
  819. }
  820. PeerData *Story::fromPeer() const {
  821. return _fromPeer;
  822. }
  823. StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)
  824. : _story(story) {
  825. if (const auto photo = _story->photo()) {
  826. if (PhotoPreload::Should(photo, story->peer())) {
  827. _task = std::make_unique<PhotoPreload>(
  828. photo,
  829. story->fullId(),
  830. std::move(done));
  831. } else {
  832. done();
  833. }
  834. } else if (const auto video = _story->document()) {
  835. if (VideoPreload::Can(video)) {
  836. _task = std::make_unique<VideoPreload>(
  837. video,
  838. story->fullId(),
  839. std::move(done));
  840. } else {
  841. done();
  842. }
  843. } else {
  844. done();
  845. }
  846. }
  847. StoryPreload::~StoryPreload() = default;
  848. FullStoryId StoryPreload::id() const {
  849. return _story->fullId();
  850. }
  851. not_null<Story*> StoryPreload::story() const {
  852. return _story;
  853. }
  854. } // namespace Data