/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_story.h" #include "base/unixtime.h" #include "api/api_text_entities.h" #include "data/data_document.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_file_origin.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_thread.h" #include "history/history_item.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/streaming/media_streaming_reader.h" #include "storage/download_manager_mtproto.h" #include "storage/file_download.h" // kMaxFileInMemory #include "ui/text/text_utilities.h" namespace Data { namespace { using UpdateFlag = StoryUpdate::Flag; [[nodiscard]] StoryArea ParseArea(const MTPMediaAreaCoordinates &area) { const auto &data = area.data(); const auto center = QPointF(data.vx().v, data.vy().v); const auto size = QSizeF(data.vw().v, data.vh().v); const auto corner = center - QPointF(size.width(), size.height()) / 2.; return { .geometry = { corner / 100., size / 100. }, .rotation = data.vrotation().v, }; } [[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) { const auto link = [&](const EntityInText &entity) { return (entity.type() == EntityType::CustomUrl) || (entity.type() == EntityType::Url) || (entity.type() == EntityType::Mention) || (entity.type() == EntityType::Hashtag); }; text.entities.erase( ranges::remove_if(text.entities, link), text.entities.end()); return text; } [[nodiscard]] auto ParseLocation(const MTPMediaArea &area) -> std::optional { auto result = std::optional(); area.match([&](const MTPDmediaAreaVenue &data) { data.vgeo().match([&](const MTPDgeoPoint &geo) { result.emplace(StoryLocation{ .area = ParseArea(data.vcoordinates()), .point = Data::LocationPoint(geo), .title = qs(data.vtitle()), .address = qs(data.vaddress()), .provider = qs(data.vprovider()), .venueId = qs(data.vvenue_id()), .venueType = qs(data.vvenue_type()), }); }, [](const MTPDgeoPointEmpty &) { }); }, [&](const MTPDmediaAreaGeoPoint &data) { data.vgeo().match([&](const MTPDgeoPoint &geo) { result.emplace(StoryLocation{ .area = ParseArea(data.vcoordinates()), .point = Data::LocationPoint(geo), }); }, [](const MTPDgeoPointEmpty &) { }); }, [&](const MTPDmediaAreaSuggestedReaction &data) { }, [&](const MTPDmediaAreaChannelPost &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); }); return result; } [[nodiscard]] auto ParseSuggestedReaction(const MTPMediaArea &area) -> std::optional { auto result = std::optional(); area.match([&](const MTPDmediaAreaVenue &data) { }, [&](const MTPDmediaAreaGeoPoint &data) { }, [&](const MTPDmediaAreaSuggestedReaction &data) { result.emplace(SuggestedReaction{ .area = ParseArea(data.vcoordinates()), .reaction = Data::ReactionFromMTP(data.vreaction()), .flipped = data.is_flipped(), .dark = data.is_dark(), }); }, [&](const MTPDmediaAreaChannelPost &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); }); return result; } [[nodiscard]] auto ParseChannelPost(const MTPMediaArea &area) -> std::optional { auto result = std::optional(); area.match([&](const MTPDmediaAreaVenue &data) { }, [&](const MTPDmediaAreaGeoPoint &data) { }, [&](const MTPDmediaAreaSuggestedReaction &data) { }, [&](const MTPDmediaAreaChannelPost &data) { result.emplace(ChannelPost{ .area = ParseArea(data.vcoordinates()), .itemId = FullMsgId( peerFromChannel(data.vchannel_id()), data.vmsg_id().v), }); }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); }); return result; } [[nodiscard]] PeerData *RepostSourcePeer( not_null owner, const MTPDstoryItem &data) { if (const auto forwarded = data.vfwd_from()) { if (const auto from = forwarded->data().vfrom()) { return owner->peer(peerFromMTP(*from)); } } return nullptr; } [[nodiscard]] QString RepostSourceName(const MTPDstoryItem &data) { if (const auto forwarded = data.vfwd_from()) { return qs(forwarded->data().vfrom_name().value_or_empty()); } return {}; } [[nodiscard]] StoryId RepostSourceId(const MTPDstoryItem &data) { if (const auto forwarded = data.vfwd_from()) { return forwarded->data().vstory_id().value_or_empty(); } return {}; } [[nodiscard]] bool RepostModified(const MTPDstoryItem &data) { if (const auto forwarded = data.vfwd_from()) { return forwarded->data().is_modified(); } return false; } [[nodiscard]] PeerData *FromPeer( not_null owner, const MTPDstoryItem &data) { if (const auto from = data.vfrom_id()) { return owner->peer(peerFromMTP(*from)); } return nullptr; } } // namespace class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask { public: LoadTask( FullStoryId id, not_null document, Fn done); ~LoadTask(); private: bool readyToRequest() const override; int64 takeNextRequestOffset() override; bool feedPart(int64 offset, const QByteArray &bytes) override; void cancelOnFail() override; bool setWebFileSizeHook(int64 size) override; base::flat_map _parts; Fn _done; base::flat_set _requestedOffsets; int64 _full = 0; int _nextRequestOffset = 0; bool _finished = false; bool _failed = false; }; StoryPreload::LoadTask::LoadTask( FullStoryId id, not_null document, Fn done) : DownloadMtprotoTask( &document->session().downloader(), document->videoPreloadLocation(), id) , _done(std::move(done)) , _full(document->size) { const auto prefix = document->videoPreloadPrefix(); Assert(prefix > 0 && prefix <= document->size); const auto part = Storage::kDownloadPartSize; const auto parts = (prefix + part - 1) / part; for (auto i = 0; i != parts; ++i) { _parts.emplace(i * part, QByteArray()); } addToQueue(); } StoryPreload::LoadTask::~LoadTask() { if (!_finished && !_failed) { cancelAllRequests(); } } bool StoryPreload::LoadTask::readyToRequest() const { const auto part = Storage::kDownloadPartSize; return !_failed && (_nextRequestOffset < _parts.size() * part); } int64 StoryPreload::LoadTask::takeNextRequestOffset() { Expects(readyToRequest()); _requestedOffsets.emplace(_nextRequestOffset); _nextRequestOffset += Storage::kDownloadPartSize; return _requestedOffsets.back(); } bool StoryPreload::LoadTask::feedPart( int64 offset, const QByteArray &bytes) { Expects(offset < _parts.size() * Storage::kDownloadPartSize); Expects(_requestedOffsets.contains(int(offset))); Expects(bytes.size() <= Storage::kDownloadPartSize); const auto part = Storage::kDownloadPartSize; _requestedOffsets.remove(int(offset)); _parts[offset] = bytes; if ((_nextRequestOffset + part >= _parts.size() * part) && _requestedOffsets.empty()) { _finished = true; removeFromQueue(); auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts); if (result.size() == _full) { // Make sure it is parsed as a complex map. result.push_back(char(0)); } _done(result); } return true; } void StoryPreload::LoadTask::cancelOnFail() { _failed = true; cancelAllRequests(); _done({}); } bool StoryPreload::LoadTask::setWebFileSizeHook(int64 size) { _failed = true; cancelAllRequests(); _done({}); return false; } Story::Story( StoryId id, not_null peer, StoryMedia media, const MTPDstoryItem &data, TimeId now) : _id(id) , _peer(peer) , _repostSourcePeer(RepostSourcePeer(&peer->owner(), data)) , _repostSourceName(RepostSourceName(data)) , _repostSourceId(RepostSourceId(data)) , _fromPeer(FromPeer(&peer->owner(), data)) , _date(data.vdate().v) , _expires(data.vexpire_date().v) , _repostModified(RepostModified(data)) { applyFields(std::move(media), data, now, true); } Session &Story::owner() const { return _peer->owner(); } Main::Session &Story::session() const { return _peer->session(); } not_null Story::peer() const { return _peer; } StoryId Story::id() const { return _id; } bool Story::mine() const { return _peer->isSelf(); } StoryIdDates Story::idDates() const { return { _id, _date, _expires }; } FullStoryId Story::fullId() const { return { _peer->id, _id }; } TimeId Story::date() const { return _date; } TimeId Story::expires() const { return _expires; } bool Story::expired(TimeId now) const { return _expires <= (now ? now : base::unixtime::now()); } bool Story::unsupported() const { return v::is_null(_media.data); } const StoryMedia &Story::media() const { return _media; } PhotoData *Story::photo() const { const auto result = std::get_if>(&_media.data); return result ? result->get() : nullptr; } DocumentData *Story::document() const { const auto result = std::get_if>(&_media.data); return result ? result->get() : nullptr; } bool Story::hasReplyPreview() const { return v::match(_media.data, [](not_null photo) { return !photo->isNull(); }, [](not_null document) { return document->hasThumbnail(); }, [](v::null_t) { return false; }); } Image *Story::replyPreview() const { return v::match(_media.data, [&](not_null photo) { return photo->getReplyPreview(fullId(), _peer, false); }, [&](not_null document) { return document->getReplyPreview(fullId(), _peer, false); }, [](v::null_t) { return (Image*)nullptr; }); } TextWithEntities Story::inReplyText() const { const auto type = tr::lng_in_dlg_story(tr::now); return _caption.text.isEmpty() ? Ui::Text::Colorized(type) : tr::lng_dialogs_text_media( tr::now, lt_media_part, tr::lng_dialogs_text_media_wrapped( tr::now, lt_media, Ui::Text::Colorized(type), Ui::Text::WithEntities), lt_caption, _caption, Ui::Text::WithEntities); } void Story::setPinnedToTop(bool pinned) { if (_pinnedToTop != pinned) { _pinnedToTop = pinned; if (const auto item = _peer->owner().stories().lookupItem(this)) { item->setIsPinned(pinned); } } } bool Story::pinnedToTop() const { return _pinnedToTop; } void Story::setInProfile(bool value) { _inProfile = value; } bool Story::inProfile() const { return _inProfile; } StoryPrivacy Story::privacy() const { return _privacyPublic ? StoryPrivacy::Public : _privacyCloseFriends ? StoryPrivacy::CloseFriends : _privacyContacts ? StoryPrivacy::Contacts : _privacySelectedContacts ? StoryPrivacy::SelectedContacts : StoryPrivacy::Other; } bool Story::forbidsForward() const { return _noForwards; } bool Story::edited() const { return _edited; } bool Story::out() const { return _out; } bool Story::canDownloadIfPremium() const { return !forbidsForward() || _peer->isSelf(); } bool Story::canDownloadChecked() const { return _peer->isSelf() || (canDownloadIfPremium() && _peer->session().premium()); } bool Story::canShare() const { return _privacyPublic && !forbidsForward() && (inProfile() || !expired()); } bool Story::canDelete() const { if (const auto channel = _peer->asChannel()) { return channel->canDeleteStories() || (out() && channel->canPostStories()); } return _peer->isSelf(); } bool Story::canReport() const { return !_peer->isSelf(); } bool Story::hasDirectLink() const { if (!_privacyPublic || (!_inProfile && expired())) { return false; } return !_peer->username().isEmpty(); } std::optional Story::errorTextForForward( not_null to) const { const auto peer = to->peer(); const auto holdsPhoto = v::is>(_media.data); const auto first = holdsPhoto ? ChatRestriction::SendPhotos : ChatRestriction::SendVideos; const auto second = holdsPhoto ? ChatRestriction::SendVideos : ChatRestriction::SendPhotos; if (const auto error = Data::RestrictionError(peer, first)) { return *error; } else if (const auto error = Data::RestrictionError(peer, second)) { return *error; } else if (!Data::CanSend(to, first, false) || !Data::CanSend(to, second, false)) { return tr::lng_forward_cant(tr::now); } return {}; } void Story::setCaption(TextWithEntities &&caption) { _caption = std::move(caption); } const TextWithEntities &Story::caption() const { static const auto empty = TextWithEntities(); return unsupported() ? empty : _caption; } Data::ReactionId Story::sentReactionId() const { return _sentReactionId; } void Story::setReactionId(Data::ReactionId id) { if (_sentReactionId != id) { const auto wasEmpty = _sentReactionId.empty(); changeSuggestedReactionCount(_sentReactionId, -1); _sentReactionId = id; changeSuggestedReactionCount(id, 1); if (_views.known && _sentReactionId.empty() != wasEmpty) { const auto delta = wasEmpty ? 1 : -1; if (_views.reactions + delta >= 0) { _views.reactions += delta; } } session().changes().storyUpdated(this, UpdateFlag::Reaction); } } void Story::changeSuggestedReactionCount(Data::ReactionId id, int delta) { if (id.empty() || !_peer->isChannel()) { return; } for (auto &suggested : _suggestedReactions) { if (suggested.reaction == id && suggested.count + delta >= 0) { suggested.count += delta; } } } const std::vector> &Story::recentViewers() const { return _recentViewers; } const StoryViews &Story::viewsList() const { return _views; } const StoryViews &Story::channelReactionsList() const { return _channelReactions; } int Story::interactions() const { return _views.total; } int Story::views() const { return _views.views; } int Story::forwards() const { return _views.forwards; } int Story::reactions() const { return _views.reactions; } void Story::applyViewsSlice( const QString &offset, const StoryViews &slice) { const auto changed = (_views.reactions != slice.reactions) || (_views.views != slice.views) || (_views.forwards != slice.forwards) || (_views.total != slice.total); _views.reactions = slice.reactions; _views.forwards = slice.forwards; _views.views = slice.views; _views.total = slice.total; _views.known = true; if (offset.isEmpty()) { _views = slice; if (!_channelReactions.total) { _channelReactions.total = _views.reactions + _views.forwards; } } else if (_views.nextOffset == offset) { _views.list.insert( end(_views.list), begin(slice.list), end(slice.list)); _views.nextOffset = slice.nextOffset; if (_views.nextOffset.isEmpty()) { _views.total = int(_views.list.size()); _views.reactions = _views.total - ranges::count( _views.list, Data::ReactionId(), &StoryView::reaction); _views.forwards = _views.total - ranges::count( _views.list, 0, [](const StoryView &view) { return view.repostId ? view.repostId : view.forwardId.bare; }); } } const auto known = int(_views.list.size()); if (known >= _recentViewers.size()) { const auto take = std::min(known, kRecentViewersMax); auto viewers = _views.list | ranges::views::take(take) | ranges::views::transform(&StoryView::peer) | ranges::to_vector; if (_recentViewers != viewers) { _recentViewers = std::move(viewers); if (!changed) { // Count not changed, but list of recent viewers changed. _peer->session().changes().storyUpdated( this, UpdateFlag::ViewsChanged); } } } if (changed) { _peer->session().changes().storyUpdated( this, UpdateFlag::ViewsChanged); } } void Story::applyChannelReactionsSlice( const QString &offset, const StoryViews &slice) { const auto changed = (_channelReactions.reactions != slice.reactions) || (_channelReactions.total != slice.total); _channelReactions.reactions = slice.reactions; _channelReactions.total = slice.total; _channelReactions.known = true; if (offset.isEmpty()) { _channelReactions = slice; } else if (_channelReactions.nextOffset == offset) { _channelReactions.list.insert( end(_channelReactions.list), begin(slice.list), end(slice.list)); _channelReactions.nextOffset = slice.nextOffset; if (_channelReactions.nextOffset.isEmpty()) { _channelReactions.total = int(_channelReactions.list.size()); } } if (changed) { _peer->session().changes().storyUpdated( this, UpdateFlag::ViewsChanged); } } const std::vector &Story::locations() const { return _locations; } const std::vector &Story::suggestedReactions() const { return _suggestedReactions; } const std::vector &Story::channelPosts() const { return _channelPosts; } void Story::applyChanges( StoryMedia media, const MTPDstoryItem &data, TimeId now) { applyFields(std::move(media), data, now, false); } Story::ViewsCounts Story::parseViewsCounts( const MTPDstoryViews &data, const Data::ReactionId &mine) { auto result = ViewsCounts{ .views = data.vviews_count().v, .forwards = data.vforwards_count().value_or_empty(), .reactions = data.vreactions_count().value_or_empty(), }; if (const auto list = data.vrecent_viewers()) { result.viewers.reserve(list->v.size()); auto &owner = _peer->owner(); auto &&cut = list->v | ranges::views::take(kRecentViewersMax); for (const auto &id : cut) { result.viewers.push_back(owner.peer(peerFromUser(id))); } } auto total = 0; if (const auto list = data.vreactions() ; list && _peer->isChannel()) { result.reactionsCounts.reserve(list->v.size()); for (const auto &reaction : list->v) { const auto &data = reaction.data(); const auto id = Data::ReactionFromMTP(data.vreaction()); const auto count = data.vcount().v; result.reactionsCounts[id] = count; total += count; } } if (!mine.empty()) { if (auto &count = result.reactionsCounts[mine]; !count) { count = 1; ++total; } } if (result.reactions < total) { result.reactions = total; } return result; } void Story::applyFields( StoryMedia media, const MTPDstoryItem &data, TimeId now, bool initial) { _lastUpdateTime = now; const auto reaction = data.is_min() ? _sentReactionId : data.vsent_reaction() ? Data::ReactionFromMTP(*data.vsent_reaction()) : Data::ReactionId(); const auto inProfile = data.is_pinned(); const auto edited = data.is_edited(); const auto privacy = data.is_public() ? StoryPrivacy::Public : data.is_close_friends() ? StoryPrivacy::CloseFriends : data.is_contacts() ? StoryPrivacy::Contacts : data.is_selected_contacts() ? StoryPrivacy::SelectedContacts : StoryPrivacy::Other; const auto noForwards = data.is_noforwards(); const auto out = data.is_min() ? _out : data.is_out(); auto caption = TextWithEntities{ data.vcaption().value_or_empty(), Api::EntitiesFromMTP( &owner().session(), data.ventities().value_or_empty()), }; if (const auto user = _peer->asUser()) { if (!user->isVerified() && !user->isPremium()) { caption = StripLinks(std::move(caption)); } } auto counts = ViewsCounts(); auto viewsKnown = _views.known; if (const auto info = data.vviews()) { counts = parseViewsCounts(info->data(), reaction); viewsKnown = true; } else { counts.views = _views.total; counts.forwards = _views.forwards; counts.reactions = _views.reactions; counts.viewers = _recentViewers; for (const auto &suggested : _suggestedReactions) { if (const auto count = suggested.count) { counts.reactionsCounts[suggested.reaction] = count; } } } auto locations = std::vector(); auto suggestedReactions = std::vector(); auto channelPosts = std::vector(); if (const auto areas = data.vmedia_areas()) { for (const auto &area : areas->v) { if (const auto location = ParseLocation(area)) { locations.push_back(*location); } else if (auto reaction = ParseSuggestedReaction(area)) { const auto i = counts.reactionsCounts.find( reaction->reaction); if (i != end(counts.reactionsCounts)) { reaction->count = i->second; } suggestedReactions.push_back(*reaction); } else if (auto post = ParseChannelPost(area)) { channelPosts.push_back(*post); } } } const auto inProfileChanged = (_inProfile != inProfile); const auto editedChanged = (_edited != edited); const auto mediaChanged = (_media != media); const auto captionChanged = (_caption != caption); const auto locationsChanged = (_locations != locations); const auto suggestedReactionsChanged = (_suggestedReactions != suggestedReactions); const auto channelPostsChanged = (_channelPosts != channelPosts); const auto reactionChanged = (_sentReactionId != reaction); _out = out; _privacyPublic = (privacy == StoryPrivacy::Public); _privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends); _privacyContacts = (privacy == StoryPrivacy::Contacts); _privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts); _edited = edited; _inProfile = inProfile; _noForwards = noForwards; if (mediaChanged) { _media = std::move(media); } if (captionChanged) { _caption = std::move(caption); } if (locationsChanged) { _locations = std::move(locations); } if (suggestedReactionsChanged) { _suggestedReactions = std::move(suggestedReactions); } if (channelPostsChanged) { _channelPosts = std::move(channelPosts); } if (reactionChanged) { _sentReactionId = reaction; } updateViewsCounts(std::move(counts), viewsKnown, initial); const auto changed = editedChanged || captionChanged || mediaChanged || locationsChanged || channelPostsChanged; const auto reactionsChanged = reactionChanged || suggestedReactionsChanged; if (!initial && (changed || reactionsChanged)) { _peer->session().changes().storyUpdated(this, UpdateFlag() | (changed ? UpdateFlag::Edited : UpdateFlag()) | (reactionsChanged ? UpdateFlag::Reaction : UpdateFlag())); } if (!initial && (captionChanged || mediaChanged)) { if (const auto item = _peer->owner().stories().lookupItem(this)) { item->applyChanges(this); } _peer->owner().refreshStoryItemViews(fullId()); } if (inProfileChanged) { _peer->owner().stories().savedStateChanged(this); } } void Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) { const auto total = _views.total ? _views.total : (counts.views + counts.forwards); const auto viewsChanged = (_views.total != total) || (_views.forwards != counts.forwards) || (_views.reactions != counts.reactions) || (_recentViewers != counts.viewers); if (_views.reactions != counts.reactions || _views.forwards != counts.forwards || _views.total != total || _views.known != known) { _views = StoryViews{ .reactions = counts.reactions, .forwards = counts.forwards, .views = counts.views, .total = total, .known = known, }; if (!_channelReactions.total) { _channelReactions.total = _views.reactions + _views.forwards; } } if (viewsChanged) { _recentViewers = std::move(counts.viewers); _peer->session().changes().storyUpdated( this, UpdateFlag::ViewsChanged); } } void Story::applyViewsCounts(const MTPDstoryViews &data) { auto counts = parseViewsCounts(data, _sentReactionId); auto suggestedCountsChanged = false; for (auto &suggested : _suggestedReactions) { const auto i = counts.reactionsCounts.find(suggested.reaction); const auto v = (i != end(counts.reactionsCounts)) ? i->second : 0; if (suggested.count != v) { suggested.count = v; suggestedCountsChanged = true; } } updateViewsCounts(std::move(counts), true, false); if (suggestedCountsChanged) { _peer->session().changes().storyUpdated(this, UpdateFlag::Reaction); } } TimeId Story::lastUpdateTime() const { return _lastUpdateTime; } bool Story::repost() const { return _repostSourcePeer || !_repostSourceName.isEmpty(); } bool Story::repostModified() const { return _repostModified; } PeerData *Story::repostSourcePeer() const { return _repostSourcePeer; } QString Story::repostSourceName() const { return _repostSourceName; } StoryId Story::repostSourceId() const { return _repostSourceId; } PeerData *Story::fromPeer() const { return _fromPeer; } StoryPreload::StoryPreload(not_null story, Fn done) : _story(story) , _done(std::move(done)) { start(); } StoryPreload::~StoryPreload() { if (_photo) { base::take(_photo)->owner()->cancel(); } } FullStoryId StoryPreload::id() const { return _story->fullId(); } not_null StoryPreload::story() const { return _story; } void StoryPreload::start() { if (const auto photo = _story->photo()) { _photo = photo->createMediaView(); if (_photo->loaded()) { callDone(); } else { _photo->automaticLoad(_story->fullId(), _story->peer()); photo->session().downloaderTaskFinished( ) | rpl::filter([=] { return _photo->loaded(); }) | rpl::start_with_next([=] { callDone(); }, _lifetime); } } else if (const auto video = _story->document()) { if (video->canBeStreamed(nullptr) && video->videoPreloadPrefix()) { const auto key = video->bigFileBaseCacheKey(); if (key) { const auto weak = base::make_weak(this); video->owner().cacheBigFile().get(key, [weak]( const QByteArray &result) { if (!result.isEmpty()) { crl::on_main([weak] { if (const auto strong = weak.get()) { strong->callDone(); } }); } else { crl::on_main([weak] { if (const auto strong = weak.get()) { strong->load(); } }); } }); } else { callDone(); } } else { callDone(); } } else { callDone(); } } void StoryPreload::load() { Expects(_story->document() != nullptr); const auto video = _story->document(); const auto valid = video->videoPreloadLocation().valid(); const auto prefix = video->videoPreloadPrefix(); const auto key = video->bigFileBaseCacheKey(); if (!valid || prefix <= 0 || prefix > video->size || !key) { callDone(); return; } _task = std::make_unique(id(), video, [=](QByteArray data) { if (!data.isEmpty()) { Assert(data.size() < Storage::kMaxFileInMemory); _story->owner().cacheBigFile().putIfEmpty( key, Storage::Cache::Database::TaggedValue(std::move(data), 0)); } callDone(); }); } void StoryPreload::callDone() { if (const auto onstack = _done) { onstack(); } } } // namespace Data