Add short-polling of stories.

This commit is contained in:
John Preston 2023-07-05 19:52:22 +04:00
parent 12fe0a836a
commit 5ccb97668c
14 changed files with 308 additions and 52 deletions

View File

@ -38,6 +38,8 @@ constexpr auto kSavedPerPage = 100;
constexpr auto kMaxPreloadSources = 10;
constexpr auto kStillPreloadFromFirst = 3;
constexpr auto kMaxSegmentsCount = 180;
constexpr auto kPollingIntervalChat = 5 * TimeId(60);
constexpr auto kPollingIntervalViewer = 1 * TimeId(60);
using UpdateFlag = StoryUpdate::Flag;
@ -98,10 +100,12 @@ Stories::Stories(not_null<Session*> owner)
: _owner(owner)
, _expireTimer([=] { processExpired(); })
, _markReadTimer([=] { sendMarkAsReadRequests(); })
, _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) {
, _incrementViewsTimer([=] { sendIncrementViewsRequests(); })
, _pollingTimer([=] { sendPollingRequests(); }) {
}
Stories::~Stories() {
Expects(_pollingSettings.empty());
}
Session &Stories::owner() const {
@ -348,7 +352,7 @@ Story *Stories::parseAndApply(
const auto result = i->second.get();
const auto pinned = result->pinned();
const auto mediaChanged = (result->media() != *media);
if (result->applyChanges(*media, data)) {
if (result->applyChanges(*media, data, now)) {
if (result->pinned() != pinned) {
savedStateUpdated(result);
}
@ -358,6 +362,11 @@ Story *Stories::parseAndApply(
if (const auto item = lookupItem(result)) {
item->applyChanges(result);
}
_owner->refreshStoryItemViews(fullId);
}
const auto j = _pollingSettings.find(result);
if (j != end(_pollingSettings)) {
maybeSchedulePolling(result, j->second, now);
}
if (mediaChanged) {
_preloaded.remove(fullId);
@ -378,7 +387,7 @@ Story *Stories::parseAndApply(
StoryMedia{ *media },
data.vdate().v,
data.vexpire_date().v)).first->second.get();
result->applyChanges(*media, data);
result->applyChanges(*media, data, now);
if (result->pinned()) {
savedStateUpdated(result);
}
@ -656,6 +665,7 @@ void Stories::applyDeleted(FullStoryId id) {
preloadFinished(id);
}
_owner->refreshStoryItemViews(id);
Assert(!_pollingSettings.contains(story.get()));
if (i->second.empty()) {
_stories.erase(i);
}
@ -818,13 +828,15 @@ base::expected<not_null<Story*>, NoStory> Stories::lookup(
_deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown);
}
void Stories::resolve(FullStoryId id, Fn<void()> done) {
const auto already = lookup(id);
if (already.has_value() || already.error() != NoStory::Unknown) {
if (done) {
done();
void Stories::resolve(FullStoryId id, Fn<void()> done, bool force) {
if (!force) {
const auto already = lookup(id);
if (already.has_value() || already.error() != NoStory::Unknown) {
if (done) {
done();
}
return;
}
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)) {
@ -1493,6 +1505,84 @@ bool Stories::isUnread(not_null<Story*> story) {
return (story->id() > readTill);
}
void Stories::registerPolling(not_null<Story*> story, Polling polling) {
auto &settings = _pollingSettings[story];
switch (polling) {
case Polling::Chat: ++settings.chat; break;
case Polling::Viewer: ++settings.viewer; break;
}
maybeSchedulePolling(story, settings, base::unixtime::now());
}
void Stories::unregisterPolling(not_null<Story*> story, Polling polling) {
const auto i = _pollingSettings.find(story);
Assert(i != end(_pollingSettings));
switch (polling) {
case Polling::Chat:
Assert(i->second.chat > 0);
--i->second.chat;
break;
case Polling::Viewer:
Assert(i->second.viewer > 0);
--i->second.viewer;
break;
}
if (!i->second.chat && !i->second.viewer) {
_pollingSettings.erase(i);
}
}
bool Stories::registerPolling(FullStoryId id, Polling polling) {
if (const auto maybeStory = lookup(id)) {
registerPolling(*maybeStory, polling);
return true;
}
return false;
}
void Stories::unregisterPolling(FullStoryId id, Polling polling) {
const auto maybeStory = lookup(id);
Assert(maybeStory.has_value());
unregisterPolling(*maybeStory, polling);
}
int Stories::pollingInterval(const PollingSettings &settings) const {
return settings.viewer ? kPollingIntervalViewer : kPollingIntervalChat;
}
void Stories::maybeSchedulePolling(
not_null<Story*> story,
const PollingSettings &settings,
TimeId now) {
const auto last = story->lastUpdateTime();
const auto next = last + pollingInterval(settings);
const auto left = std::max(next - now, 0) * crl::time(1000) + 1;
if (!_pollingTimer.isActive() || _pollingTimer.remainingTime() > left) {
_pollingTimer.callOnce(left);
}
}
void Stories::sendPollingRequests() {
auto min = 0;
const auto now = base::unixtime::now();
for (const auto &[story, settings] : _pollingSettings) {
const auto last = story->lastUpdateTime();
const auto next = last + pollingInterval(settings);
if (now >= next) {
resolve(story->fullId(), nullptr, true);
} else {
const auto left = (next - now) * crl::time(1000) + 1;
if (!min || left < min) {
min = left;
}
}
}
if (min > 0) {
_pollingTimer.callOnce(min);
}
}
void Stories::updateUserStoriesState(not_null<PeerData*> peer) {
const auto till = _readTill.find(peer->id);
const auto readTill = (till != end(_readTill)) ? till->second : 0;

View File

@ -153,7 +153,7 @@ public:
[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
FullStoryId id) const;
void resolve(FullStoryId id, Fn<void()> done);
void resolve(FullStoryId id, Fn<void()> done, bool force = false);
[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(FullStoryId id);
[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(
not_null<Story*> story);
@ -209,6 +209,16 @@ public:
StoryId storyMaxId);
[[nodiscard]] bool isUnread(not_null<Story*> story);
enum class Polling {
Chat,
Viewer,
};
void registerPolling(not_null<Story*> story, Polling polling);
void unregisterPolling(not_null<Story*> story, Polling polling);
bool registerPolling(FullStoryId id, Polling polling);
void unregisterPolling(FullStoryId id, Polling polling);
private:
struct Saved {
StoriesIds ids;
@ -217,6 +227,10 @@ private:
bool loaded = false;
mtpRequestId requestId = 0;
};
struct PollingSettings {
int chat = 0;
int viewer = 0;
};
void parseAndApply(const MTPUserStories &stories);
[[nodiscard]] Story *parseAndApply(
@ -265,6 +279,14 @@ private:
void startPreloading(not_null<Story*> story);
void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
[[nodiscard]] int pollingInterval(
const PollingSettings &settings) const;
void maybeSchedulePolling(
not_null<Story*> story,
const PollingSettings &settings,
TimeId now);
void sendPollingRequests();
const not_null<Session*> _owner;
std::unordered_map<
PeerId,
@ -336,6 +358,9 @@ private:
mtpRequestId _readTillsRequestId = 0;
bool _readTillReceived = false;
base::flat_map<not_null<Story*>, PollingSettings> _pollingSettings;
base::Timer _pollingTimer;
};
} // namespace Data

View File

@ -371,7 +371,12 @@ void Story::applyViewsSlice(
}
}
bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
bool Story::applyChanges(
StoryMedia media,
const MTPDstoryItem &data,
TimeId now) {
_lastUpdateTime = now;
const auto pinned = data.is_pinned();
const auto edited = data.is_edited();
const auto isPublic = data.is_public();
@ -424,6 +429,10 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
return true;
}
TimeId Story::lastUpdateTime() const {
return _lastUpdateTime;
}
StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)
: _story(story)
, _done(std::move(done)) {

View File

@ -113,7 +113,11 @@ public:
const std::vector<StoryView> &slice,
int total);
bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
bool applyChanges(
StoryMedia media,
const MTPDstoryItem &data,
TimeId now);
[[nodiscard]] TimeId lastUpdateTime() const;
private:
const StoryId _id = 0;
@ -125,6 +129,7 @@ private:
int _views = 0;
const TimeId _date = 0;
const TimeId _expires = 0;
TimeId _lastUpdateTime = 0;
bool _pinned : 1 = false;
bool _isPublic : 1 = false;
bool _closeFriends : 1 = false;

View File

@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/spoiler_mess.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_streaming.h"
#include "data/data_document.h"
#include "data/data_file_click_handler.h"
@ -134,6 +135,7 @@ Gif::~Gif() {
_parent->checkHeavyPart();
}
}
togglePollingStory(false);
}
bool Gif::CanPlayInline(not_null<DocumentData*> document) {
@ -1432,6 +1434,22 @@ void Gif::dataMediaCreated() const {
_dataMedia->videoThumbnailWanted(_realParent->fullId());
}
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
}
void Gif::togglePollingStory(bool enabled) const {
if (!_story || _pollingStory == enabled) {
return;
}
const auto polling = Data::Stories::Polling::Chat;
const auto media = _parent->data()->media();
const auto id = media ? media->storyId() : FullStoryId();
if (!enabled) {
_data->owner().stories().unregisterPolling(id, polling);
} else if (!_data->owner().stories().registerPolling(id, polling)) {
return;
}
_pollingStory = enabled;
}
bool Gif::uploading() const {
@ -1686,6 +1704,7 @@ void Gif::unloadHeavyPart() {
_thumbCache = QImage();
_videoThumbnailFrame = nullptr;
_caption.unloadPersistentAnimation();
togglePollingStory(false);
}
void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
@ -1815,6 +1834,7 @@ void Gif::setStreamed(std::unique_ptr<Streamed> value) {
_streamed = std::move(value);
if (set) {
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
} else if (removed) {
_parent->checkHeavyPart();
}

View File

@ -208,6 +208,8 @@ private:
StateRequest request,
QPoint position) const;
void togglePollingStory(bool enabled) const;
const not_null<DocumentData*> _data;
Ui::Text::String _caption;
std::unique_ptr<Streamed> _streamed;
@ -222,6 +224,7 @@ private:
mutable bool _thumbCacheBlurred : 1 = false;
mutable bool _thumbIsEllipse : 1 = false;
mutable bool _story : 1 = false;
mutable bool _pollingStory : 1 = false;
};

View File

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_streaming.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
@ -101,6 +102,7 @@ Photo::~Photo() {
_parent->checkHeavyPart();
}
}
togglePollingStory(false);
}
void Photo::create(FullMsgId contextId, PeerData *chat) {
@ -145,6 +147,7 @@ void Photo::dataMediaCreated() const {
_dataMedia->wanted(PhotoSize::Small, _realParent->fullId());
}
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
}
bool Photo::hasHeavyPart() const {
@ -160,6 +163,23 @@ void Photo::unloadHeavyPart() {
}
_imageCache = QImage();
_caption.unloadPersistentAnimation();
togglePollingStory(false);
}
void Photo::togglePollingStory(bool enabled) const {
const auto pollingStory = (enabled ? 1 : 0);
if (!_story || _pollingStory == pollingStory) {
return;
}
const auto polling = Data::Stories::Polling::Chat;
const auto media = _parent->data()->media();
const auto id = media ? media->storyId() : FullStoryId();
if (!enabled) {
_data->owner().stories().unregisterPolling(id, polling);
} else if (!_data->owner().stories().registerPolling(id, polling)) {
return;
}
_pollingStory = pollingStory;
}
QSize Photo::countOptimalSize() {
@ -926,6 +946,7 @@ void Photo::setStreamed(std::unique_ptr<Streamed> value) {
_streamed = std::move(value);
if (set) {
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
} else if (removed) {
_parent->checkHeavyPart();
}

View File

@ -160,6 +160,8 @@ private:
[[nodiscard]] QSize photoSize() const;
void togglePollingStory(bool enabled) const;
const not_null<PhotoData*> _data;
Ui::Text::String _caption;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
@ -167,10 +169,11 @@ private:
const std::unique_ptr<MediaSpoiler> _spoiler;
mutable QImage _imageCache;
mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
uint32 _serviceWidth : 29 = 0;
uint32 _serviceWidth : 28 = 0;
mutable uint32 _imageCacheForum : 1 = 0;
mutable uint32 _imageCacheBlurred : 1 = 0;
mutable uint32 _story : 1 = 0;
mutable uint32 _pollingStory : 1 = 0;
};

View File

@ -52,7 +52,9 @@ StoryMention::StoryMention(
, _unread(story->owner().stories().isUnread(story) ? 1 : 0) {
}
StoryMention::~StoryMention() = default;
StoryMention::~StoryMention() {
changeSubscribedTo(0);
}
int StoryMention::top() {
return st::msgServiceGiftBoxButtonMargins.top();
@ -105,13 +107,12 @@ void StoryMention::draw(
? history->session().user()
: history->peer);
_thumbnailFromStory = showStory;
_subscribed = 0;
changeSubscribedTo(0);
}
if (!_subscribed) {
if (changeSubscribedTo(1)) {
_thumbnail->subscribeToUpdates([=] {
_parent->data()->history()->owner().requestViewRepaint(_parent);
});
_subscribed = 1;
}
const auto padding = (geometry.width() - st::storyMentionSize) / 2;
@ -164,10 +165,26 @@ bool StoryMention::hasHeavyPart() {
}
void StoryMention::unloadHeavyPart() {
if (_subscribed) {
_subscribed = 0;
if (changeSubscribedTo(0)) {
_thumbnail->subscribeToUpdates(nullptr);
}
}
bool StoryMention::changeSubscribedTo(uint32 value) {
Expects(value == 0 || value == 1);
if (_subscribed == value) {
return false;
}
_subscribed = value;
const auto stories = &_parent->history()->owner().stories();
if (value) {
_parent->history()->owner().registerHeavyViewPart(_parent);
stories->registerPolling(_story, Data::Stories::Polling::Chat);
} else {
stories->unregisterPolling(_story, Data::Stories::Polling::Chat);
}
return true;
}
} // namespace HistoryView

View File

@ -55,6 +55,8 @@ public:
private:
using Thumbnail = Dialogs::Stories::Thumbnail;
bool changeSubscribedTo(uint32 value);
const not_null<Element*> _parent;
const not_null<Data::Story*> _story;
std::shared_ptr<Thumbnail> _thumbnail;

View File

@ -56,6 +56,10 @@ Provider::Provider(not_null<AbstractController*> controller)
}, _lifetime);
}
Provider::~Provider() {
clear();
}
Type Provider::type() {
return Type::PhotoVideo;
}
@ -90,11 +94,20 @@ std::optional<int> Provider::fullCount() {
return _slice.fullCount();
}
void Provider::restart() {
void Provider::clear() {
for (const auto &[storyId, _] : _layouts) {
_peer->owner().stories().unregisterPolling(
{ _peer->id, storyId },
Data::Stories::Polling::Chat);
}
_layouts.clear();
_aroundId = kDefaultAroundId;
_idsLimit = kMinimalIdsLimit;
_slice = Data::StoriesIdsSlice();
}
void Provider::restart() {
clear();
refreshViewer();
}
@ -210,6 +223,9 @@ void Provider::markLayoutsStale() {
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_peer->owner().stories().unregisterPolling(
{ _peer->id, i->first },
Data::Stories::Polling::Chat);
_layoutRemoved.fire(i->second.item.get());
const auto taken = _items.take(i->first);
i = _layouts.erase(i);
@ -240,6 +256,9 @@ bool Provider::isAfter(
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
const auto id = StoryIdFromMsgId(item->id);
if (const auto i = _layouts.find(id); i != end(_layouts)) {
_peer->owner().stories().unregisterPolling(
{ _peer->id, id },
Data::Stories::Polling::Chat);
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
@ -253,6 +272,9 @@ BaseLayout *Provider::getLayout(
if (auto layout = createLayout(id, delegate)) {
layout->initDimensions();
it = _layouts.emplace(id, std::move(layout)).first;
_peer->owner().stories().registerPolling(
{ _peer->id, id },
Data::Stories::Polling::Chat);
} else {
return nullptr;
}

View File

@ -30,6 +30,7 @@ class Provider final
, public base::has_weak_ptr {
public:
explicit Provider(not_null<AbstractController*> controller);
~Provider();
Media::Type type() override;
bool hasSelectRestriction() override;
@ -99,6 +100,7 @@ private:
void itemRemoved(not_null<const HistoryItem*> item);
void markLayoutsStale();
void clearStaleLayouts();
void clear();
[[nodiscard]] HistoryItem *ensureItem(StoryId id);
[[nodiscard]] Media::BaseLayout *getLayout(

View File

@ -302,7 +302,9 @@ Controller::Controller(not_null<Delegate*> delegate)
_contentFadeAnimation.stop();
}
Controller::~Controller() = default;
Controller::~Controller() {
changeShown(nullptr);
}
void Controller::updateContentFaded() {
if (_contentFaded == _replyActive) {
@ -480,6 +482,9 @@ void Controller::initLayout() {
}
Data::Story *Controller::story() const {
if (!_session) {
return nullptr;
}
const auto maybeStory = _session->data().stories().lookup(_shown);
return maybeStory ? maybeStory->get() : nullptr;
}
@ -748,10 +753,9 @@ void Controller::show(
.date = story->date(),
.edited = story->edited(),
});
if (_shown == storyId && _session == &story->session()) {
if (!changeShown(story)) {
return;
}
_shown = storyId;
_viewed = false;
invalidate_weak_ptrs(&_viewsLoadGuard);
_reactions->hide();
@ -769,41 +773,72 @@ void Controller::show(
.valid = user->isSelf(),
});
const auto session = &story->session();
if (_session != session) {
_session = session;
_sessionLifetime = session->changes().storyUpdates(
Data::StoryUpdate::Flag::Destroyed
) | rpl::start_with_next([=](Data::StoryUpdate update) {
if (update.story->fullId() == _shown) {
_delegate->storiesClose();
}
});
session->data().stories().itemsChanged(
) | rpl::start_with_next([=](PeerId peerId) {
if (_waitingForId.peer == peerId) {
checkWaitingFor();
}
}, _sessionLifetime);
session->changes().storyUpdates(
Data::StoryUpdate::Flag::Edited
) | rpl::filter([=](const Data::StoryUpdate &update) {
return (update.story == this->story());
}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
show(update.story, _context);
_delegate->storiesRedisplay(update.story);
}, _sessionLifetime);
_sessionLifetime.add([=] {
session->data().stories().setPreloadingInViewer({});
});
}
stories.loadAround(storyId, context);
updatePlayingAllowed();
user->updateFull();
}
bool Controller::changeShown(Data::Story *story) {
const auto id = story ? story->fullId() : FullStoryId();
const auto session = story ? &story->session() : nullptr;
const auto sessionChanged = (_session != session);
if (_shown == id && !sessionChanged) {
return false;
}
if (const auto now = this->story()) {
now->owner().stories().unregisterPolling(
now,
Data::Stories::Polling::Viewer);
}
if (sessionChanged) {
_sessionLifetime.destroy();
}
_shown = id;
_session = session;
if (sessionChanged) {
subscribeToSession();
}
if (story) {
story->owner().stories().registerPolling(
story,
Data::Stories::Polling::Viewer);
}
return true;
}
void Controller::subscribeToSession() {
Expects(!_sessionLifetime);
if (!_session) {
return;
}
_session->changes().storyUpdates(
Data::StoryUpdate::Flag::Destroyed
) | rpl::start_with_next([=](Data::StoryUpdate update) {
if (update.story->fullId() == _shown) {
_delegate->storiesClose();
}
}, _sessionLifetime);
_session->data().stories().itemsChanged(
) | rpl::start_with_next([=](PeerId peerId) {
if (_waitingForId.peer == peerId) {
checkWaitingFor();
}
}, _sessionLifetime);
_session->changes().storyUpdates(
Data::StoryUpdate::Flag::Edited
) | rpl::filter([=](const Data::StoryUpdate &update) {
return (update.story == this->story());
}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
show(update.story, _context);
_delegate->storiesRedisplay(update.story);
}, _sessionLifetime);
_sessionLifetime.add([=] {
_session->data().stories().setPreloadingInViewer({});
});
}
void Controller::updatePlayingAllowed() {
if (!_shown) {
return;

View File

@ -164,6 +164,8 @@ private:
class Unsupported;
void initLayout();
bool changeShown(Data::Story *story);
void subscribeToSession();
void updatePhotoPlayback(const Player::TrackState &state);
void updatePlayback(const Player::TrackState &state);
void updatePowerSaveBlocker(const Player::TrackState &state);