Mark stories as read.

This commit is contained in:
John Preston 2023-05-29 19:09:36 +04:00
parent f323370752
commit f814e401b9
7 changed files with 158 additions and 27 deletions

View File

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_download_manager.h"
@ -1685,6 +1686,9 @@ bool Application::readyToQuit() {
if (session->api().isQuitPrevent()) {
prevented = true;
}
if (session->data().stories().isQuitPrevent()) {
prevented = true;
}
}
}
}

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "core/application.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
@ -30,6 +31,7 @@ namespace {
constexpr auto kMaxResolveTogether = 100;
constexpr auto kIgnorePreloadAroundIfLoaded = 15;
constexpr auto kPreloadAroundCount = 30;
constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
using UpdateFlag = StoryUpdate::Flag;
@ -61,7 +63,7 @@ std::optional<StoryMedia> ParseMedia(
} // namespace
bool StoriesList::unread() const {
return !ids.empty() && readTill < ids.front();
return !ids.empty() && readTill < ids.back();
}
Story::Story(
@ -188,7 +190,9 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
return true;
}
Stories::Stories(not_null<Session*> owner) : _owner(owner) {
Stories::Stories(not_null<Session*> owner)
: _owner(owner)
, _markReadTimer([=] { sendMarkAsReadRequests(); }) {
}
Stories::~Stories() {
@ -222,13 +226,13 @@ StoriesList Stories::parse(const MTPUserStories &stories) {
for (const auto &story : list) {
story.match([&](const MTPDstoryItem &data) {
if (const auto story = parseAndApply(result.user, data)) {
result.ids.push_back(story->id());
result.ids.emplace(story->id());
} else {
applyDeleted({ peerFromUser(userId), data.vid().v });
--result.total;
}
}, [&](const MTPDstoryItemSkipped &data) {
result.ids.push_back(data.vid().v);
result.ids.emplace(data.vid().v);
}, [&](const MTPDstoryItemDeleted &data) {
applyDeleted({ peerFromUser(userId), data.vid().v });
--result.total;
@ -434,13 +438,13 @@ void Stories::applyDeleted(FullStoryId id) {
return list.user->id;
});
if (j != end(_all)) {
const auto till = ranges::remove(j->ids, id.story);
const auto removed = int(std::distance(till, end(j->ids)));
if (till != end(j->ids)) {
j->ids.erase(till, end(j->ids));
j->total = std::max(j->total - removed, 0);
const auto removed = j->ids.remove(id.story);
if (removed) {
if (j->ids.empty()) {
_all.erase(j);
} else {
Assert(j->total > 0);
--j->total;
}
_allChanged.fire({});
}
@ -534,8 +538,8 @@ void Stories::applyChanges(StoriesList &&list) {
if (i != end(_all)) {
auto added = false;
for (const auto id : list.ids) {
if (!ranges::contains(i->ids, id)) {
i->ids.push_back(id);
if (!i->ids.contains(id)) {
i->ids.emplace(id);
++i->total;
added = true;
}
@ -584,4 +588,78 @@ void Stories::loadAround(FullStoryId id) {
}
}
void Stories::markAsRead(FullStoryId id) {
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);
}
}
bool Stories::isQuitPrevent() {
if (!_markReadPending.empty()) {
sendMarkAsReadRequests();
}
if (_markReadRequests.empty()) {
return false;
}
LOG(("Stories prevents quit, marking as read..."));
return true;
}
} // namespace Data

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/expected.h"
#include "base/timer.h"
class Image;
class PhotoData;
@ -70,7 +71,7 @@ private:
struct StoriesList {
not_null<UserData*> user;
std::vector<StoryId> ids;
base::flat_set<StoryId> ids;
StoryId readTill = 0;
int total = 0;
@ -113,6 +114,9 @@ public:
FullStoryId id) const;
void resolve(FullStoryId id, Fn<void()> done);
[[nodiscard]] bool isQuitPrevent();
void markAsRead(FullStoryId id);
private:
[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
[[nodiscard]] Story *parseAndApply(
@ -129,6 +133,9 @@ private:
void applyDeleted(FullStoryId id);
void removeDependencyStory(not_null<Story*> story);
void sendMarkAsReadRequests();
void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
const not_null<Session*> _owner;
base::flat_map<
PeerId,
@ -154,6 +161,10 @@ private:
mtpRequestId _loadMoreRequestId = 0;
base::flat_set<PeerId> _markReadPending;
base::Timer _markReadTimer;
base::flat_set<PeerId> _markReadRequests;
};
} // namespace Data

View File

@ -37,11 +37,15 @@ struct List::Layout {
float64 userpicLeft = 0.;
float64 photoLeft = 0.;
float64 left = 0.;
float64 single = 0.;
int leftFull = 0;
int leftSmall = 0;
int singleFull = 0;
int singleSmall = 0;
int startIndexSmall = 0;
int endIndexSmall = 0;
int startIndexFull = 0;
int endIndexFull = 0;
int singleFull = 0;
};
List::List(
@ -277,11 +281,15 @@ List::Layout List::computeLayout() const {
.userpicLeft = userpicLeft,
.photoLeft = photoLeft,
.left = userpicLeft - photoLeft,
.single = lerp(st.shift, singleFull),
.leftFull = leftFull,
.leftSmall = leftSmall,
.singleFull = singleFull,
.singleSmall = st.shift,
.startIndexSmall = startIndexSmall,
.endIndexSmall = endIndexSmall,
.startIndexFull = startIndexFull,
.endIndexFull = endIndexFull,
.singleFull = singleFull,
};
}
@ -296,8 +304,6 @@ void List::paintEvent(QPaintEvent *e) {
auto &rendering = _data.empty() ? _hidingData : _data;
const auto line = lerp(st.lineTwice, full.lineTwice) / 2.;
const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.;
const auto singleSmall = st.shift;
const auto single = lerp(singleSmall, layout.singleFull);
const auto photoTopSmall = (st.height - st.photo) / 2.;
const auto photoTop = lerp(photoTopSmall, full.photoTop);
const auto photo = lerp(st.photo, full.photo);
@ -346,7 +352,7 @@ void List::paintEvent(QPaintEvent *e) {
const auto full = (drawFull && indexFull < layout.endIndexFull)
? &rendering.items[indexFull]
: nullptr;
const auto x = layout.left + single * index;
const auto x = layout.left + layout.single * index;
return Single{ x, indexSmall, small, indexFull, full };
};
const auto hasUnread = [&](const Single &single) {
@ -697,19 +703,17 @@ void List::updateSelected() {
const auto &full = st::dialogsStoriesFull;
const auto p = mapFromGlobal(_lastMousePosition);
const auto layout = computeLayout();
const auto firstRightFull = full.left + layout.singleFull;
const auto firstRightSmall = st.left
const auto firstRightFull = layout.leftFull
+ (layout.startIndexFull + 1) * layout.singleFull;
const auto firstRightSmall = layout.leftSmall
+ st.photoLeft
+ st.photo;
const auto stepFull = layout.singleFull;
const auto stepSmall = st.shift;
const auto lastRightAddFull = 0;
const auto lastRightAddSmall = st.photoLeft;
const auto lerp = [&](float64 a, float64 b) {
return a + (b - a) * layout.ratio;
};
const auto firstRight = lerp(firstRightSmall, firstRightFull);
const auto step = lerp(stepSmall, stepFull);
const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull);
const auto activateFull = (layout.ratio >= 0.5);
const auto startIndex = activateFull
@ -721,14 +725,16 @@ void List::updateSelected() {
const auto x = p.x();
const auto infiniteIndex = (x < firstRight)
? 0
: int(std::floor(((x - firstRight) / step) + 1));
: int(std::floor(((x - firstRight) / layout.single) + 1));
const auto index = (endIndex == startIndex)
? -1
: (infiniteIndex == endIndex - startIndex
&& x < firstRight
+ (endIndex - startIndex - 1) * step
+ (endIndex - startIndex - 1) * layout.single
+ lastRightAdd)
? (infiniteIndex - 1) // Last small part should still be clickable.
: (startIndex + infiniteIndex >= endIndex)
? -1
: infiniteIndex;
const auto selected = (index < 0
|| startIndex + index >= layout.itemsCount)

View File

@ -41,6 +41,8 @@ constexpr auto kSiblingOutsidePart = 0.24;
constexpr auto kSiblingUserpicSize = 0.3;
constexpr auto kInnerHeightMultiplier = 1.6;
constexpr auto kPreloadUsersCount = 3;
constexpr auto kMarkAsReadAfterSeconds = 1;
constexpr auto kMarkAsReadAfterProgress = 0.2;
} // namespace
@ -360,7 +362,7 @@ void Controller::show(
showSiblings(lists, index);
const auto &list = lists[index];
const auto id = list.ids[subindex];
const auto id = *(begin(list.ids) + subindex);
const auto storyId = FullStoryId{
.peer = list.user->id,
.story = id,
@ -464,6 +466,7 @@ void Controller::updatePhotoPlayback(const Player::TrackState &state) {
void Controller::updatePlayback(const Player::TrackState &state) {
_slider->updatePlayback(state);
updatePowerSaveBlocker(state);
maybeMarkAsRead(state);
if (Player::IsStoppedAtEnd(state.state)) {
if (!subjumpFor(1)) {
_delegate->storiesClose();
@ -471,6 +474,26 @@ void Controller::updatePlayback(const Player::TrackState &state) {
}
}
void Controller::maybeMarkAsRead(const Player::TrackState &state) {
const auto length = state.length;
const auto position = Player::IsStoppedAtEnd(state.state)
? state.length
: Player::IsStoppedOrStopping(state.state)
? 0
: state.position;
if (position > state.frequency * kMarkAsReadAfterSeconds) {
if (position > kMarkAsReadAfterProgress * length) {
markAsRead();
}
}
}
void Controller::markAsRead() {
Expects(_list.has_value());
_list->user->owner().stories().markAsRead(_shown);
}
bool Controller::subjumpAvailable(int delta) const {
const auto index = _index + delta;
if (index < 0) {
@ -482,6 +505,9 @@ bool Controller::subjumpAvailable(int delta) const {
}
bool Controller::subjumpFor(int delta) {
if (delta > 0) {
markAsRead();
}
const auto index = _index + delta;
if (index < 0) {
if (_siblingLeft && _siblingLeft->shownId().valid()) {
@ -507,7 +533,7 @@ void Controller::subjumpTo(int index) {
const auto id = FullStoryId{
.peer = _list->user->id,
.story = _list->ids[index]
.story = *(begin(_list->ids) + index)
};
auto &stories = _list->user->owner().stories();
if (stories.lookup(id)) {
@ -554,6 +580,9 @@ bool Controller::jumpFor(int delta) {
return true;
}
} else if (delta == 1) {
if (_list && _index + 1 >= _list->total) {
markAsRead();
}
if (const auto right = _siblingRight.get()) {
_delegate->storiesJumpTo(
&right->peer()->session(),

View File

@ -123,6 +123,8 @@ private:
void updatePhotoPlayback(const Player::TrackState &state);
void updatePlayback(const Player::TrackState &state);
void updatePowerSaveBlocker(const Player::TrackState &state);
void maybeMarkAsRead(const Player::TrackState &state);
void markAsRead();
void showSiblings(
const std::vector<Data::StoriesList> &lists,

View File

@ -2486,7 +2486,8 @@ void SessionController::openPeerStories(PeerId peerId) {
return list.user->id;
});
if (i != end(all) && !i->ids.empty()) {
openPeerStory(i->user, i->ids.front());
const auto j = i->ids.lower_bound(i->readTill + 1);
openPeerStory(i->user, j != i->ids.end() ? *j : i->ids.front());
}
}