2023-05-03 18:30:37 +00:00
|
|
|
/*
|
|
|
|
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 "api/api_text_entities.h"
|
2023-05-26 09:27:34 +00:00
|
|
|
#include "apiwrap.h"
|
2023-05-29 15:09:36 +00:00
|
|
|
#include "core/application.h"
|
2023-05-26 09:27:34 +00:00
|
|
|
#include "data/data_changes.h"
|
2023-05-03 18:30:37 +00:00
|
|
|
#include "data/data_document.h"
|
2023-05-26 09:27:34 +00:00
|
|
|
#include "data/data_file_origin.h"
|
2023-05-03 18:30:37 +00:00
|
|
|
#include "data/data_photo.h"
|
|
|
|
#include "data/data_session.h"
|
2023-05-26 09:27:34 +00:00
|
|
|
#include "lang/lang_keys.h"
|
2023-05-03 18:30:37 +00:00
|
|
|
#include "main/main_session.h"
|
2023-05-26 09:27:34 +00:00
|
|
|
#include "ui/text/text_utilities.h"
|
2023-05-03 18:30:37 +00:00
|
|
|
|
|
|
|
// #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 {
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
constexpr auto kMaxResolveTogether = 100;
|
2023-05-29 12:03:23 +00:00
|
|
|
constexpr auto kIgnorePreloadAroundIfLoaded = 15;
|
|
|
|
constexpr auto kPreloadAroundCount = 30;
|
2023-05-29 15:09:36 +00:00
|
|
|
constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
|
2023-05-26 09:27:34 +00:00
|
|
|
|
|
|
|
using UpdateFlag = StoryUpdate::Flag;
|
|
|
|
|
2023-05-26 15:18:10 +00:00
|
|
|
std::optional<StoryMedia> ParseMedia(
|
2023-05-30 17:12:15 +00:00
|
|
|
not_null<Session*> owner,
|
|
|
|
const MTPMessageMedia &media) {
|
2023-05-26 15:18:10 +00:00
|
|
|
return media.match([&](const MTPDmessageMediaPhoto &data)
|
2023-05-30 17:12:15 +00:00
|
|
|
-> std::optional<StoryMedia> {
|
2023-05-26 15:18:10 +00:00
|
|
|
if (const auto photo = data.vphoto()) {
|
|
|
|
const auto result = owner->processPhoto(*photo);
|
|
|
|
if (!result->isNull()) {
|
|
|
|
return StoryMedia{ result };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}, [&](const MTPDmessageMediaDocument &data)
|
2023-05-30 17:12:15 +00:00
|
|
|
-> std::optional<StoryMedia> {
|
2023-05-26 15:18:10 +00:00
|
|
|
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<StoryMedia>(); });
|
|
|
|
}
|
|
|
|
|
2023-05-03 18:30:37 +00:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
bool StoriesList::unread() const {
|
2023-05-29 15:09:36 +00:00
|
|
|
return !ids.empty() && readTill < ids.back();
|
2023-05-26 07:21:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Story::Story(
|
|
|
|
StoryId id,
|
|
|
|
not_null<PeerData*> peer,
|
|
|
|
StoryMedia media,
|
|
|
|
TimeId date)
|
2023-05-30 17:12:15 +00:00
|
|
|
: _id(id)
|
|
|
|
, _peer(peer)
|
|
|
|
, _media(std::move(media))
|
|
|
|
, _date(date) {
|
2023-05-26 07:21:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Session &Story::owner() const {
|
|
|
|
return _peer->owner();
|
|
|
|
}
|
|
|
|
|
|
|
|
Main::Session &Story::session() const {
|
|
|
|
return _peer->session();
|
|
|
|
}
|
|
|
|
|
|
|
|
not_null<PeerData*> Story::peer() const {
|
|
|
|
return _peer;
|
|
|
|
}
|
|
|
|
|
|
|
|
StoryId Story::id() const {
|
|
|
|
return _id;
|
|
|
|
}
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
FullStoryId Story::fullId() const {
|
|
|
|
return { _peer->id, _id };
|
|
|
|
}
|
|
|
|
|
2023-05-26 07:21:19 +00:00
|
|
|
TimeId Story::date() const {
|
|
|
|
return _date;
|
|
|
|
}
|
|
|
|
|
|
|
|
const StoryMedia &Story::media() const {
|
|
|
|
return _media;
|
|
|
|
}
|
|
|
|
|
|
|
|
PhotoData *Story::photo() const {
|
|
|
|
const auto result = std::get_if<not_null<PhotoData*>>(&_media.data);
|
|
|
|
return result ? result->get() : nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
DocumentData *Story::document() const {
|
|
|
|
const auto result = std::get_if<not_null<DocumentData*>>(&_media.data);
|
|
|
|
return result ? result->get() : nullptr;
|
|
|
|
}
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
bool Story::hasReplyPreview() const {
|
|
|
|
return v::match(_media.data, [](not_null<PhotoData*> photo) {
|
|
|
|
return !photo->isNull();
|
|
|
|
}, [](not_null<DocumentData*> document) {
|
|
|
|
return document->hasThumbnail();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Image *Story::replyPreview() const {
|
|
|
|
return v::match(_media.data, [&](not_null<PhotoData*> photo) {
|
|
|
|
return photo->getReplyPreview(
|
|
|
|
Data::FileOriginStory(_peer->id, _id),
|
|
|
|
_peer,
|
|
|
|
false);
|
|
|
|
}, [&](not_null<DocumentData*> document) {
|
|
|
|
return document->getReplyPreview(
|
|
|
|
Data::FileOriginStory(_peer->id, _id),
|
|
|
|
_peer,
|
|
|
|
false);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
TextWithEntities Story::inReplyText() const {
|
2023-05-26 15:18:10 +00:00
|
|
|
const auto type = tr::lng_in_dlg_story(tr::now);
|
2023-05-26 09:27:34 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-05-26 07:21:19 +00:00
|
|
|
void Story::setPinned(bool pinned) {
|
|
|
|
_pinned = pinned;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Story::pinned() const {
|
|
|
|
return _pinned;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Story::setCaption(TextWithEntities &&caption) {
|
|
|
|
_caption = std::move(caption);
|
|
|
|
}
|
|
|
|
|
|
|
|
const TextWithEntities &Story::caption() const {
|
|
|
|
return _caption;
|
|
|
|
}
|
|
|
|
|
2023-05-30 17:12:15 +00:00
|
|
|
void Story::setViewsData(
|
|
|
|
std::vector<not_null<PeerData*>> recent,
|
|
|
|
int total) {
|
|
|
|
_recentViewers = std::move(recent);
|
|
|
|
_views = total;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
|
|
|
|
return _recentViewers;
|
|
|
|
}
|
|
|
|
|
2023-06-01 16:01:29 +00:00
|
|
|
const std::vector<StoryView> &Story::viewsList() const {
|
|
|
|
return _viewsList;
|
|
|
|
}
|
|
|
|
|
2023-05-30 17:12:15 +00:00
|
|
|
int Story::views() const {
|
|
|
|
return _views;
|
|
|
|
}
|
|
|
|
|
2023-06-01 16:01:29 +00:00
|
|
|
void Story::applyViewsSlice(
|
|
|
|
const std::optional<StoryView> &offset,
|
|
|
|
const std::vector<StoryView> &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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 15:18:10 +00:00
|
|
|
bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
|
|
|
|
const auto pinned = data.is_pinned();
|
|
|
|
auto caption = TextWithEntities{
|
2023-05-26 07:21:19 +00:00
|
|
|
data.vcaption().value_or_empty(),
|
|
|
|
Api::EntitiesFromMTP(
|
|
|
|
&owner().session(),
|
|
|
|
data.ventities().value_or_empty()),
|
|
|
|
};
|
2023-05-30 17:12:15 +00:00
|
|
|
auto views = 0;
|
|
|
|
auto recent = std::vector<not_null<PeerData*>>();
|
|
|
|
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)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 15:18:10 +00:00
|
|
|
const auto changed = (_media != media)
|
|
|
|
|| (_pinned != pinned)
|
2023-05-30 17:12:15 +00:00
|
|
|
|| (_caption != caption)
|
|
|
|
|| (_views != views)
|
|
|
|
|| (_recentViewers != recent);
|
2023-05-26 15:18:10 +00:00
|
|
|
if (!changed) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
_media = std::move(media);
|
|
|
|
_pinned = pinned;
|
|
|
|
_caption = std::move(caption);
|
2023-05-30 17:12:15 +00:00
|
|
|
_views = views;
|
|
|
|
_recentViewers = std::move(recent);
|
2023-05-26 15:18:10 +00:00
|
|
|
return true;
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-05-29 15:09:36 +00:00
|
|
|
Stories::Stories(not_null<Session*> owner)
|
|
|
|
: _owner(owner)
|
|
|
|
, _markReadTimer([=] { sendMarkAsReadRequests(); }) {
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Stories::~Stories() {
|
|
|
|
}
|
|
|
|
|
|
|
|
Session &Stories::owner() const {
|
|
|
|
return *_owner;
|
|
|
|
}
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
Main::Session &Stories::session() const {
|
|
|
|
return _owner->session();
|
|
|
|
}
|
|
|
|
|
2023-05-03 18:30:37 +00:00
|
|
|
void Stories::apply(const MTPDupdateStories &data) {
|
2023-05-26 14:48:33 +00:00
|
|
|
applyChanges(parse(data.vstories()));
|
2023-05-26 07:21:19 +00:00
|
|
|
_allChanged.fire({});
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
StoriesList Stories::parse(const MTPUserStories &stories) {
|
|
|
|
const auto &data = stories.data();
|
|
|
|
const auto userId = UserId(data.vuser_id());
|
|
|
|
const auto readTill = data.vmax_read_id().value_or_empty();
|
|
|
|
const auto count = int(data.vstories().v.size());
|
|
|
|
auto result = StoriesList{
|
|
|
|
.user = _owner->user(userId),
|
|
|
|
.readTill = readTill,
|
|
|
|
.total = count,
|
|
|
|
};
|
|
|
|
const auto &list = data.vstories().v;
|
2023-05-26 07:21:19 +00:00
|
|
|
result.ids.reserve(list.size());
|
2023-05-03 18:30:37 +00:00
|
|
|
for (const auto &story : list) {
|
|
|
|
story.match([&](const MTPDstoryItem &data) {
|
2023-05-26 15:18:10 +00:00
|
|
|
if (const auto story = parseAndApply(result.user, data)) {
|
2023-05-29 15:09:36 +00:00
|
|
|
result.ids.emplace(story->id());
|
2023-05-03 18:30:37 +00:00
|
|
|
} else {
|
2023-05-26 15:18:10 +00:00
|
|
|
applyDeleted({ peerFromUser(userId), data.vid().v });
|
2023-05-03 18:30:37 +00:00
|
|
|
--result.total;
|
|
|
|
}
|
2023-05-26 07:21:19 +00:00
|
|
|
}, [&](const MTPDstoryItemSkipped &data) {
|
2023-05-29 15:09:36 +00:00
|
|
|
result.ids.emplace(data.vid().v);
|
2023-05-26 07:21:19 +00:00
|
|
|
}, [&](const MTPDstoryItemDeleted &data) {
|
2023-05-26 09:27:34 +00:00
|
|
|
applyDeleted({ peerFromUser(userId), data.vid().v });
|
2023-05-03 18:30:37 +00:00
|
|
|
--result.total;
|
|
|
|
});
|
|
|
|
}
|
2023-05-26 07:21:19 +00:00
|
|
|
result.total = std::max(result.total, int(result.ids.size()));
|
2023-05-03 18:30:37 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-05-26 15:18:10 +00:00
|
|
|
Story *Stories::parseAndApply(
|
|
|
|
not_null<PeerData*> peer,
|
|
|
|
const MTPDstoryItem &data) {
|
|
|
|
const auto media = ParseMedia(_owner, data.vmedia());
|
|
|
|
if (!media) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2023-05-03 18:30:37 +00:00
|
|
|
const auto id = data.vid().v;
|
2023-05-26 07:21:19 +00:00
|
|
|
auto &stories = _stories[peer->id];
|
|
|
|
const auto i = stories.find(id);
|
|
|
|
if (i != end(stories)) {
|
2023-05-26 15:18:10 +00:00
|
|
|
if (i->second->applyChanges(*media, data)) {
|
|
|
|
session().changes().storyUpdated(
|
|
|
|
i->second.get(),
|
|
|
|
UpdateFlag::Edited);
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
2023-05-26 15:18:10 +00:00
|
|
|
return i->second.get();
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
2023-05-26 07:21:19 +00:00
|
|
|
const auto result = stories.emplace(id, std::make_unique<Story>(
|
|
|
|
id,
|
|
|
|
peer,
|
|
|
|
StoryMedia{ *media },
|
|
|
|
data.vdate().v)).first->second.get();
|
2023-05-26 15:18:10 +00:00
|
|
|
result->applyChanges(*media, data);
|
2023-05-26 07:21:19 +00:00
|
|
|
return result;
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
void Stories::updateDependentMessages(not_null<Data::Story*> 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<HistoryItem*> dependent,
|
|
|
|
not_null<Data::Story*> dependency) {
|
|
|
|
_dependentMessages[dependency].emplace(dependent);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stories::unregisterDependentMessage(
|
|
|
|
not_null<HistoryItem*> dependent,
|
|
|
|
not_null<Data::Story*> dependency) {
|
|
|
|
const auto i = _dependentMessages.find(dependency);
|
|
|
|
if (i != end(_dependentMessages)) {
|
|
|
|
if (i->second.remove(dependent) && i->second.empty()) {
|
|
|
|
_dependentMessages.erase(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-03 18:30:37 +00:00
|
|
|
void Stories::loadMore() {
|
|
|
|
if (_loadMoreRequestId || _allLoaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto api = &_owner->session().api();
|
|
|
|
using Flag = MTPstories_GetAllStories::Flag;
|
|
|
|
_loadMoreRequestId = api->request(MTPstories_GetAllStories(
|
2023-05-29 08:06:21 +00:00
|
|
|
MTP_flags(_state.isEmpty()
|
|
|
|
? Flag(0)
|
|
|
|
: (Flag::f_next | Flag::f_state)),
|
2023-05-03 18:30:37 +00:00
|
|
|
MTP_string(_state)
|
|
|
|
)).done([=](const MTPstories_AllStories &result) {
|
|
|
|
_loadMoreRequestId = 0;
|
|
|
|
|
|
|
|
result.match([&](const MTPDstories_allStories &data) {
|
|
|
|
_owner->processUsers(data.vusers());
|
|
|
|
_state = qs(data.vstate());
|
|
|
|
_allLoaded = !data.is_has_more();
|
|
|
|
for (const auto &single : data.vuser_stories().v) {
|
|
|
|
pushToBack(parse(single));
|
|
|
|
}
|
2023-05-26 07:21:19 +00:00
|
|
|
_allChanged.fire({});
|
2023-05-03 18:30:37 +00:00
|
|
|
}, [](const MTPDstories_allStoriesNotModified &) {
|
|
|
|
});
|
|
|
|
}).fail([=] {
|
|
|
|
_loadMoreRequestId = 0;
|
|
|
|
}).send();
|
|
|
|
}
|
|
|
|
|
2023-05-26 09:27:34 +00:00
|
|
|
void Stories::sendResolveRequests() {
|
2023-05-29 12:03:23 +00:00
|
|
|
if (!_resolveSent.empty()) {
|
2023-05-26 09:27:34 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
auto leftToSend = kMaxResolveTogether;
|
2023-05-29 12:03:23 +00:00
|
|
|
auto byPeer = base::flat_map<PeerId, QVector<MTPint>>();
|
|
|
|
for (auto i = begin(_resolvePending); i != end(_resolvePending);) {
|
2023-05-26 09:27:34 +00:00
|
|
|
auto &[peerId, ids] = *i;
|
2023-05-29 12:03:23 +00:00
|
|
|
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;
|
|
|
|
}
|
2023-05-26 09:27:34 +00:00
|
|
|
auto &prepared = byPeer[peerId];
|
2023-05-29 12:03:23 +00:00
|
|
|
for (auto &[storyId, callbacks] : sent) {
|
|
|
|
prepared.push_back(MTP_int(storyId));
|
2023-05-26 09:27:34 +00:00
|
|
|
}
|
2023-05-29 12:03:23 +00:00
|
|
|
if (!leftToSend) {
|
2023-05-26 09:27:34 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const auto api = &_owner->session().api();
|
|
|
|
for (auto &entry : byPeer) {
|
|
|
|
const auto peerId = entry.first;
|
|
|
|
auto &prepared = entry.second;
|
2023-05-29 12:03:23 +00:00
|
|
|
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) {
|
2023-05-26 09:27:34 +00:00
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
2023-05-29 12:03:23 +00:00
|
|
|
_itemsChanged.fire_copy(peerId);
|
|
|
|
if (_resolveSent.empty() && !_resolvePending.empty()) {
|
2023-05-26 09:27:34 +00:00
|
|
|
crl::on_main(&session(), [=] { sendResolveRequests(); });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const auto user = _owner->session().data().peer(peerId)->asUser();
|
|
|
|
if (!user) {
|
2023-05-29 12:03:23 +00:00
|
|
|
finish(peerId);
|
2023-05-26 09:27:34 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const auto requestId = api->request(MTPstories_GetStoriesByID(
|
|
|
|
user->inputUser,
|
2023-05-29 12:03:23 +00:00
|
|
|
MTP_vector<MTPint>(prepared)
|
|
|
|
)).done([=](const MTPstories_Stories &result) {
|
2023-05-26 09:27:34 +00:00
|
|
|
owner().processUsers(result.data().vusers());
|
|
|
|
processResolvedStories(user, result.data().vstories().v);
|
2023-05-29 12:03:23 +00:00
|
|
|
finish(user->id);
|
|
|
|
}).fail([=] {
|
|
|
|
finish(peerId);
|
2023-05-26 09:27:34 +00:00
|
|
|
}).send();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stories::processResolvedStories(
|
|
|
|
not_null<PeerData*> peer,
|
|
|
|
const QVector<MTPStoryItem> &list) {
|
|
|
|
for (const auto &item : list) {
|
|
|
|
item.match([&](const MTPDstoryItem &data) {
|
2023-05-26 15:18:10 +00:00
|
|
|
if (!parseAndApply(peer, data)) {
|
|
|
|
applyDeleted({ peer->id, data.vid().v });
|
|
|
|
}
|
2023-05-26 09:27:34 +00:00
|
|
|
}, [&](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) {
|
|
|
|
const auto i = _stories.find(id.peer);
|
|
|
|
if (i != end(_stories)) {
|
|
|
|
const auto j = i->second.find(id.story);
|
|
|
|
if (j != end(i->second)) {
|
|
|
|
auto story = std::move(j->second);
|
|
|
|
i->second.erase(j);
|
|
|
|
session().changes().storyUpdated(
|
|
|
|
story.get(),
|
|
|
|
UpdateFlag::Destroyed);
|
2023-05-26 14:48:33 +00:00
|
|
|
removeDependencyStory(story.get());
|
2023-05-26 09:27:34 +00:00
|
|
|
if (i->second.empty()) {
|
|
|
|
_stories.erase(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-05-26 14:48:33 +00:00
|
|
|
const auto j = ranges::find(_all, id.peer, [](const StoriesList &list) {
|
|
|
|
return list.user->id;
|
|
|
|
});
|
|
|
|
if (j != end(_all)) {
|
2023-05-29 15:09:36 +00:00
|
|
|
const auto removed = j->ids.remove(id.story);
|
|
|
|
if (removed) {
|
2023-05-26 14:48:33 +00:00
|
|
|
if (j->ids.empty()) {
|
|
|
|
_all.erase(j);
|
2023-05-29 15:09:36 +00:00
|
|
|
} else {
|
|
|
|
Assert(j->total > 0);
|
|
|
|
--j->total;
|
2023-05-26 14:48:33 +00:00
|
|
|
}
|
|
|
|
_allChanged.fire({});
|
|
|
|
}
|
|
|
|
}
|
2023-05-26 09:27:34 +00:00
|
|
|
_deleted.emplace(id);
|
|
|
|
}
|
|
|
|
|
2023-05-26 14:48:33 +00:00
|
|
|
void Stories::removeDependencyStory(not_null<Story*> 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-03 18:30:37 +00:00
|
|
|
const std::vector<StoriesList> &Stories::all() {
|
|
|
|
return _all;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Stories::allLoaded() const {
|
|
|
|
return _allLoaded;
|
|
|
|
}
|
|
|
|
|
2023-05-22 15:59:16 +00:00
|
|
|
rpl::producer<> Stories::allChanged() const {
|
|
|
|
return _allChanged.events();
|
|
|
|
}
|
|
|
|
|
2023-05-29 12:03:23 +00:00
|
|
|
rpl::producer<PeerId> Stories::itemsChanged() const {
|
|
|
|
return _itemsChanged.events();
|
|
|
|
}
|
|
|
|
|
2023-05-26 07:21:19 +00:00
|
|
|
base::expected<not_null<Story*>, 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();
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
2023-05-26 07:21:19 +00:00
|
|
|
}
|
|
|
|
return base::make_unexpected(
|
|
|
|
_deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stories::resolve(FullStoryId id, Fn<void()> done) {
|
2023-05-26 09:27:34 +00:00
|
|
|
const auto already = lookup(id);
|
|
|
|
if (already.has_value() || already.error() != NoStory::Unknown) {
|
2023-05-29 12:03:23 +00:00
|
|
|
if (done) {
|
|
|
|
done();
|
|
|
|
}
|
2023-05-26 09:27:34 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-05-29 12:03:23 +00:00
|
|
|
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];
|
2023-05-26 09:27:34 +00:00
|
|
|
if (ids.empty()) {
|
|
|
|
crl::on_main(&session(), [=] {
|
|
|
|
sendResolveRequests();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
auto &callbacks = ids[id.story];
|
|
|
|
if (done) {
|
|
|
|
callbacks.push_back(std::move(done));
|
|
|
|
}
|
2023-05-03 18:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Stories::pushToBack(StoriesList &&list) {
|
|
|
|
const auto i = ranges::find(_all, list.user, &StoriesList::user);
|
|
|
|
if (i != end(_all)) {
|
2023-05-22 15:59:16 +00:00
|
|
|
if (*i == list) {
|
|
|
|
return;
|
|
|
|
}
|
2023-05-03 18:30:37 +00:00
|
|
|
*i = std::move(list);
|
|
|
|
} else {
|
|
|
|
_all.push_back(std::move(list));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 14:48:33 +00:00
|
|
|
void Stories::applyChanges(StoriesList &&list) {
|
2023-05-03 18:30:37 +00:00
|
|
|
const auto i = ranges::find(_all, list.user, &StoriesList::user);
|
|
|
|
if (i != end(_all)) {
|
2023-05-26 14:48:33 +00:00
|
|
|
auto added = false;
|
|
|
|
for (const auto id : list.ids) {
|
2023-05-29 15:09:36 +00:00
|
|
|
if (!i->ids.contains(id)) {
|
|
|
|
i->ids.emplace(id);
|
2023-05-26 14:48:33 +00:00
|
|
|
++i->total;
|
|
|
|
added = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (added) {
|
|
|
|
ranges::rotate(begin(_all), i, i + 1);
|
|
|
|
}
|
|
|
|
} else if (!list.ids.empty()) {
|
2023-05-03 18:30:37 +00:00
|
|
|
_all.insert(begin(_all), std::move(list));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-29 12:03:23 +00:00
|
|
|
void Stories::loadAround(FullStoryId id) {
|
|
|
|
const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) {
|
|
|
|
return list.user->id;
|
|
|
|
});
|
|
|
|
if (i == end(_all)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto j = ranges::find(i->ids, id.story);
|
|
|
|
if (j == end(i->ids)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto ignore = [&] {
|
|
|
|
const auto side = kIgnorePreloadAroundIfLoaded;
|
2023-05-31 17:53:17 +00:00
|
|
|
const auto left = ranges::min(int(j - begin(i->ids)), side);
|
|
|
|
const auto right = ranges::min(int(end(i->ids) - j), side);
|
2023-05-29 12:03:23 +00:00
|
|
|
for (auto k = j - left; k != j + right; ++k) {
|
|
|
|
const auto maybeStory = lookup({ id.peer, *k });
|
|
|
|
if (!maybeStory && maybeStory.error() == NoStory::Unknown) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}();
|
|
|
|
if (ignore) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto side = kPreloadAroundCount;
|
2023-05-31 17:53:17 +00:00
|
|
|
const auto left = ranges::min(int(j - begin(i->ids)), side);
|
|
|
|
const auto right = ranges::min(int(end(i->ids) - j), side);
|
2023-05-29 12:03:23 +00:00
|
|
|
const auto from = j - left;
|
|
|
|
const auto till = j + right;
|
|
|
|
for (auto k = from; k != till; ++k) {
|
|
|
|
resolve({ id.peer, *k }, nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-30 13:57:11 +00:00
|
|
|
void Stories::markAsRead(FullStoryId id, bool viewed) {
|
2023-05-29 15:09:36 +00:00
|
|
|
const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) {
|
|
|
|
return list.user->id;
|
|
|
|
});
|
|
|
|
Assert(i != end(_all));
|
|
|
|
if (i->readTill >= id.story) {
|
|
|
|
return;
|
|
|
|
} else if (!_markReadPending.contains(id.peer)) {
|
|
|
|
sendMarkAsReadRequests();
|
|
|
|
}
|
|
|
|
_markReadPending.emplace(id.peer);
|
|
|
|
i->readTill = id.story;
|
|
|
|
_markReadTimer.callOnce(kMarkAsReadDelay);
|
|
|
|
_allChanged.fire({});
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stories::sendMarkAsReadRequest(
|
|
|
|
not_null<PeerData*> 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();
|
|
|
|
}
|
|
|
|
if (_markReadRequests.empty()) {
|
|
|
|
if (Core::Quitting()) {
|
|
|
|
LOG(("Stories doesn't prevent quit any more."));
|
|
|
|
}
|
|
|
|
Core::App().quitPreventFinished();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const auto api = &_owner->session().api();
|
|
|
|
api->request(MTPstories_ReadStories(
|
|
|
|
peer->asUser()->inputUser,
|
|
|
|
MTP_int(tillId)
|
|
|
|
)).done(finish).fail(finish).send();
|
|
|
|
}
|
|
|
|
|
|
|
|
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 = ranges::find(_all, peerId, [](
|
|
|
|
const StoriesList &list) {
|
|
|
|
return list.user->id;
|
|
|
|
});
|
|
|
|
if (j != end(_all)) {
|
|
|
|
sendMarkAsReadRequest(j->user, j->readTill);
|
|
|
|
}
|
|
|
|
i = _markReadPending.erase(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-01 16:01:29 +00:00
|
|
|
void Stories::loadViewsSlice(
|
|
|
|
StoryId id,
|
|
|
|
std::optional<StoryView> offset,
|
|
|
|
Fn<void(std::vector<StoryView>)> 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(2)
|
|
|
|
)).done([=](const MTPstories_StoryViewsList &result) {
|
|
|
|
_viewsRequestId = 0;
|
|
|
|
|
|
|
|
auto slice = std::vector<StoryView>();
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-05-29 15:09:36 +00:00
|
|
|
bool Stories::isQuitPrevent() {
|
|
|
|
if (!_markReadPending.empty()) {
|
|
|
|
sendMarkAsReadRequests();
|
|
|
|
}
|
|
|
|
if (_markReadRequests.empty()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
LOG(("Stories prevents quit, marking as read..."));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-05-03 18:30:37 +00:00
|
|
|
} // namespace Data
|