1000 lines
26 KiB
C++
1000 lines
26 KiB
C++
/*
|
|
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_changes.h"
|
|
#include "data/data_channel.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_stories.h"
|
|
#include "data/data_thread.h"
|
|
#include "history/history_item.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 "storage/file_download.h" // kMaxFileInMemory
|
|
#include "ui/text/text_utilities.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
using UpdateFlag = StoryUpdate::Flag;
|
|
|
|
[[nodiscard]] StoryArea ParseArea(const MTPMediaAreaCoordinates &area) {
|
|
const auto &data = area.data();
|
|
const auto center = QPointF(data.vx().v, data.vy().v);
|
|
const auto size = QSizeF(data.vw().v, data.vh().v);
|
|
const auto corner = center - QPointF(size.width(), size.height()) / 2.;
|
|
return {
|
|
.geometry = { corner / 100., size / 100. },
|
|
.rotation = data.vrotation().v,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {
|
|
const auto link = [&](const EntityInText &entity) {
|
|
return (entity.type() == EntityType::CustomUrl)
|
|
|| (entity.type() == EntityType::Url)
|
|
|| (entity.type() == EntityType::Mention)
|
|
|| (entity.type() == EntityType::Hashtag);
|
|
};
|
|
text.entities.erase(
|
|
ranges::remove_if(text.entities, link),
|
|
text.entities.end());
|
|
return text;
|
|
}
|
|
|
|
[[nodiscard]] auto ParseLocation(const MTPMediaArea &area)
|
|
-> std::optional<StoryLocation> {
|
|
auto result = std::optional<StoryLocation>();
|
|
area.match([&](const MTPDmediaAreaVenue &data) {
|
|
data.vgeo().match([&](const MTPDgeoPoint &geo) {
|
|
result.emplace(StoryLocation{
|
|
.area = ParseArea(data.vcoordinates()),
|
|
.point = Data::LocationPoint(geo),
|
|
.title = qs(data.vtitle()),
|
|
.address = qs(data.vaddress()),
|
|
.provider = qs(data.vprovider()),
|
|
.venueId = qs(data.vvenue_id()),
|
|
.venueType = qs(data.vvenue_type()),
|
|
});
|
|
}, [](const MTPDgeoPointEmpty &) {
|
|
});
|
|
}, [&](const MTPDmediaAreaGeoPoint &data) {
|
|
data.vgeo().match([&](const MTPDgeoPoint &geo) {
|
|
result.emplace(StoryLocation{
|
|
.area = ParseArea(data.vcoordinates()),
|
|
.point = Data::LocationPoint(geo),
|
|
});
|
|
}, [](const MTPDgeoPointEmpty &) {
|
|
});
|
|
}, [&](const MTPDmediaAreaSuggestedReaction &data) {
|
|
}, [&](const MTPDmediaAreaChannelPost &data) {
|
|
}, [&](const MTPDinputMediaAreaChannelPost &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
|
|
}, [&](const MTPDinputMediaAreaVenue &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] auto ParseSuggestedReaction(const MTPMediaArea &area)
|
|
-> std::optional<SuggestedReaction> {
|
|
auto result = std::optional<SuggestedReaction>();
|
|
area.match([&](const MTPDmediaAreaVenue &data) {
|
|
}, [&](const MTPDmediaAreaGeoPoint &data) {
|
|
}, [&](const MTPDmediaAreaSuggestedReaction &data) {
|
|
result.emplace(SuggestedReaction{
|
|
.area = ParseArea(data.vcoordinates()),
|
|
.reaction = Data::ReactionFromMTP(data.vreaction()),
|
|
.flipped = data.is_flipped(),
|
|
.dark = data.is_dark(),
|
|
});
|
|
}, [&](const MTPDmediaAreaChannelPost &data) {
|
|
}, [&](const MTPDinputMediaAreaChannelPost &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
|
|
}, [&](const MTPDinputMediaAreaVenue &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] auto ParseChannelPost(const MTPMediaArea &area)
|
|
-> std::optional<ChannelPost> {
|
|
auto result = std::optional<ChannelPost>();
|
|
area.match([&](const MTPDmediaAreaVenue &data) {
|
|
}, [&](const MTPDmediaAreaGeoPoint &data) {
|
|
}, [&](const MTPDmediaAreaSuggestedReaction &data) {
|
|
}, [&](const MTPDmediaAreaChannelPost &data) {
|
|
result.emplace(ChannelPost{
|
|
.area = ParseArea(data.vcoordinates()),
|
|
.itemId = FullMsgId(
|
|
peerFromChannel(data.vchannel_id()),
|
|
data.vmsg_id().v),
|
|
});
|
|
}, [&](const MTPDinputMediaAreaChannelPost &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
|
|
}, [&](const MTPDinputMediaAreaVenue &data) {
|
|
LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] PeerData *RepostSourcePeer(
|
|
not_null<Session*> owner,
|
|
const MTPDstoryItem &data) {
|
|
if (const auto forwarded = data.vfwd_from()) {
|
|
if (const auto from = forwarded->data().vfrom()) {
|
|
return owner->peer(peerFromMTP(*from));
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
[[nodiscard]] QString RepostSourceName(const MTPDstoryItem &data) {
|
|
if (const auto forwarded = data.vfwd_from()) {
|
|
return qs(forwarded->data().vfrom_name().value_or_empty());
|
|
}
|
|
return {};
|
|
}
|
|
|
|
[[nodiscard]] StoryId RepostSourceId(const MTPDstoryItem &data) {
|
|
if (const auto forwarded = data.vfwd_from()) {
|
|
return forwarded->data().vstory_id().value_or_empty();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
[[nodiscard]] bool RepostModified(const MTPDstoryItem &data) {
|
|
if (const auto forwarded = data.vfwd_from()) {
|
|
return forwarded->data().is_modified();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] PeerData *FromPeer(
|
|
not_null<Session*> owner,
|
|
const MTPDstoryItem &data) {
|
|
if (const auto from = data.vfrom_id()) {
|
|
return owner->peer(peerFromMTP(*from));
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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(),
|
|
id)
|
|
, _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;
|
|
_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,
|
|
const MTPDstoryItem &data,
|
|
TimeId now)
|
|
: _id(id)
|
|
, _peer(peer)
|
|
, _repostSourcePeer(RepostSourcePeer(&peer->owner(), data))
|
|
, _repostSourceName(RepostSourceName(data))
|
|
, _repostSourceId(RepostSourceId(data))
|
|
, _fromPeer(FromPeer(&peer->owner(), data))
|
|
, _date(data.vdate().v)
|
|
, _expires(data.vexpire_date().v)
|
|
, _repostModified(RepostModified(data)) {
|
|
applyFields(std::move(media), data, now, true);
|
|
}
|
|
|
|
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(fullId(), _peer, false);
|
|
}, [&](not_null<DocumentData*> document) {
|
|
return document->getReplyPreview(fullId(), _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::Colorized(type)
|
|
: tr::lng_dialogs_text_media(
|
|
tr::now,
|
|
lt_media_part,
|
|
tr::lng_dialogs_text_media_wrapped(
|
|
tr::now,
|
|
lt_media,
|
|
Ui::Text::Colorized(type),
|
|
Ui::Text::WithEntities),
|
|
lt_caption,
|
|
_caption,
|
|
Ui::Text::WithEntities);
|
|
}
|
|
|
|
void Story::setPinned(bool pinned) {
|
|
_pinned = pinned;
|
|
}
|
|
|
|
bool Story::pinned() const {
|
|
return _pinned;
|
|
}
|
|
|
|
StoryPrivacy Story::privacy() const {
|
|
return _privacyPublic
|
|
? StoryPrivacy::Public
|
|
: _privacyCloseFriends
|
|
? StoryPrivacy::CloseFriends
|
|
: _privacyContacts
|
|
? StoryPrivacy::Contacts
|
|
: _privacySelectedContacts
|
|
? StoryPrivacy::SelectedContacts
|
|
: StoryPrivacy::Other;
|
|
}
|
|
|
|
bool Story::forbidsForward() const {
|
|
return _noForwards;
|
|
}
|
|
|
|
bool Story::edited() const {
|
|
return _edited;
|
|
}
|
|
|
|
bool Story::out() const {
|
|
return _out;
|
|
}
|
|
|
|
bool Story::canDownloadIfPremium() const {
|
|
return !forbidsForward() || _peer->isSelf();
|
|
}
|
|
|
|
bool Story::canDownloadChecked() const {
|
|
return _peer->isSelf()
|
|
|| (canDownloadIfPremium() && _peer->session().premium());
|
|
}
|
|
|
|
bool Story::canShare() const {
|
|
return _privacyPublic && !forbidsForward() && (pinned() || !expired());
|
|
}
|
|
|
|
bool Story::canDelete() const {
|
|
if (const auto channel = _peer->asChannel()) {
|
|
return channel->canDeleteStories()
|
|
|| (out() && channel->canPostStories());
|
|
}
|
|
return _peer->isSelf();
|
|
}
|
|
|
|
bool Story::canReport() const {
|
|
return !_peer->isSelf();
|
|
}
|
|
|
|
bool Story::hasDirectLink() const {
|
|
if (!_privacyPublic || (!_pinned && expired())) {
|
|
return false;
|
|
}
|
|
return !_peer->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;
|
|
}
|
|
|
|
Data::ReactionId Story::sentReactionId() const {
|
|
return _sentReactionId;
|
|
}
|
|
|
|
void Story::setReactionId(Data::ReactionId id) {
|
|
if (_sentReactionId != id) {
|
|
const auto wasEmpty = _sentReactionId.empty();
|
|
changeSuggestedReactionCount(_sentReactionId, -1);
|
|
_sentReactionId = id;
|
|
changeSuggestedReactionCount(id, 1);
|
|
|
|
if (_views.known && _sentReactionId.empty() != wasEmpty) {
|
|
const auto delta = wasEmpty ? 1 : -1;
|
|
if (_views.reactions + delta >= 0) {
|
|
_views.reactions += delta;
|
|
}
|
|
}
|
|
session().changes().storyUpdated(this, UpdateFlag::Reaction);
|
|
}
|
|
}
|
|
|
|
void Story::changeSuggestedReactionCount(Data::ReactionId id, int delta) {
|
|
if (id.empty() || !_peer->isChannel()) {
|
|
return;
|
|
}
|
|
for (auto &suggested : _suggestedReactions) {
|
|
if (suggested.reaction == id && suggested.count + delta >= 0) {
|
|
suggested.count += delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
|
|
return _recentViewers;
|
|
}
|
|
|
|
const StoryViews &Story::viewsList() const {
|
|
return _views;
|
|
}
|
|
|
|
const StoryViews &Story::channelReactionsList() const {
|
|
return _channelReactions;
|
|
}
|
|
|
|
int Story::interactions() const {
|
|
return _views.total;
|
|
}
|
|
|
|
int Story::views() const {
|
|
return _views.views;
|
|
}
|
|
|
|
int Story::forwards() const {
|
|
return _views.forwards;
|
|
}
|
|
|
|
int Story::reactions() const {
|
|
return _views.reactions;
|
|
}
|
|
|
|
void Story::applyViewsSlice(
|
|
const QString &offset,
|
|
const StoryViews &slice) {
|
|
const auto changed = (_views.reactions != slice.reactions)
|
|
|| (_views.views != slice.views)
|
|
|| (_views.forwards != slice.forwards)
|
|
|| (_views.total != slice.total);
|
|
_views.reactions = slice.reactions;
|
|
_views.forwards = slice.forwards;
|
|
_views.views = slice.views;
|
|
_views.total = slice.total;
|
|
_views.known = true;
|
|
if (offset.isEmpty()) {
|
|
_views = slice;
|
|
if (!_channelReactions.total) {
|
|
_channelReactions.total = _views.reactions + _views.forwards;
|
|
}
|
|
} else if (_views.nextOffset == offset) {
|
|
_views.list.insert(
|
|
end(_views.list),
|
|
begin(slice.list),
|
|
end(slice.list));
|
|
_views.nextOffset = slice.nextOffset;
|
|
if (_views.nextOffset.isEmpty()) {
|
|
_views.total = int(_views.list.size());
|
|
_views.reactions = _views.total
|
|
- ranges::count(
|
|
_views.list,
|
|
Data::ReactionId(),
|
|
&StoryView::reaction);
|
|
_views.forwards = _views.total
|
|
- ranges::count(
|
|
_views.list,
|
|
0,
|
|
[](const StoryView &view) {
|
|
return view.repostId
|
|
? view.repostId
|
|
: view.forwardId.bare;
|
|
});
|
|
}
|
|
}
|
|
const auto known = int(_views.list.size());
|
|
if (known >= _recentViewers.size()) {
|
|
const auto take = std::min(known, kRecentViewersMax);
|
|
auto viewers = _views.list
|
|
| ranges::views::take(take)
|
|
| ranges::views::transform(&StoryView::peer)
|
|
| ranges::to_vector;
|
|
if (_recentViewers != viewers) {
|
|
_recentViewers = std::move(viewers);
|
|
if (!changed) {
|
|
// Count not changed, but list of recent viewers changed.
|
|
_peer->session().changes().storyUpdated(
|
|
this,
|
|
UpdateFlag::ViewsChanged);
|
|
}
|
|
}
|
|
}
|
|
if (changed) {
|
|
_peer->session().changes().storyUpdated(
|
|
this,
|
|
UpdateFlag::ViewsChanged);
|
|
}
|
|
}
|
|
|
|
void Story::applyChannelReactionsSlice(
|
|
const QString &offset,
|
|
const StoryViews &slice) {
|
|
const auto changed = (_channelReactions.reactions != slice.reactions)
|
|
|| (_channelReactions.total != slice.total);
|
|
_channelReactions.reactions = slice.reactions;
|
|
_channelReactions.total = slice.total;
|
|
_channelReactions.known = true;
|
|
if (offset.isEmpty()) {
|
|
_channelReactions = slice;
|
|
} else if (_channelReactions.nextOffset == offset) {
|
|
_channelReactions.list.insert(
|
|
end(_channelReactions.list),
|
|
begin(slice.list),
|
|
end(slice.list));
|
|
_channelReactions.nextOffset = slice.nextOffset;
|
|
if (_channelReactions.nextOffset.isEmpty()) {
|
|
_channelReactions.total = int(_channelReactions.list.size());
|
|
}
|
|
}
|
|
if (changed) {
|
|
_peer->session().changes().storyUpdated(
|
|
this,
|
|
UpdateFlag::ViewsChanged);
|
|
}
|
|
}
|
|
|
|
const std::vector<StoryLocation> &Story::locations() const {
|
|
return _locations;
|
|
}
|
|
|
|
const std::vector<SuggestedReaction> &Story::suggestedReactions() const {
|
|
return _suggestedReactions;
|
|
}
|
|
|
|
const std::vector<ChannelPost> &Story::channelPosts() const {
|
|
return _channelPosts;
|
|
}
|
|
|
|
void Story::applyChanges(
|
|
StoryMedia media,
|
|
const MTPDstoryItem &data,
|
|
TimeId now) {
|
|
applyFields(std::move(media), data, now, false);
|
|
}
|
|
|
|
Story::ViewsCounts Story::parseViewsCounts(
|
|
const MTPDstoryViews &data,
|
|
const Data::ReactionId &mine) {
|
|
auto result = ViewsCounts{
|
|
.views = data.vviews_count().v,
|
|
.forwards = data.vforwards_count().value_or_empty(),
|
|
.reactions = data.vreactions_count().value_or_empty(),
|
|
};
|
|
if (const auto list = data.vrecent_viewers()) {
|
|
result.viewers.reserve(list->v.size());
|
|
auto &owner = _peer->owner();
|
|
auto &&cut = list->v
|
|
| ranges::views::take(kRecentViewersMax);
|
|
for (const auto &id : cut) {
|
|
result.viewers.push_back(owner.peer(peerFromUser(id)));
|
|
}
|
|
}
|
|
auto total = 0;
|
|
if (const auto list = data.vreactions()
|
|
; list && _peer->isChannel()) {
|
|
result.reactionsCounts.reserve(list->v.size());
|
|
for (const auto &reaction : list->v) {
|
|
const auto &data = reaction.data();
|
|
const auto id = Data::ReactionFromMTP(data.vreaction());
|
|
const auto count = data.vcount().v;
|
|
result.reactionsCounts[id] = count;
|
|
total += count;
|
|
}
|
|
}
|
|
if (!mine.empty()) {
|
|
if (auto &count = result.reactionsCounts[mine]; !count) {
|
|
count = 1;
|
|
++total;
|
|
}
|
|
}
|
|
if (result.reactions < total) {
|
|
result.reactions = total;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Story::applyFields(
|
|
StoryMedia media,
|
|
const MTPDstoryItem &data,
|
|
TimeId now,
|
|
bool initial) {
|
|
_lastUpdateTime = now;
|
|
|
|
const auto reaction = data.is_min()
|
|
? _sentReactionId
|
|
: data.vsent_reaction()
|
|
? Data::ReactionFromMTP(*data.vsent_reaction())
|
|
: Data::ReactionId();
|
|
const auto pinned = data.is_pinned();
|
|
const auto edited = data.is_edited();
|
|
const auto privacy = data.is_public()
|
|
? StoryPrivacy::Public
|
|
: data.is_close_friends()
|
|
? StoryPrivacy::CloseFriends
|
|
: data.is_contacts()
|
|
? StoryPrivacy::Contacts
|
|
: data.is_selected_contacts()
|
|
? StoryPrivacy::SelectedContacts
|
|
: StoryPrivacy::Other;
|
|
const auto noForwards = data.is_noforwards();
|
|
const auto out = data.is_min() ? _out : data.is_out();
|
|
auto caption = TextWithEntities{
|
|
data.vcaption().value_or_empty(),
|
|
Api::EntitiesFromMTP(
|
|
&owner().session(),
|
|
data.ventities().value_or_empty()),
|
|
};
|
|
if (const auto user = _peer->asUser()) {
|
|
if (!user->isVerified() && !user->isPremium()) {
|
|
caption = StripLinks(std::move(caption));
|
|
}
|
|
}
|
|
auto counts = ViewsCounts();
|
|
auto viewsKnown = _views.known;
|
|
if (const auto info = data.vviews()) {
|
|
counts = parseViewsCounts(info->data(), reaction);
|
|
viewsKnown = true;
|
|
} else {
|
|
counts.views = _views.total;
|
|
counts.forwards = _views.forwards;
|
|
counts.reactions = _views.reactions;
|
|
counts.viewers = _recentViewers;
|
|
for (const auto &suggested : _suggestedReactions) {
|
|
if (const auto count = suggested.count) {
|
|
counts.reactionsCounts[suggested.reaction] = count;
|
|
}
|
|
}
|
|
}
|
|
auto locations = std::vector<StoryLocation>();
|
|
auto suggestedReactions = std::vector<SuggestedReaction>();
|
|
auto channelPosts = std::vector<ChannelPost>();
|
|
if (const auto areas = data.vmedia_areas()) {
|
|
for (const auto &area : areas->v) {
|
|
if (const auto location = ParseLocation(area)) {
|
|
locations.push_back(*location);
|
|
} else if (auto reaction = ParseSuggestedReaction(area)) {
|
|
const auto i = counts.reactionsCounts.find(
|
|
reaction->reaction);
|
|
if (i != end(counts.reactionsCounts)) {
|
|
reaction->count = i->second;
|
|
}
|
|
suggestedReactions.push_back(*reaction);
|
|
} else if (auto post = ParseChannelPost(area)) {
|
|
channelPosts.push_back(*post);
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto pinnedChanged = (_pinned != pinned);
|
|
const auto editedChanged = (_edited != edited);
|
|
const auto mediaChanged = (_media != media);
|
|
const auto captionChanged = (_caption != caption);
|
|
const auto locationsChanged = (_locations != locations);
|
|
const auto suggestedReactionsChanged
|
|
= (_suggestedReactions != suggestedReactions);
|
|
const auto channelPostsChanged = (_channelPosts != channelPosts);
|
|
const auto reactionChanged = (_sentReactionId != reaction);
|
|
|
|
_out = out;
|
|
_privacyPublic = (privacy == StoryPrivacy::Public);
|
|
_privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends);
|
|
_privacyContacts = (privacy == StoryPrivacy::Contacts);
|
|
_privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts);
|
|
_edited = edited;
|
|
_pinned = pinned;
|
|
_noForwards = noForwards;
|
|
if (mediaChanged) {
|
|
_media = std::move(media);
|
|
}
|
|
if (captionChanged) {
|
|
_caption = std::move(caption);
|
|
}
|
|
if (locationsChanged) {
|
|
_locations = std::move(locations);
|
|
}
|
|
if (suggestedReactionsChanged) {
|
|
_suggestedReactions = std::move(suggestedReactions);
|
|
}
|
|
if (channelPostsChanged) {
|
|
_channelPosts = std::move(channelPosts);
|
|
}
|
|
if (reactionChanged) {
|
|
_sentReactionId = reaction;
|
|
}
|
|
updateViewsCounts(std::move(counts), viewsKnown, initial);
|
|
|
|
const auto changed = editedChanged
|
|
|| captionChanged
|
|
|| mediaChanged
|
|
|| locationsChanged
|
|
|| channelPostsChanged;
|
|
const auto reactionsChanged = reactionChanged
|
|
|| suggestedReactionsChanged;
|
|
if (!initial && (changed || reactionsChanged)) {
|
|
_peer->session().changes().storyUpdated(this, UpdateFlag()
|
|
| (changed ? UpdateFlag::Edited : UpdateFlag())
|
|
| (reactionsChanged ? UpdateFlag::Reaction : UpdateFlag()));
|
|
}
|
|
if (!initial && (captionChanged || mediaChanged)) {
|
|
if (const auto item = _peer->owner().stories().lookupItem(this)) {
|
|
item->applyChanges(this);
|
|
}
|
|
_peer->owner().refreshStoryItemViews(fullId());
|
|
}
|
|
if (pinnedChanged) {
|
|
_peer->owner().stories().savedStateChanged(this);
|
|
}
|
|
}
|
|
|
|
void Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) {
|
|
const auto total = _views.total
|
|
? _views.total
|
|
: (counts.views + counts.forwards);
|
|
const auto viewsChanged = (_views.total != total)
|
|
|| (_views.forwards != counts.forwards)
|
|
|| (_views.reactions != counts.reactions)
|
|
|| (_recentViewers != counts.viewers);
|
|
if (_views.reactions != counts.reactions
|
|
|| _views.forwards != counts.forwards
|
|
|| _views.total != total
|
|
|| _views.known != known) {
|
|
_views = StoryViews{
|
|
.reactions = counts.reactions,
|
|
.forwards = counts.forwards,
|
|
.views = counts.views,
|
|
.total = total,
|
|
.known = known,
|
|
};
|
|
if (!_channelReactions.total) {
|
|
_channelReactions.total = _views.reactions + _views.forwards;
|
|
}
|
|
}
|
|
if (viewsChanged) {
|
|
_recentViewers = std::move(counts.viewers);
|
|
_peer->session().changes().storyUpdated(
|
|
this,
|
|
UpdateFlag::ViewsChanged);
|
|
}
|
|
}
|
|
|
|
void Story::applyViewsCounts(const MTPDstoryViews &data) {
|
|
auto counts = parseViewsCounts(data, _sentReactionId);
|
|
auto suggestedCountsChanged = false;
|
|
for (auto &suggested : _suggestedReactions) {
|
|
const auto i = counts.reactionsCounts.find(suggested.reaction);
|
|
const auto v = (i != end(counts.reactionsCounts)) ? i->second : 0;
|
|
if (suggested.count != v) {
|
|
suggested.count = v;
|
|
suggestedCountsChanged = true;
|
|
}
|
|
}
|
|
updateViewsCounts(std::move(counts), true, false);
|
|
if (suggestedCountsChanged) {
|
|
_peer->session().changes().storyUpdated(this, UpdateFlag::Reaction);
|
|
}
|
|
}
|
|
|
|
TimeId Story::lastUpdateTime() const {
|
|
return _lastUpdateTime;
|
|
}
|
|
|
|
bool Story::repost() const {
|
|
return _repostSourcePeer || !_repostSourceName.isEmpty();
|
|
}
|
|
|
|
bool Story::repostModified() const {
|
|
return _repostModified;
|
|
}
|
|
|
|
PeerData *Story::repostSourcePeer() const {
|
|
return _repostSourcePeer;
|
|
}
|
|
|
|
QString Story::repostSourceName() const {
|
|
return _repostSourceName;
|
|
}
|
|
|
|
StoryId Story::repostSourceId() const {
|
|
return _repostSourceId;
|
|
}
|
|
|
|
PeerData *Story::fromPeer() const {
|
|
return _fromPeer;
|
|
}
|
|
|
|
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() {
|
|
if (const auto photo = _story->photo()) {
|
|
_photo = photo->createMediaView();
|
|
if (_photo->loaded()) {
|
|
callDone();
|
|
} else {
|
|
_photo->automaticLoad(_story->fullId(), _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()) {
|
|
Assert(data.size() < Storage::kMaxFileInMemory);
|
|
_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
|