mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-15 21:57:10 +02:00
Preload next stories inside the media viewer.
This commit is contained in:
parent
d567282430
commit
010c666d23
7 changed files with 749 additions and 12 deletions
|
@ -637,7 +637,7 @@ void Stories::applyDeleted(FullStoryId id) {
|
|||
}
|
||||
}
|
||||
if (_preloading && _preloading->id() == id) {
|
||||
preloadFinished();
|
||||
preloadFinished(id);
|
||||
}
|
||||
if (i->second.empty()) {
|
||||
_stories.erase(i);
|
||||
|
@ -1294,6 +1294,24 @@ bool Stories::isQuitPrevent() {
|
|||
return true;
|
||||
}
|
||||
|
||||
void Stories::incrementPreloadingMainSources() {
|
||||
Expects(_preloadingMainSourcesCounter >= 0);
|
||||
|
||||
if (++_preloadingMainSourcesCounter == 1
|
||||
&& rebuildPreloadSources(StorySourcesList::NotHidden)) {
|
||||
continuePreloading();
|
||||
}
|
||||
}
|
||||
|
||||
void Stories::decrementPreloadingMainSources() {
|
||||
Expects(_preloadingMainSourcesCounter > 0);
|
||||
|
||||
if (!--_preloadingMainSourcesCounter
|
||||
&& rebuildPreloadSources(StorySourcesList::NotHidden)) {
|
||||
continuePreloading();
|
||||
}
|
||||
}
|
||||
|
||||
void Stories::incrementPreloadingHiddenSources() {
|
||||
Expects(_preloadingHiddenSourcesCounter >= 0);
|
||||
|
||||
|
@ -1330,8 +1348,10 @@ void Stories::preloadSourcesChanged(StorySourcesList list) {
|
|||
|
||||
bool Stories::rebuildPreloadSources(StorySourcesList list) {
|
||||
const auto index = static_cast<int>(list);
|
||||
if (!_preloadingHiddenSourcesCounter
|
||||
&& list == StorySourcesList::Hidden) {
|
||||
const auto &counter = (list == StorySourcesList::Hidden)
|
||||
? _preloadingHiddenSourcesCounter
|
||||
: _preloadingMainSourcesCounter;
|
||||
if (!counter) {
|
||||
return !base::take(_toPreloadSources[index]).empty();
|
||||
}
|
||||
auto now = std::vector<FullStoryId>();
|
||||
|
@ -1401,15 +1421,16 @@ void Stories::startPreloading(not_null<Story*> story) {
|
|||
Expects(!_preloaded.contains(story->fullId()));
|
||||
|
||||
const auto id = story->fullId();
|
||||
_preloading = std::make_unique<StoryPreload>(story, [=] {
|
||||
preloadFinished(true);
|
||||
auto preloading = std::make_unique<StoryPreload>(story, [=] {
|
||||
_preloading = nullptr;
|
||||
preloadFinished(id, true);
|
||||
});
|
||||
if (!_preloaded.contains(id)) {
|
||||
_preloading = std::move(preloading);
|
||||
}
|
||||
}
|
||||
|
||||
void Stories::preloadFinished(bool markAsPreloaded) {
|
||||
Expects(_preloading != nullptr);
|
||||
|
||||
const auto id = base::take(_preloading)->id();
|
||||
void Stories::preloadFinished(FullStoryId id, bool markAsPreloaded) {
|
||||
for (auto &sources : _toPreloadSources) {
|
||||
sources.erase(ranges::remove(sources, id), end(sources));
|
||||
}
|
||||
|
|
|
@ -190,6 +190,8 @@ public:
|
|||
Ui::ReportReason reason,
|
||||
QString text);
|
||||
|
||||
void incrementPreloadingMainSources();
|
||||
void decrementPreloadingMainSources();
|
||||
void incrementPreloadingHiddenSources();
|
||||
void decrementPreloadingHiddenSources();
|
||||
void setPreloadingInViewer(std::vector<FullStoryId> ids);
|
||||
|
@ -245,7 +247,7 @@ private:
|
|||
[[nodiscard]] bool shouldContinuePreload(FullStoryId id) const;
|
||||
[[nodiscard]] FullStoryId nextPreloadId() const;
|
||||
void startPreloading(not_null<Story*> story);
|
||||
void preloadFinished(bool markAsPreloaded = false);
|
||||
void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
std::unordered_map<
|
||||
|
@ -310,6 +312,7 @@ private:
|
|||
std::vector<FullStoryId> _toPreloadViewer;
|
||||
std::unique_ptr<StoryPreload> _preloading;
|
||||
int _preloadingHiddenSourcesCounter = 0;
|
||||
int _preloadingMainSourcesCounter = 0;
|
||||
|
||||
};
|
||||
|
||||
|
|
515
Telegram/SourceFiles/data/data_story.cpp
Normal file
515
Telegram/SourceFiles/data/data_story.cpp
Normal file
|
@ -0,0 +1,515 @@
|
|||
/*
|
||||
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_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_thread.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 "ui/text/text_utilities.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask {
|
||||
public:
|
||||
LoadTask(
|
||||
FullStoryId id,
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(QByteArray)> 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<uint32, QByteArray> _parts;
|
||||
Fn<void(QByteArray)> _done;
|
||||
base::flat_set<int> _requestedOffsets;
|
||||
int64 _full = 0;
|
||||
int _nextRequestOffset = 0;
|
||||
bool _finished = false;
|
||||
bool _failed = false;
|
||||
|
||||
};
|
||||
|
||||
StoryPreload::LoadTask::LoadTask(
|
||||
FullStoryId id,
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(QByteArray)> done)
|
||||
: DownloadMtprotoTask(
|
||||
&document->session().downloader(),
|
||||
document->videoPreloadLocation(),
|
||||
FileOriginStory(id.peer, id.story))
|
||||
, _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;
|
||||
const auto index = offset / part;
|
||||
_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<PeerData*> 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<PeerData*> 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<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;
|
||||
}
|
||||
|
||||
bool Story::hasReplyPreview() const {
|
||||
return v::match(_media.data, [](not_null<PhotoData*> photo) {
|
||||
return !photo->isNull();
|
||||
}, [](not_null<DocumentData*> document) {
|
||||
return document->hasThumbnail();
|
||||
}, [](v::null_t) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}, [](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::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::canDownload() const {
|
||||
return _peer->isSelf();
|
||||
}
|
||||
|
||||
bool Story::canShare() const {
|
||||
return isPublic() && (pinned() || !expired());
|
||||
}
|
||||
|
||||
bool Story::canDelete() const {
|
||||
return _peer->isSelf();
|
||||
}
|
||||
|
||||
bool Story::canReport() const {
|
||||
return !_peer->isSelf();
|
||||
}
|
||||
|
||||
bool Story::hasDirectLink() const {
|
||||
if (!_isPublic || (!_pinned && expired())) {
|
||||
return false;
|
||||
}
|
||||
const auto user = _peer->asUser();
|
||||
return user && !user->username().isEmpty();
|
||||
}
|
||||
|
||||
std::optional<QString> Story::errorTextForForward(
|
||||
not_null<Thread*> to) const {
|
||||
const auto peer = to->peer();
|
||||
const auto holdsPhoto = v::is<not_null<PhotoData*>>(_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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const std::vector<StoryView> &Story::viewsList() const {
|
||||
return _viewsList;
|
||||
}
|
||||
|
||||
int Story::views() const {
|
||||
return _views;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = -1;
|
||||
auto recent = std::vector<not_null<PeerData*>>();
|
||||
if (!data.is_min()) {
|
||||
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 >= 0 && _views != views)
|
||||
|| (_recentViewers != recent);
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
_media = std::move(media);
|
||||
_pinned = pinned;
|
||||
_isPublic = isPublic;
|
||||
_closeFriends = closeFriends;
|
||||
_caption = std::move(caption);
|
||||
if (views >= 0) {
|
||||
_views = views;
|
||||
}
|
||||
_recentViewers = std::move(recent);
|
||||
return true;
|
||||
}
|
||||
|
||||
StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> 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<Story*> StoryPreload::story() const {
|
||||
return _story;
|
||||
}
|
||||
|
||||
void StoryPreload::start() {
|
||||
const auto origin = FileOriginStory(
|
||||
_story->peer()->id,
|
||||
_story->id());
|
||||
if (const auto photo = _story->photo()) {
|
||||
_photo = photo->createMediaView();
|
||||
if (_photo->loaded()) {
|
||||
callDone();
|
||||
} else {
|
||||
_photo->automaticLoad(origin, _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<LoadTask>(id(), video, [=](QByteArray data) {
|
||||
if (!data.isEmpty()) {
|
||||
_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
|
158
Telegram/SourceFiles/data/data_story.h
Normal file
158
Telegram/SourceFiles/data/data_story.h
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class Image;
|
||||
class PhotoData;
|
||||
class DocumentData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
class Thread;
|
||||
class PhotoMedia;
|
||||
|
||||
struct StoryIdDates {
|
||||
StoryId id = 0;
|
||||
TimeId date = 0;
|
||||
TimeId expires = 0;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
return id != 0;
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default;
|
||||
friend inline bool operator==(StoryIdDates, StoryIdDates) = default;
|
||||
};
|
||||
|
||||
struct StoryMedia {
|
||||
std::variant<
|
||||
v::null_t,
|
||||
not_null<PhotoData*>,
|
||||
not_null<DocumentData*>> data;
|
||||
|
||||
friend inline bool operator==(StoryMedia, StoryMedia) = default;
|
||||
};
|
||||
|
||||
struct StoryView {
|
||||
not_null<PeerData*> peer;
|
||||
TimeId date = 0;
|
||||
|
||||
friend inline bool operator==(StoryView, StoryView) = default;
|
||||
};
|
||||
|
||||
class Story final {
|
||||
public:
|
||||
Story(
|
||||
StoryId id,
|
||||
not_null<PeerData*> peer,
|
||||
StoryMedia media,
|
||||
TimeId date,
|
||||
TimeId expires);
|
||||
|
||||
[[nodiscard]] Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
|
||||
[[nodiscard]] StoryId id() const;
|
||||
[[nodiscard]] bool mine() const;
|
||||
[[nodiscard]] StoryIdDates idDates() const;
|
||||
[[nodiscard]] FullStoryId fullId() const;
|
||||
[[nodiscard]] TimeId date() const;
|
||||
[[nodiscard]] TimeId expires() const;
|
||||
[[nodiscard]] bool unsupported() const;
|
||||
[[nodiscard]] bool expired(TimeId now = 0) const;
|
||||
[[nodiscard]] const StoryMedia &media() const;
|
||||
[[nodiscard]] PhotoData *photo() const;
|
||||
[[nodiscard]] DocumentData *document() const;
|
||||
|
||||
[[nodiscard]] bool hasReplyPreview() const;
|
||||
[[nodiscard]] Image *replyPreview() const;
|
||||
[[nodiscard]] TextWithEntities inReplyText() const;
|
||||
|
||||
void setPinned(bool pinned);
|
||||
[[nodiscard]] bool pinned() const;
|
||||
void setIsPublic(bool isPublic);
|
||||
[[nodiscard]] bool isPublic() const;
|
||||
void setCloseFriends(bool closeFriends);
|
||||
[[nodiscard]] bool closeFriends() const;
|
||||
|
||||
[[nodiscard]] bool canDownload() const;
|
||||
[[nodiscard]] bool canShare() const;
|
||||
[[nodiscard]] bool canDelete() const;
|
||||
[[nodiscard]] bool canReport() const;
|
||||
|
||||
[[nodiscard]] bool hasDirectLink() const;
|
||||
[[nodiscard]] std::optional<QString> errorTextForForward(
|
||||
not_null<Thread*> to) const;
|
||||
|
||||
void setCaption(TextWithEntities &&caption);
|
||||
[[nodiscard]] const TextWithEntities &caption() const;
|
||||
|
||||
void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
|
||||
[[nodiscard]] auto recentViewers() const
|
||||
-> const std::vector<not_null<PeerData*>> &;
|
||||
[[nodiscard]] const std::vector<StoryView> &viewsList() const;
|
||||
[[nodiscard]] int views() const;
|
||||
void applyViewsSlice(
|
||||
const std::optional<StoryView> &offset,
|
||||
const std::vector<StoryView> &slice,
|
||||
int total);
|
||||
|
||||
bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
|
||||
|
||||
private:
|
||||
const StoryId _id = 0;
|
||||
const not_null<PeerData*> _peer;
|
||||
StoryMedia _media;
|
||||
TextWithEntities _caption;
|
||||
std::vector<not_null<PeerData*>> _recentViewers;
|
||||
std::vector<StoryView> _viewsList;
|
||||
int _views = 0;
|
||||
const TimeId _date = 0;
|
||||
const TimeId _expires = 0;
|
||||
bool _pinned : 1 = false;
|
||||
bool _isPublic : 1 = false;
|
||||
bool _closeFriends : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
class StoryPreload final : public base::has_weak_ptr {
|
||||
public:
|
||||
StoryPreload(not_null<Story*> story, Fn<void()> done);
|
||||
~StoryPreload();
|
||||
|
||||
[[nodiscard]] FullStoryId id() const;
|
||||
[[nodiscard]] not_null<Story*> story() const;
|
||||
|
||||
private:
|
||||
class LoadTask;
|
||||
|
||||
void start();
|
||||
void load();
|
||||
void callDone();
|
||||
|
||||
const not_null<Story*> _story;
|
||||
Fn<void()> _done;
|
||||
|
||||
std::shared_ptr<Data::PhotoMedia> _photo;
|
||||
std::unique_ptr<LoadTask> _task;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
|
@ -366,6 +366,8 @@ InnerWidget::InnerWidget(
|
|||
Data::StorySourcesList::NotHidden);
|
||||
}, lifetime());
|
||||
|
||||
session().data().stories().incrementPreloadingMainSources();
|
||||
|
||||
handleChatListEntryRefreshes();
|
||||
|
||||
refreshWithCollapsedRows(true);
|
||||
|
@ -2426,6 +2428,7 @@ void InnerWidget::appendToFiltered(Key key) {
|
|||
}
|
||||
|
||||
InnerWidget::~InnerWidget() {
|
||||
session().data().stories().decrementPreloadingMainSources();
|
||||
clearSearchResults();
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ constexpr auto kSiblingUserpicSize = 0.3;
|
|||
constexpr auto kInnerHeightMultiplier = 1.6;
|
||||
constexpr auto kPreloadUsersCount = 3;
|
||||
constexpr auto kPreloadStoriesCount = 5;
|
||||
constexpr auto kPreloadNextMediaCount = 3;
|
||||
constexpr auto kPreloadPreviousMediaCount = 1;
|
||||
constexpr auto kMarkAsReadAfterSeconds = 1;
|
||||
constexpr auto kMarkAsReadAfterProgress = 0.2;
|
||||
|
||||
|
@ -637,9 +639,28 @@ void Controller::rebuildFromContext(
|
|||
}
|
||||
}
|
||||
}
|
||||
preloadNext();
|
||||
_slider->show({ .index = _index, .total = shownCount() });
|
||||
}
|
||||
|
||||
void Controller::preloadNext() {
|
||||
Expects(shown());
|
||||
|
||||
auto ids = std::vector<FullStoryId>();
|
||||
ids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);
|
||||
const auto user = shownUser();
|
||||
const auto count = shownCount();
|
||||
const auto till = std::min(_index + kPreloadNextMediaCount, count);
|
||||
for (auto i = _index + 1; i != till; ++i) {
|
||||
ids.push_back({ .peer = user->id, .story = shownId(i) });
|
||||
}
|
||||
const auto from = std::max(_index - kPreloadPreviousMediaCount, 0);
|
||||
for (auto i = _index; i != from;) {
|
||||
ids.push_back({ .peer = user->id, .story = shownId(--i) });
|
||||
}
|
||||
user->owner().stories().setPreloadingInViewer(std::move(ids));
|
||||
}
|
||||
|
||||
void Controller::checkMoveByDelta() {
|
||||
const auto index = _index + _waitingForDelta;
|
||||
if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
|
||||
|
@ -659,7 +680,17 @@ void Controller::show(
|
|||
|
||||
rebuildFromContext(user, storyId);
|
||||
_contextLifetime.destroy();
|
||||
v::match(_context.data, [&](Data::StoriesContextSaved) {
|
||||
const auto subscribeToSource = [&] {
|
||||
stories.sourceChanged() | rpl::filter(
|
||||
rpl::mappers::_1 == storyId.peer
|
||||
) | rpl::start_with_next([=] {
|
||||
rebuildFromContext(user, storyId);
|
||||
}, _contextLifetime);
|
||||
};
|
||||
v::match(_context.data, [&](Data::StoriesContextSingle) {
|
||||
}, [&](Data::StoriesContextPeer) {
|
||||
subscribeToSource();
|
||||
}, [&](Data::StoriesContextSaved) {
|
||||
stories.savedChanged() | rpl::filter(
|
||||
rpl::mappers::_1 == storyId.peer
|
||||
) | rpl::start_with_next([=] {
|
||||
|
@ -672,7 +703,9 @@ void Controller::show(
|
|||
rebuildFromContext(user, storyId);
|
||||
checkMoveByDelta();
|
||||
}, _contextLifetime);
|
||||
}, [](const auto &) {});
|
||||
}, [&](Data::StorySourcesList) {
|
||||
subscribeToSource();
|
||||
});
|
||||
|
||||
const auto guard = gsl::finally([&] {
|
||||
_paused = false;
|
||||
|
@ -733,6 +766,9 @@ void Controller::show(
|
|||
checkWaitingFor();
|
||||
}
|
||||
}, _sessionLifetime);
|
||||
_sessionLifetime.add([=] {
|
||||
session->data().stories().setPreloadingInViewer({});
|
||||
});
|
||||
}
|
||||
|
||||
stories.loadAround(storyId, context);
|
||||
|
|
|
@ -183,6 +183,7 @@ private:
|
|||
void rebuildFromContext(not_null<UserData*> user, FullStoryId storyId);
|
||||
void checkMoveByDelta();
|
||||
void loadMoreToList();
|
||||
void preloadNext();
|
||||
void rebuildCachedSourcesList(
|
||||
const std::vector<Data::StoriesSourceInfo> &lists,
|
||||
int index);
|
||||
|
|
Loading…
Add table
Reference in a new issue