/* 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_stories.h" #include "base/unixtime.h" #include "api/api_text_entities.h" #include "apiwrap.h" #include "core/application.h" #include "data/data_changes.h" #include "data/data_chat_participant_status.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_photo.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/text/text_utilities.h" // #TODO stories testing #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "storage/storage_shared_media.h" namespace Data { namespace { constexpr auto kMaxResolveTogether = 100; constexpr auto kIgnorePreloadAroundIfLoaded = 15; constexpr auto kPreloadAroundCount = 30; constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); constexpr auto kIncrementViewsDelay = 5 * crl::time(1000); constexpr auto kArchiveFirstPerPage = 30; constexpr auto kArchivePerPage = 100; constexpr auto kSavedFirstPerPage = 30; constexpr auto kSavedPerPage = 100; using UpdateFlag = StoryUpdate::Flag; [[nodiscard]] std::optional ParseMedia( not_null owner, const MTPMessageMedia &media) { return media.match([&](const MTPDmessageMediaPhoto &data) -> std::optional { if (const auto photo = data.vphoto()) { const auto result = owner->processPhoto(*photo); if (!result->isNull()) { return StoryMedia{ result }; } } return {}; }, [&](const MTPDmessageMediaDocument &data) -> std::optional { if (const auto document = data.vdocument()) { const auto result = owner->processDocument(*document); if (!result->isNull() && (result->isGifv() || result->isVideoFile())) { return StoryMedia{ result }; } } return {}; }, [](const auto &) { return std::optional(); }); } } // namespace StoriesSourceInfo StoriesSource::info() const { return { .id = user->id, .last = ids.empty() ? 0 : ids.back().date, .unread = unread(), .premium = user->isPremium(), .hidden = hidden, }; } bool StoriesSource::unread() const { return !ids.empty() && readTill < ids.back().id; } Story::Story( StoryId id, not_null peer, StoryMedia media, TimeId date, TimeId expires) : _id(id) , _peer(peer) , _media(std::move(media)) , _date(date) , _expires(expires) { } 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()); } 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(); }); } Image *Story::replyPreview() const { return v::match(_media.data, [&](not_null photo) { return photo->getReplyPreview( Data::FileOriginStory(_peer->id, _id), _peer, false); }, [&](not_null document) { return document->getReplyPreview( Data::FileOriginStory(_peer->id, _id), _peer, false); }); } TextWithEntities Story::inReplyText() const { const auto type = tr::lng_in_dlg_story(tr::now); return _caption.text.isEmpty() ? Ui::Text::PlainLink(type) : tr::lng_dialogs_text_media( tr::now, lt_media_part, tr::lng_dialogs_text_media_wrapped( tr::now, lt_media, Ui::Text::PlainLink(type), Ui::Text::WithEntities), lt_caption, _caption, Ui::Text::WithEntities); } void Story::setPinned(bool pinned) { _pinned = pinned; } bool Story::pinned() const { return _pinned; } void Story::setIsPublic(bool isPublic) { _isPublic = isPublic; } bool Story::isPublic() const { return _isPublic; } void Story::setCloseFriends(bool closeFriends) { _closeFriends = closeFriends; } bool Story::closeFriends() const { return _closeFriends; } bool Story::hasDirectLink() const { if (!_isPublic || (!_pinned && expired())) { return false; } const auto user = _peer->asUser(); return user && !user->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 { return _caption; } void Story::setViewsData( std::vector> recent, int total) { _recentViewers = std::move(recent); _views = total; } const std::vector> &Story::recentViewers() const { return _recentViewers; } const std::vector &Story::viewsList() const { return _viewsList; } int Story::views() const { return _views; } void Story::applyViewsSlice( const std::optional &offset, const std::vector &slice, int total) { _views = total; if (!offset) { const auto i = _viewsList.empty() ? end(slice) : ranges::find(slice, _viewsList.front()); const auto merge = (i != end(slice)) && !ranges::contains(slice, _viewsList.back()); if (merge) { _viewsList.insert(begin(_viewsList), begin(slice), i); } else { _viewsList = slice; } } else if (!slice.empty()) { const auto i = ranges::find(_viewsList, *offset); const auto merge = (i != end(_viewsList)) && !ranges::contains(_viewsList, slice.back()); if (merge) { const auto after = i + 1; if (after == end(_viewsList)) { _viewsList.insert(after, begin(slice), end(slice)); } else { const auto j = ranges::find(slice, _viewsList.back()); if (j != end(slice)) { _viewsList.insert(end(_viewsList), j + 1, end(slice)); } } } } } bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { const auto pinned = data.is_pinned(); const auto isPublic = data.is_public(); const auto closeFriends = data.is_close_friends(); auto caption = TextWithEntities{ data.vcaption().value_or_empty(), Api::EntitiesFromMTP( &owner().session(), data.ventities().value_or_empty()), }; auto views = 0; auto recent = std::vector>(); if (const auto info = data.vviews()) { views = info->data().vviews_count().v; if (const auto list = info->data().vrecent_viewers()) { recent.reserve(list->v.size()); auto &owner = _peer->owner(); for (const auto &id : list->v) { recent.push_back(owner.peer(peerFromUser(id))); } } } const auto changed = (_media != media) || (_pinned != pinned) || (_isPublic != isPublic) || (_closeFriends != closeFriends) || (_caption != caption) || (_views != views) || (_recentViewers != recent); if (!changed) { return false; } _media = std::move(media); _pinned = pinned; _isPublic = isPublic; _closeFriends = closeFriends; _caption = std::move(caption); _views = views; _recentViewers = std::move(recent); return true; } Stories::Stories(not_null owner) : _owner(owner) , _expireTimer([=] { processExpired(); }) , _markReadTimer([=] { sendMarkAsReadRequests(); }) , _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) { } Stories::~Stories() { } Session &Stories::owner() const { return *_owner; } Main::Session &Stories::session() const { return _owner->session(); } void Stories::apply(const MTPDupdateStory &data) { const auto peerId = peerFromUser(data.vuser_id()); const auto user = not_null(_owner->peer(peerId)->asUser()); const auto now = base::unixtime::now(); const auto idDates = parseAndApply(user, data.vstory(), now); if (!idDates) { return; } const auto expired = (idDates.expires <= now); if (expired) { applyExpired({ peerId, idDates.id }); return; } const auto i = _all.find(peerId); if (i == end(_all)) { requestUserStories(user); return; } else if (i->second.ids.contains(idDates)) { return; } const auto wasInfo = i->second.info(); i->second.ids.emplace(idDates); const auto nowInfo = i->second.info(); if (user->isSelf() && i->second.readTill < idDates.id) { i->second.readTill = idDates.id; } if (wasInfo == nowInfo) { return; } const auto refreshInList = [&](StorySourcesList list) { auto &sources = _sources[static_cast(list)]; const auto i = ranges::find( sources, peerId, &StoriesSourceInfo::id); if (i != end(sources)) { *i = nowInfo; sort(list); } }; refreshInList(StorySourcesList::All); if (!user->hasStoriesHidden()) { refreshInList(StorySourcesList::NotHidden); } } void Stories::apply(not_null peer, const MTPUserStories *data) { if (!data) { applyDeletedFromSources(peer->id, StorySourcesList::All); _all.erase(peer->id); _sourceChanged.fire_copy(peer->id); } else { parseAndApply(*data); } } void Stories::requestUserStories(not_null user) { if (!_requestingUserStories.emplace(user).second) { return; } _owner->session().api().request(MTPstories_GetUserStories( user->inputUser )).done([=](const MTPstories_UserStories &result) { _requestingUserStories.remove(user); const auto &data = result.data(); _owner->processUsers(data.vusers()); parseAndApply(data.vstories()); }).fail([=] { _requestingUserStories.remove(user); applyDeletedFromSources(user->id, StorySourcesList::All); }).send(); } void Stories::registerExpiring(TimeId expires, FullStoryId id) { for (auto i = _expiring.findFirst(expires) ; (i != end(_expiring)) && (i->first == expires) ; ++i) { if (i->second == id) { return; } } const auto reschedule = _expiring.empty() || (_expiring.front().first > expires); _expiring.emplace(expires, id); if (reschedule) { scheduleExpireTimer(); } } void Stories::scheduleExpireTimer() { if (_expireSchedulePosted) { return; } _expireSchedulePosted = true; crl::on_main(this, [=] { if (!_expireSchedulePosted) { return; } _expireSchedulePosted = false; if (_expiring.empty()) { _expireTimer.cancel(); } else { const auto nearest = _expiring.front().first; const auto now = base::unixtime::now(); const auto delay = (nearest > now) ? (nearest - now) : 0; _expireTimer.callOnce(delay * crl::time(1000)); } }); } void Stories::processExpired() { const auto now = base::unixtime::now(); auto expired = base::flat_set(); auto i = begin(_expiring); for (; i != end(_expiring) && i->first <= now; ++i) { expired.emplace(i->second); } _expiring.erase(begin(_expiring), i); for (const auto &id : expired) { applyExpired(id); } if (!_expiring.empty()) { scheduleExpireTimer(); } } void Stories::parseAndApply(const MTPUserStories &stories) { const auto &data = stories.data(); const auto peerId = peerFromUser(data.vuser_id()); const auto readTill = data.vmax_read_id().value_or_empty(); const auto count = int(data.vstories().v.size()); const auto user = _owner->peer(peerId)->asUser(); auto result = StoriesSource{ .user = user, .readTill = readTill, .hidden = user->hasStoriesHidden(), }; const auto &list = data.vstories().v; const auto now = base::unixtime::now(); result.ids.reserve(list.size()); for (const auto &story : list) { if (const auto id = parseAndApply(result.user, story, now)) { result.ids.emplace(id); } } if (result.ids.empty()) { applyDeletedFromSources(peerId, StorySourcesList::All); return; } else if (user->isSelf()) { result.readTill = result.ids.back().id; } const auto info = result.info(); const auto i = _all.find(peerId); if (i != end(_all)) { if (i->second != result) { i->second = std::move(result); } } else { _all.emplace(peerId, std::move(result)); } const auto add = [&](StorySourcesList list) { auto &sources = _sources[static_cast(list)]; const auto i = ranges::find( sources, peerId, &StoriesSourceInfo::id); if (i == end(sources)) { sources.push_back(info); } else if (*i == info) { return; } else { *i = info; } sort(list); }; if (result.user->isContact()) { add(StorySourcesList::All); if (result.user->hasStoriesHidden()) { applyDeletedFromSources(peerId, StorySourcesList::NotHidden); } else { add(StorySourcesList::NotHidden); } } else { applyDeletedFromSources(peerId, StorySourcesList::All); } _sourceChanged.fire_copy(peerId); } Story *Stories::parseAndApply( not_null peer, const MTPDstoryItem &data, TimeId now) { const auto media = ParseMedia(_owner, data.vmedia()); if (!media) { return nullptr; } const auto expires = data.vexpire_date().v; const auto expired = (expires <= now); if (expired && !data.is_pinned() && !peer->isSelf()) { return nullptr; } const auto id = data.vid().v; auto &stories = _stories[peer->id]; const auto i = stories.find(id); if (i != end(stories)) { const auto result = i->second.get(); const auto pinned = result->pinned(); if (result->applyChanges(*media, data)) { if (result->pinned() != pinned) { savedStateUpdated(result); } session().changes().storyUpdated( result, UpdateFlag::Edited); if (const auto item = lookupItem(result)) { item->applyChanges(result); } } return result; } const auto result = stories.emplace(id, std::make_unique( id, peer, StoryMedia{ *media }, data.vdate().v, data.vexpire_date().v)).first->second.get(); result->applyChanges(*media, data); if (result->pinned()) { savedStateUpdated(result); } if (peer->isSelf()) { const auto added = _archive.list.emplace(id).second; if (added) { if (_archiveTotal >= 0 && id > _archiveLastId) { ++_archiveTotal; } _archiveChanged.fire({}); } } if (expired) { _expiring.remove(expires, result->fullId()); applyExpired(result->fullId()); } else { registerExpiring(expires, result->fullId()); } return result; } StoryIdDates Stories::parseAndApply( not_null peer, const MTPstoryItem &story, TimeId now) { return story.match([&](const MTPDstoryItem &data) { if (const auto story = parseAndApply(peer, data, now)) { return story->idDates(); } applyDeleted({ peer->id, data.vid().v }); return StoryIdDates(); }, [&](const MTPDstoryItemSkipped &data) { const auto expires = data.vexpire_date().v; const auto expired = (expires <= now); const auto fullId = FullStoryId{ peer->id, data.vid().v }; if (!expired) { registerExpiring(expires, fullId); } else if (!peer->isSelf()) { applyDeleted(fullId); return StoryIdDates(); } else { _expiring.remove(expires, fullId); applyExpired(fullId); } return StoryIdDates{ data.vid().v, data.vdate().v, data.vexpire_date().v, }; }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peer->id, data.vid().v }); return StoryIdDates(); }); } void Stories::updateDependentMessages(not_null story) { const auto i = _dependentMessages.find(story); if (i != end(_dependentMessages)) { for (const auto &dependent : i->second) { dependent->updateDependencyItem(); } } session().changes().storyUpdated( story, Data::StoryUpdate::Flag::Edited); } void Stories::registerDependentMessage( not_null dependent, not_null dependency) { _dependentMessages[dependency].emplace(dependent); } void Stories::unregisterDependentMessage( not_null dependent, not_null dependency) { const auto i = _dependentMessages.find(dependency); if (i != end(_dependentMessages)) { if (i->second.remove(dependent) && i->second.empty()) { _dependentMessages.erase(i); } } } void Stories::savedStateUpdated(not_null story) { const auto id = story->id(); const auto peer = story->peer()->id; const auto pinned = story->pinned(); if (pinned) { auto &saved = _saved[peer]; const auto added = saved.ids.list.emplace(id).second; if (added) { if (saved.total >= 0 && id > saved.lastId) { ++saved.total; } _savedChanged.fire_copy(peer); } } else if (const auto i = _saved.find(peer); i != end(_saved)) { auto &saved = i->second; if (saved.ids.list.remove(id)) { if (saved.total > 0) { --saved.total; } _savedChanged.fire_copy(peer); } } } void Stories::loadMore(StorySourcesList list) { const auto index = static_cast(list); if (_loadMoreRequestId[index] || _sourcesLoaded[index]) { return; } const auto all = (list == StorySourcesList::All); const auto api = &_owner->session().api(); using Flag = MTPstories_GetAllStories::Flag; _loadMoreRequestId[index] = api->request(MTPstories_GetAllStories( MTP_flags((all ? Flag::f_include_hidden : Flag()) | (_sourcesStates[index].isEmpty() ? Flag(0) : (Flag::f_next | Flag::f_state))), MTP_string(_sourcesStates[index]) )).done([=](const MTPstories_AllStories &result) { _loadMoreRequestId[index] = 0; result.match([&](const MTPDstories_allStories &data) { _owner->processUsers(data.vusers()); _sourcesStates[index] = qs(data.vstate()); _sourcesLoaded[index] = !data.is_has_more(); for (const auto &single : data.vuser_stories().v) { parseAndApply(single); } }, [](const MTPDstories_allStoriesNotModified &) { }); }).fail([=] { _loadMoreRequestId[index] = 0; }).send(); } void Stories::sendResolveRequests() { if (!_resolveSent.empty()) { return; } auto leftToSend = kMaxResolveTogether; auto byPeer = base::flat_map>(); for (auto i = begin(_resolvePending); i != end(_resolvePending);) { auto &[peerId, ids] = *i; auto &sent = _resolveSent[peerId]; if (ids.size() <= leftToSend) { sent = base::take(ids); i = _resolvePending.erase(i); leftToSend -= int(sent.size()); } else { sent = { std::make_move_iterator(begin(ids)), std::make_move_iterator(begin(ids) + leftToSend) }; ids.erase(begin(ids), begin(ids) + leftToSend); leftToSend = 0; } auto &prepared = byPeer[peerId]; for (auto &[storyId, callbacks] : sent) { prepared.push_back(MTP_int(storyId)); } if (!leftToSend) { break; } } const auto api = &_owner->session().api(); for (auto &entry : byPeer) { const auto peerId = entry.first; auto &prepared = entry.second; const auto finish = [=](PeerId peerId) { const auto sent = _resolveSent.take(peerId); Assert(sent.has_value()); for (const auto &[storyId, list] : *sent) { finalizeResolve({ peerId, storyId }); for (const auto &callback : list) { callback(); } } _itemsChanged.fire_copy(peerId); if (_resolveSent.empty() && !_resolvePending.empty()) { crl::on_main(&session(), [=] { sendResolveRequests(); }); } }; const auto user = _owner->session().data().peer(peerId)->asUser(); if (!user) { finish(peerId); continue; } const auto requestId = api->request(MTPstories_GetStoriesByID( user->inputUser, MTP_vector(prepared) )).done([=](const MTPstories_Stories &result) { owner().processUsers(result.data().vusers()); processResolvedStories(user, result.data().vstories().v); finish(user->id); }).fail([=] { finish(peerId); }).send(); } } void Stories::processResolvedStories( not_null peer, const QVector &list) { const auto now = base::unixtime::now(); for (const auto &item : list) { item.match([&](const MTPDstoryItem &data) { if (!parseAndApply(peer, data, now)) { applyDeleted({ peer->id, data.vid().v }); } }, [&](const MTPDstoryItemSkipped &data) { LOG(("API Error: Unexpected storyItemSkipped in resolve.")); }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peer->id, data.vid().v }); }); } } void Stories::finalizeResolve(FullStoryId id) { const auto already = lookup(id); if (!already.has_value() && already.error() == NoStory::Unknown) { LOG(("API Error: Could not resolve story %1_%2" ).arg(id.peer.value ).arg(id.story)); applyDeleted(id); } } void Stories::applyDeleted(FullStoryId id) { applyRemovedFromActive(id); _deleted.emplace(id); const auto i = _stories.find(id.peer); if (i != end(_stories)) { const auto j = i->second.find(id.story); if (j != end(i->second)) { // Duplicated in Stories::apply(peer, const MTPUserStories*). auto story = std::move(j->second); _expiring.remove(story->expires(), story->fullId()); i->second.erase(j); session().changes().storyUpdated( story.get(), UpdateFlag::Destroyed); removeDependencyStory(story.get()); if (id.peer == session().userPeerId() && _archive.list.remove(id.story)) { if (_archiveTotal > 0) { --_archiveTotal; } _archiveChanged.fire({}); } if (story->pinned()) { if (const auto k = _saved.find(id.peer); k != end(_saved)) { const auto saved = &k->second; if (saved->ids.list.remove(id.story)) { if (saved->total > 0) { --saved->total; } _savedChanged.fire_copy(id.peer); } } } if (i->second.empty()) { _stories.erase(i); } } } } void Stories::applyExpired(FullStoryId id) { if (const auto maybeStory = lookup(id)) { const auto story = *maybeStory; if (!story->peer()->isSelf() && !story->pinned()) { applyDeleted(id); return; } } applyRemovedFromActive(id); } void Stories::applyRemovedFromActive(FullStoryId id) { const auto removeFromList = [&](StorySourcesList list) { const auto index = static_cast(list); auto &sources = _sources[index]; const auto i = ranges::find( sources, id.peer, &StoriesSourceInfo::id); if (i != end(sources)) { sources.erase(i); _sourcesChanged[index].fire({}); } }; const auto i = _all.find(id.peer); if (i != end(_all)) { const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j != end(i->second.ids) && j->id == id.story) { i->second.ids.erase(j); if (i->second.ids.empty()) { _all.erase(i); removeFromList(StorySourcesList::NotHidden); removeFromList(StorySourcesList::All); } _sourceChanged.fire_copy(id.peer); } } } void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { const auto removeFromList = [&](StorySourcesList from) { auto &sources = _sources[static_cast(from)]; const auto i = ranges::find( sources, id, &StoriesSourceInfo::id); if (i != end(sources)) { sources.erase(i); } _sourcesChanged[static_cast(from)].fire({}); }; removeFromList(StorySourcesList::NotHidden); if (list == StorySourcesList::All) { removeFromList(StorySourcesList::All); } } void Stories::removeDependencyStory(not_null story) { const auto i = _dependentMessages.find(story); if (i != end(_dependentMessages)) { const auto items = std::move(i->second); _dependentMessages.erase(i); for (const auto &dependent : items) { dependent->dependencyStoryRemoved(story); } } } void Stories::sort(StorySourcesList list) { const auto index = static_cast(list); auto &sources = _sources[index]; const auto self = _owner->session().user()->id; const auto proj = [&](const StoriesSourceInfo &info) { const auto key = int64(info.last) + (info.premium ? (int64(1) << 48) : 0) + (info.unread ? (int64(1) << 49) : 0) + ((info.id == self) ? (int64(1) << 50) : 0); return std::make_pair(key, info.id); }; ranges::sort(sources, ranges::greater(), proj); _sourcesChanged[index].fire({}); } std::shared_ptr Stories::lookupItem(not_null story) { const auto i = _items.find(story->peer()->id); if (i == end(_items)) { return nullptr; } const auto j = i->second.find(story->id()); if (j == end(i->second)) { return nullptr; } return j->second.lock(); } std::shared_ptr Stories::resolveItem(not_null story) { auto &items = _items[story->peer()->id]; auto i = items.find(story->id()); if (i == end(items)) { i = items.emplace(story->id()).first; } else if (const auto result = i->second.lock()) { return result; } const auto history = _owner->history(story->peer()); auto result = std::shared_ptr( history->makeMessage(story).get(), HistoryItem::Destroyer()); i->second = result; return result; } std::shared_ptr Stories::resolveItem(FullStoryId id) { const auto story = lookup(id); return story ? resolveItem(*story) : std::shared_ptr(); } const StoriesSource *Stories::source(PeerId id) const { const auto i = _all.find(id); return (i != end(_all)) ? &i->second : nullptr; } const std::vector &Stories::sources( StorySourcesList list) const { return _sources[static_cast(list)]; } bool Stories::sourcesLoaded(StorySourcesList list) const { return _sourcesLoaded[static_cast(list)]; } rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { return _sourcesChanged[static_cast(list)].events(); } rpl::producer Stories::sourceChanged() const { return _sourceChanged.events(); } rpl::producer Stories::itemsChanged() const { return _itemsChanged.events(); } base::expected, NoStory> Stories::lookup( FullStoryId id) const { const auto i = _stories.find(id.peer); if (i != end(_stories)) { const auto j = i->second.find(id.story); if (j != end(i->second)) { return j->second.get(); } } return base::make_unexpected( _deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown); } void Stories::resolve(FullStoryId id, Fn done) { const auto already = lookup(id); if (already.has_value() || already.error() != NoStory::Unknown) { if (done) { done(); } return; } if (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) { if (const auto j = i->second.find(id.story); j != end(i->second)) { if (done) { j->second.push_back(std::move(done)); } return; } } auto &ids = _resolvePending[id.peer]; if (ids.empty()) { crl::on_main(&session(), [=] { sendResolveRequests(); }); } auto &callbacks = ids[id.story]; if (done) { callbacks.push_back(std::move(done)); } } void Stories::loadAround(FullStoryId id, StoriesContext context) { if (v::is(context.data)) { return; } else if (v::is(context.data) || v::is(context.data)) { return; } const auto i = _all.find(id.peer); if (i == end(_all)) { return; } const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j == end(i->second.ids) || j->id != id.story) { return; } const auto ignore = [&] { const auto side = kIgnorePreloadAroundIfLoaded; const auto left = ranges::min(int(j - begin(i->second.ids)), side); const auto right = ranges::min(int(end(i->second.ids) - j), side); for (auto k = j - left; k != j + right; ++k) { const auto maybeStory = lookup({ id.peer, k->id }); if (!maybeStory && maybeStory.error() == NoStory::Unknown) { return false; } } return true; }(); if (ignore) { return; } const auto side = kPreloadAroundCount; const auto left = ranges::min(int(j - begin(i->second.ids)), side); const auto right = ranges::min(int(end(i->second.ids) - j), side); const auto from = j - left; const auto till = j + right; for (auto k = from; k != till; ++k) { resolve({ id.peer, k->id }, nullptr); } } void Stories::markAsRead(FullStoryId id, bool viewed) { if (id.peer == _owner->session().userPeerId()) { return; } const auto maybeStory = lookup(id); if (!maybeStory) { return; } const auto story = *maybeStory; if (story->expired() && story->pinned()) { _incrementViewsPending[id.peer].emplace(id.story); if (!_incrementViewsTimer.isActive()) { _incrementViewsTimer.callOnce(kIncrementViewsDelay); } } const auto i = _all.find(id.peer); Assert(i != end(_all)); if (i->second.readTill >= id.story) { return; } else if (!_markReadPending.contains(id.peer)) { sendMarkAsReadRequests(); } _markReadPending.emplace(id.peer); const auto wasUnread = i->second.unread(); i->second.readTill = id.story; const auto nowUnread = i->second.unread(); if (wasUnread != nowUnread) { const auto refreshInList = [&](StorySourcesList list) { auto &sources = _sources[static_cast(list)]; const auto i = ranges::find( sources, id.peer, &StoriesSourceInfo::id); if (i != end(sources)) { i->unread = nowUnread; sort(list); } }; refreshInList(StorySourcesList::All); refreshInList(StorySourcesList::NotHidden); } _markReadTimer.callOnce(kMarkAsReadDelay); } void Stories::toggleHidden(PeerId peerId, bool hidden) { const auto user = _owner->peer(peerId)->asUser(); Assert(user != nullptr); if (user->hasStoriesHidden() != hidden) { user->setFlags(hidden ? (user->flags() | UserDataFlag::StoriesHidden) : (user->flags() & ~UserDataFlag::StoriesHidden)); session().api().request(MTPcontacts_ToggleStoriesHidden( user->inputUser, MTP_bool(hidden) )).send(); } const auto i = _all.find(peerId); if (i == end(_all)) { return; } i->second.hidden = hidden; const auto main = static_cast(StorySourcesList::NotHidden); const auto all = static_cast(StorySourcesList::All); if (hidden) { const auto i = ranges::find( _sources[main], peerId, &StoriesSourceInfo::id); if (i != end(_sources[main])) { _sources[main].erase(i); _sourcesChanged[main].fire({}); } const auto j = ranges::find(_sources[all], peerId, &StoriesSourceInfo::id); if (j != end(_sources[all])) { j->hidden = hidden; _sourcesChanged[all].fire({}); } } else { const auto i = ranges::find( _sources[all], peerId, &StoriesSourceInfo::id); if (i != end(_sources[all])) { i->hidden = hidden; _sourcesChanged[all].fire({}); auto &sources = _sources[main]; if (!ranges::contains(sources, peerId, &StoriesSourceInfo::id)) { sources.push_back(*i); sort(StorySourcesList::NotHidden); } } } } void Stories::sendMarkAsReadRequest( not_null peer, StoryId tillId) { Expects(peer->isUser()); const auto peerId = peer->id; _markReadRequests.emplace(peerId); const auto finish = [=] { _markReadRequests.remove(peerId); if (!_markReadTimer.isActive() && _markReadPending.contains(peerId)) { sendMarkAsReadRequests(); } checkQuitPreventFinished(); }; const auto api = &_owner->session().api(); api->request(MTPstories_ReadStories( peer->asUser()->inputUser, MTP_int(tillId) )).done(finish).fail(finish).send(); } void Stories::checkQuitPreventFinished() { if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { if (Core::Quitting()) { LOG(("Stories doesn't prevent quit any more.")); } Core::App().quitPreventFinished(); } } void Stories::sendMarkAsReadRequests() { _markReadTimer.cancel(); for (auto i = begin(_markReadPending); i != end(_markReadPending);) { const auto peerId = *i; if (_markReadRequests.contains(peerId)) { ++i; continue; } const auto j = _all.find(peerId); if (j != end(_all)) { sendMarkAsReadRequest(j->second.user, j->second.readTill); } i = _markReadPending.erase(i); } } void Stories::sendIncrementViewsRequests() { if (_incrementViewsPending.empty()) { return; } auto ids = QVector(); auto peer = PeerId(); struct Prepared { PeerId peer = 0; QVector ids; }; auto prepared = std::vector(); for (const auto &[peer, ids] : _incrementViewsPending) { if (_incrementViewsRequests.contains(peer)) { continue; } prepared.push_back({ .peer = peer }); for (const auto &id : ids) { prepared.back().ids.push_back(MTP_int(id)); } } const auto api = &_owner->session().api(); for (auto &[peer, ids] : prepared) { _incrementViewsRequests.emplace(peer); const auto finish = [=, peer = peer] { _incrementViewsRequests.remove(peer); if (!_incrementViewsTimer.isActive() && _incrementViewsPending.contains(peer)) { sendIncrementViewsRequests(); } checkQuitPreventFinished(); }; api->request(MTPstories_IncrementStoryViews( _owner->peer(peer)->asUser()->inputUser, MTP_vector(std::move(ids)) )).done(finish).fail(finish).send(); _incrementViewsPending.remove(peer); } } void Stories::loadViewsSlice( StoryId id, std::optional offset, Fn)> done) { _viewsDone = std::move(done); if (_viewsStoryId == id && _viewsOffset == offset) { return; } _viewsStoryId = id; _viewsOffset = offset; const auto api = &_owner->session().api(); api->request(_viewsRequestId).cancel(); _viewsRequestId = api->request(MTPstories_GetStoryViewsList( MTP_int(id), MTP_int(offset ? offset->date : 0), MTP_long(offset ? peerToUser(offset->peer->id).bare : 0), MTP_int(kViewsPerPage) )).done([=](const MTPstories_StoryViewsList &result) { _viewsRequestId = 0; auto slice = std::vector(); const auto &data = result.data(); _owner->processUsers(data.vusers()); slice.reserve(data.vviews().v.size()); for (const auto &view : data.vviews().v) { slice.push_back({ .peer = _owner->peer(peerFromUser(view.data().vuser_id())), .date = view.data().vdate().v, }); } const auto fullId = FullStoryId{ .peer = _owner->session().userPeerId(), .story = _viewsStoryId, }; if (const auto story = lookup(fullId)) { (*story)->applyViewsSlice(_viewsOffset, slice, data.vcount().v); } if (const auto done = base::take(_viewsDone)) { done(std::move(slice)); } }).fail([=] { _viewsRequestId = 0; if (const auto done = base::take(_viewsDone)) { done({}); } }).send(); } const StoriesIds &Stories::archive() const { return _archive; } rpl::producer<> Stories::archiveChanged() const { return _archiveChanged.events(); } int Stories::archiveCount() const { return std::max(_archiveTotal, 0); } bool Stories::archiveCountKnown() const { return _archiveTotal >= 0; } bool Stories::archiveLoaded() const { return _archiveLoaded; } const StoriesIds *Stories::saved(PeerId peerId) const { const auto i = _saved.find(peerId); return (i != end(_saved)) ? &i->second.ids : nullptr; } rpl::producer Stories::savedChanged() const { return _savedChanged.events(); } int Stories::savedCount(PeerId peerId) const { const auto i = _saved.find(peerId); return (i != end(_saved)) ? i->second.total : 0; } bool Stories::savedCountKnown(PeerId peerId) const { const auto i = _saved.find(peerId); return (i != end(_saved)) && (i->second.total >= 0); } bool Stories::savedLoaded(PeerId peerId) const { const auto i = _saved.find(peerId); return (i != end(_saved)) && i->second.loaded; } void Stories::archiveLoadMore() { if (_archiveRequestId || _archiveLoaded) { return; } const auto api = &_owner->session().api(); _archiveRequestId = api->request(MTPstories_GetStoriesArchive( MTP_int(_archiveLastId), MTP_int(_archiveLastId ? kArchivePerPage : kArchiveFirstPerPage) )).done([=](const MTPstories_Stories &result) { _archiveRequestId = 0; const auto &data = result.data(); const auto self = _owner->session().user(); const auto now = base::unixtime::now(); _archiveTotal = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { return id.vid().v; }); _archive.list.emplace(id); _archiveLastId = id; if (!parseAndApply(self, story, now)) { _archive.list.remove(id); if (_archiveTotal > 0) { --_archiveTotal; } } } const auto ids = int(_archive.list.size()); _archiveLoaded = data.vstories().v.empty(); _archiveTotal = _archiveLoaded ? ids : std::max(_archiveTotal, ids); _archiveChanged.fire({}); }).fail([=] { _archiveRequestId = 0; _archiveLoaded = true; _archiveTotal = int(_archive.list.size()); _archiveChanged.fire({}); }).send(); } void Stories::savedLoadMore(PeerId peerId) { Expects(peerIsUser(peerId)); auto &saved = _saved[peerId]; if (saved.requestId || saved.loaded) { return; } const auto api = &_owner->session().api(); const auto peer = _owner->peer(peerId); saved.requestId = api->request(MTPstories_GetPinnedStories( peer->asUser()->inputUser, MTP_int(saved.lastId), MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) )).done([=](const MTPstories_Stories &result) { auto &saved = _saved[peerId]; saved.requestId = 0; const auto &data = result.data(); const auto now = base::unixtime::now(); saved.total = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { return id.vid().v; }); saved.ids.list.emplace(id); saved.lastId = id; if (!parseAndApply(peer, story, now)) { saved.ids.list.remove(id); if (saved.total > 0) { --saved.total; } } } const auto ids = int(saved.ids.list.size()); saved.loaded = data.vstories().v.empty(); saved.total = saved.loaded ? ids : std::max(saved.total, ids); _savedChanged.fire_copy(peerId); }).fail([=] { auto &saved = _saved[peerId]; saved.requestId = 0; saved.loaded = true; saved.total = int(saved.ids.list.size()); _savedChanged.fire_copy(peerId); }).send(); } bool Stories::isQuitPrevent() { if (!_markReadPending.empty()) { sendMarkAsReadRequests(); } if (!_incrementViewsPending.empty()) { sendIncrementViewsRequests(); } if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { return false; } LOG(("Stories prevents quit, marking as read...")); return true; } } // namespace Data