Start stories overview in profile / My Stories.
This commit is contained in:
parent
8eac04cb90
commit
7f8a985067
|
@ -551,6 +551,8 @@ PRIVATE
|
||||||
data/data_sponsored_messages.h
|
data/data_sponsored_messages.h
|
||||||
data/data_stories.cpp
|
data/data_stories.cpp
|
||||||
data/data_stories.h
|
data/data_stories.h
|
||||||
|
data/data_stories_ids.cpp
|
||||||
|
data/data_stories_ids.h
|
||||||
data/data_streaming.cpp
|
data/data_streaming.cpp
|
||||||
data/data_streaming.h
|
data/data_streaming.h
|
||||||
data/data_thread.cpp
|
data/data_thread.cpp
|
||||||
|
@ -871,6 +873,12 @@ PRIVATE
|
||||||
info/profile/info_profile_widget.h
|
info/profile/info_profile_widget.h
|
||||||
info/settings/info_settings_widget.cpp
|
info/settings/info_settings_widget.cpp
|
||||||
info/settings/info_settings_widget.h
|
info/settings/info_settings_widget.h
|
||||||
|
info/stories/info_stories_inner_widget.cpp
|
||||||
|
info/stories/info_stories_inner_widget.h
|
||||||
|
info/stories/info_stories_provider.cpp
|
||||||
|
info/stories/info_stories_provider.h
|
||||||
|
info/stories/info_stories_widget.cpp
|
||||||
|
info/stories/info_stories_widget.h
|
||||||
info/userpic/info_userpic_colors_editor.cpp
|
info/userpic/info_userpic_colors_editor.cpp
|
||||||
info/userpic/info_userpic_colors_editor.h
|
info/userpic/info_userpic_colors_editor.h
|
||||||
info/userpic/info_userpic_emoji_builder.cpp
|
info/userpic/info_userpic_emoji_builder.cpp
|
||||||
|
|
|
@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_menu_activate" = "Activate";
|
"lng_menu_activate" = "Activate";
|
||||||
"lng_menu_set_status" = "Set Emoji Status";
|
"lng_menu_set_status" = "Set Emoji Status";
|
||||||
"lng_menu_change_status" = "Change Emoji Status";
|
"lng_menu_change_status" = "Change Emoji Status";
|
||||||
|
"lng_menu_my_stories" = "My Stories";
|
||||||
|
|
||||||
"lng_disable_notifications_from_tray" = "Disable notifications";
|
"lng_disable_notifications_from_tray" = "Disable notifications";
|
||||||
"lng_enable_notifications_from_tray" = "Enable notifications";
|
"lng_enable_notifications_from_tray" = "Enable notifications";
|
||||||
|
|
|
@ -57,7 +57,7 @@ public:
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
[[nodiscard]] std::optional<Id> nearest(Id id) const {
|
[[nodiscard]] std::optional<Id> nearest(Id id) const {
|
||||||
static_assert(std::is_same_v<IdsContainer, base::flat_set<MsgId>>);
|
static_assert(std::is_same_v<IdsContainer, base::flat_set<Id>>);
|
||||||
if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) {
|
if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) {
|
||||||
return *it;
|
return *it;
|
||||||
} else if (_ids.empty()) {
|
} else if (_ids.empty()) {
|
||||||
|
@ -70,6 +70,10 @@ public:
|
||||||
std::swap(_skippedBefore, _skippedAfter);
|
std::swap(_skippedBefore, _skippedAfter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
friend inline bool operator==(
|
||||||
|
const AbstractSparseIds&,
|
||||||
|
const AbstractSparseIds&) = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
IdsContainer _ids;
|
IdsContainer _ids;
|
||||||
std::optional<int> _fullCount;
|
std::optional<int> _fullCount;
|
||||||
|
|
|
@ -51,14 +51,49 @@ Q_DECLARE_METATYPE(MsgId);
|
||||||
return MsgId(a.bare - b.bare);
|
return MsgId(a.bare - b.bare);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using StoryId = int32;
|
||||||
|
|
||||||
|
struct FullStoryId {
|
||||||
|
PeerId peer = 0;
|
||||||
|
StoryId story = 0;
|
||||||
|
|
||||||
|
[[nodiscard]] bool valid() const {
|
||||||
|
return peer != 0 && story != 0;
|
||||||
|
}
|
||||||
|
explicit operator bool() const {
|
||||||
|
return valid();
|
||||||
|
}
|
||||||
|
friend inline auto operator<=>(FullStoryId, FullStoryId) = default;
|
||||||
|
friend inline bool operator==(FullStoryId, FullStoryId) = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FullReplyTo {
|
||||||
|
MsgId msgId = 0;
|
||||||
|
MsgId topicRootId = 0;
|
||||||
|
FullStoryId storyId;
|
||||||
|
|
||||||
|
[[nodiscard]] bool valid() const {
|
||||||
|
return msgId || storyId;
|
||||||
|
}
|
||||||
|
explicit operator bool() const {
|
||||||
|
return valid();
|
||||||
|
}
|
||||||
|
friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default;
|
||||||
|
friend inline bool operator==(FullReplyTo, FullReplyTo) = default;
|
||||||
|
};
|
||||||
|
|
||||||
constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58));
|
constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58));
|
||||||
constexpr auto ClientMsgIds = (1LL << 31);
|
constexpr auto ClientMsgIds = (1LL << 31);
|
||||||
constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds);
|
constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds);
|
||||||
|
constexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1);
|
||||||
|
constexpr auto ServerMaxStoryId = StoryId(1 << 30);
|
||||||
|
constexpr auto StoryMsgIds = int64(ServerMaxStoryId);
|
||||||
|
constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds);
|
||||||
constexpr auto ServerMaxMsgId = MsgId(1LL << 56);
|
constexpr auto ServerMaxMsgId = MsgId(1LL << 56);
|
||||||
constexpr auto ScheduledMsgIdsRange = (1LL << 32);
|
constexpr auto ScheduledMsgIdsRange = (1LL << 32);
|
||||||
constexpr auto ShowAtUnreadMsgId = MsgId(0);
|
constexpr auto ShowAtUnreadMsgId = MsgId(0);
|
||||||
|
|
||||||
constexpr auto SpecialMsgIdShift = EndClientMsgId.bare;
|
constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare;
|
||||||
constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1);
|
constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1);
|
||||||
constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2);
|
constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2);
|
||||||
constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4);
|
constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4);
|
||||||
|
@ -81,6 +116,20 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId);
|
||||||
return MsgId(StartClientMsgId.bare + index);
|
return MsgId(StartClientMsgId.bare + index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept {
|
||||||
|
return (id >= StartStoryMsgId && id < EndStoryMsgId);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept {
|
||||||
|
Expects(IsStoryMsgId(id));
|
||||||
|
|
||||||
|
return StoryId(id.bare - StartStoryMsgId.bare);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept {
|
||||||
|
Expects(id >= 0);
|
||||||
|
|
||||||
|
return MsgId(StartStoryMsgId.bare + id);
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept {
|
[[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept {
|
||||||
return (id > 0 && id < ServerMaxMsgId);
|
return (id > 0 && id < ServerMaxMsgId);
|
||||||
}
|
}
|
||||||
|
@ -136,37 +185,6 @@ struct GlobalMsgId {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using StoryId = int32;
|
|
||||||
|
|
||||||
struct FullStoryId {
|
|
||||||
PeerId peer = 0;
|
|
||||||
StoryId story = 0;
|
|
||||||
|
|
||||||
[[nodiscard]] bool valid() const {
|
|
||||||
return peer != 0 && story != 0;
|
|
||||||
}
|
|
||||||
explicit operator bool() const {
|
|
||||||
return valid();
|
|
||||||
}
|
|
||||||
friend inline auto operator<=>(FullStoryId, FullStoryId) = default;
|
|
||||||
friend inline bool operator==(FullStoryId, FullStoryId) = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FullReplyTo {
|
|
||||||
MsgId msgId = 0;
|
|
||||||
MsgId topicRootId = 0;
|
|
||||||
FullStoryId storyId;
|
|
||||||
|
|
||||||
[[nodiscard]] bool valid() const {
|
|
||||||
return msgId || storyId;
|
|
||||||
}
|
|
||||||
explicit operator bool() const {
|
|
||||||
return valid();
|
|
||||||
}
|
|
||||||
friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default;
|
|
||||||
friend inline bool operator==(FullReplyTo, FullReplyTo) = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace std {
|
namespace std {
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
|
|
|
@ -35,6 +35,8 @@ constexpr auto kPreloadAroundCount = 30;
|
||||||
constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
|
constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
|
||||||
constexpr auto kExpiredMineFirstPerPage = 30;
|
constexpr auto kExpiredMineFirstPerPage = 30;
|
||||||
constexpr auto kExpiredMinePerPage = 100;
|
constexpr auto kExpiredMinePerPage = 100;
|
||||||
|
constexpr auto kSavedFirstPerPage = 30;
|
||||||
|
constexpr auto kSavedPerPage = 100;
|
||||||
|
|
||||||
using UpdateFlag = StoryUpdate::Flag;
|
using UpdateFlag = StoryUpdate::Flag;
|
||||||
|
|
||||||
|
@ -351,6 +353,30 @@ void Stories::apply(const MTPDupdateStory &data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
|
||||||
|
if (!data) {
|
||||||
|
applyDeletedFromSources(peer->id, StorySourcesList::All);
|
||||||
|
_all.erase(peer->id);
|
||||||
|
const auto i = _stories.find(peer->id);
|
||||||
|
if (i != end(_stories)) {
|
||||||
|
auto stories = base::take(i->second);
|
||||||
|
_stories.erase(i);
|
||||||
|
for (const auto &[id, story] : stories) {
|
||||||
|
// Duplicated in Stories::applyDeleted.
|
||||||
|
_deleted.emplace(FullStoryId{ peer->id, id });
|
||||||
|
_expiring.remove(story->expires(), story->fullId());
|
||||||
|
session().changes().storyUpdated(
|
||||||
|
story.get(),
|
||||||
|
UpdateFlag::Destroyed);
|
||||||
|
removeDependencyStory(story.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_sourceChanged.fire_copy(peer->id);
|
||||||
|
} else {
|
||||||
|
parseAndApply(*data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Stories::requestUserStories(not_null<UserData*> user) {
|
void Stories::requestUserStories(not_null<UserData*> user) {
|
||||||
if (!_requestingUserStories.emplace(user).second) {
|
if (!_requestingUserStories.emplace(user).second) {
|
||||||
return;
|
return;
|
||||||
|
@ -480,6 +506,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
|
||||||
} else {
|
} else {
|
||||||
applyDeletedFromSources(peerId, StorySourcesList::All);
|
applyDeletedFromSources(peerId, StorySourcesList::All);
|
||||||
}
|
}
|
||||||
|
_sourceChanged.fire_copy(peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Story *Stories::parseAndApply(
|
Story *Stories::parseAndApply(
|
||||||
|
@ -503,6 +530,9 @@ Story *Stories::parseAndApply(
|
||||||
session().changes().storyUpdated(
|
session().changes().storyUpdated(
|
||||||
i->second.get(),
|
i->second.get(),
|
||||||
UpdateFlag::Edited);
|
UpdateFlag::Edited);
|
||||||
|
if (const auto item = lookupItem(i->second.get())) {
|
||||||
|
item->applyChanges(i->second.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return i->second.get();
|
return i->second.get();
|
||||||
}
|
}
|
||||||
|
@ -718,6 +748,7 @@ void Stories::applyDeleted(FullStoryId id) {
|
||||||
if (j != end(_stories)) {
|
if (j != end(_stories)) {
|
||||||
const auto k = j->second.find(id.story);
|
const auto k = j->second.find(id.story);
|
||||||
if (k != end(j->second)) {
|
if (k != end(j->second)) {
|
||||||
|
// Duplicated in Stories::apply(peer, const MTPUserStories*).
|
||||||
auto story = std::move(k->second);
|
auto story = std::move(k->second);
|
||||||
_expiring.remove(story->expires(), story->fullId());
|
_expiring.remove(story->expires(), story->fullId());
|
||||||
j->second.erase(k);
|
j->second.erase(k);
|
||||||
|
@ -746,7 +777,7 @@ void Stories::applyExpired(FullStoryId id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Stories::addToExpiredMine(not_null<Story*> story) {
|
void Stories::addToExpiredMine(not_null<Story*> story) {
|
||||||
const auto added = _expiredMine.emplace(story->id()).second;
|
const auto added = _expiredMine.list.emplace(story->id()).second;
|
||||||
if (added && _expiredMineTotal >= 0) {
|
if (added && _expiredMineTotal >= 0) {
|
||||||
++_expiredMineTotal;
|
++_expiredMineTotal;
|
||||||
}
|
}
|
||||||
|
@ -775,6 +806,7 @@ void Stories::applyRemovedFromActive(FullStoryId id) {
|
||||||
removeFromList(StorySourcesList::NotHidden);
|
removeFromList(StorySourcesList::NotHidden);
|
||||||
removeFromList(StorySourcesList::All);
|
removeFromList(StorySourcesList::All);
|
||||||
}
|
}
|
||||||
|
_sourceChanged.fire_copy(id.peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -824,8 +856,42 @@ void Stories::sort(StorySourcesList list) {
|
||||||
_sourcesChanged[index].fire({});
|
_sourcesChanged[index].fire({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const base::flat_map<PeerId, StoriesSource> &Stories::all() const {
|
std::shared_ptr<HistoryItem> Stories::lookupItem(not_null<Story*> story) {
|
||||||
return _all;
|
const auto i = _items.find(story->peer()->id);
|
||||||
|
if (i == end(_items)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto j = i->second.find(story->id());
|
||||||
|
if (j == end(i->second)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return j->second.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) {
|
||||||
|
auto &items = _items[story->peer()->id];
|
||||||
|
auto i = items.find(story->id());
|
||||||
|
if (i == end(items)) {
|
||||||
|
i = items.emplace(story->id()).first;
|
||||||
|
} else if (const auto result = i->second.lock()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const auto history = _owner->history(story->peer());
|
||||||
|
auto result = std::shared_ptr<HistoryItem>(
|
||||||
|
history->makeMessage(story).get(),
|
||||||
|
HistoryItem::Destroyer());
|
||||||
|
i->second = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<HistoryItem> Stories::resolveItem(FullStoryId id) {
|
||||||
|
const auto story = lookup(id);
|
||||||
|
return story ? resolveItem(*story) : std::shared_ptr<HistoryItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const StoriesSource *Stories::source(PeerId id) const {
|
||||||
|
const auto i = _all.find(id);
|
||||||
|
return (i != end(_all)) ? &i->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<StoriesSourceInfo> &Stories::sources(
|
const std::vector<StoriesSourceInfo> &Stories::sources(
|
||||||
|
@ -841,6 +907,10 @@ rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const {
|
||||||
return _sourcesChanged[static_cast<int>(list)].events();
|
return _sourcesChanged[static_cast<int>(list)].events();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpl::producer<PeerId> Stories::sourceChanged() const {
|
||||||
|
return _sourceChanged.events();
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<PeerId> Stories::itemsChanged() const {
|
rpl::producer<PeerId> Stories::itemsChanged() const {
|
||||||
return _itemsChanged.events();
|
return _itemsChanged.events();
|
||||||
}
|
}
|
||||||
|
@ -1102,7 +1172,7 @@ void Stories::loadViewsSlice(
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
const base::flat_set<StoryId> &Stories::expiredMine() const {
|
const StoriesIds &Stories::expiredMine() const {
|
||||||
return _expiredMine;
|
return _expiredMine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1122,6 +1192,30 @@ bool Stories::expiredMineLoaded() const {
|
||||||
return _expiredMineLoaded;
|
return _expiredMineLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StoriesIds *Stories::saved(PeerId peerId) const {
|
||||||
|
const auto i = _saved.find(peerId);
|
||||||
|
return (i != end(_saved)) ? &i->second.ids : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<PeerId> Stories::savedChanged() const {
|
||||||
|
return _savedChanged.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Stories::savedCount(PeerId peerId) const {
|
||||||
|
const auto i = _saved.find(peerId);
|
||||||
|
return (i != end(_saved)) ? i->second.total : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Stories::savedCountKnown(PeerId peerId) const {
|
||||||
|
const auto i = _saved.find(peerId);
|
||||||
|
return (i != end(_saved)) && (i->second.total >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Stories::savedLoaded(PeerId peerId) const {
|
||||||
|
const auto i = _saved.find(peerId);
|
||||||
|
return (i != end(_saved)) && i->second.loaded;
|
||||||
|
}
|
||||||
|
|
||||||
void Stories::expiredMineLoadMore() {
|
void Stories::expiredMineLoadMore() {
|
||||||
if (_expiredMineRequestId) {
|
if (_expiredMineRequestId) {
|
||||||
return;
|
return;
|
||||||
|
@ -1136,33 +1230,81 @@ void Stories::expiredMineLoadMore() {
|
||||||
_expiredMineRequestId = 0;
|
_expiredMineRequestId = 0;
|
||||||
|
|
||||||
const auto &data = result.data();
|
const auto &data = result.data();
|
||||||
_expiredMineTotal = std::max(
|
|
||||||
data.vcount().v,
|
|
||||||
int(_expiredMine.size()));
|
|
||||||
_expiredMineLoaded = data.vstories().v.empty();
|
|
||||||
const auto self = _owner->session().user();
|
const auto self = _owner->session().user();
|
||||||
const auto now = base::unixtime::now();
|
const auto now = base::unixtime::now();
|
||||||
|
_expiredMineTotal = data.vcount().v;
|
||||||
for (const auto &story : data.vstories().v) {
|
for (const auto &story : data.vstories().v) {
|
||||||
const auto id = story.match([&](const auto &id) {
|
const auto id = story.match([&](const auto &id) {
|
||||||
return id.vid().v;
|
return id.vid().v;
|
||||||
});
|
});
|
||||||
_expiredMine.emplace(id);
|
_expiredMine.list.emplace(id);
|
||||||
_expiredMineLastId = id;
|
_expiredMineLastId = id;
|
||||||
if (!parseAndApply(self, story, now)) {
|
if (!parseAndApply(self, story, now)) {
|
||||||
_expiredMine.remove(id);
|
_expiredMine.list.remove(id);
|
||||||
if (_expiredMineTotal > 0) {
|
if (_expiredMineTotal > 0) {
|
||||||
--_expiredMineTotal;
|
--_expiredMineTotal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_expiredMineTotal = std::max(
|
||||||
|
_expiredMineTotal,
|
||||||
|
int(_expiredMine.list.size()));
|
||||||
|
_expiredMineLoaded = data.vstories().v.empty();
|
||||||
_expiredMineChanged.fire({});
|
_expiredMineChanged.fire({});
|
||||||
}).fail([=] {
|
}).fail([=] {
|
||||||
_expiredMineRequestId = 0;
|
_expiredMineRequestId = 0;
|
||||||
_expiredMineLoaded = true;
|
_expiredMineLoaded = true;
|
||||||
_expiredMineTotal = int(_expiredMine.size());
|
_expiredMineTotal = int(_expiredMine.list.size());
|
||||||
|
_expiredMineChanged.fire({});
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Stories::savedLoadMore(PeerId peerId) {
|
||||||
|
Expects(peerIsUser(peerId));
|
||||||
|
|
||||||
|
auto &saved = _saved[peerId];
|
||||||
|
if (saved.requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto api = &_owner->session().api();
|
||||||
|
const auto peer = _owner->peer(peerId);
|
||||||
|
saved.requestId = api->request(MTPstories_GetPinnedStories(
|
||||||
|
peer->asUser()->inputUser,
|
||||||
|
MTP_int(saved.lastId),
|
||||||
|
MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage)
|
||||||
|
)).done([=](const MTPstories_Stories &result) {
|
||||||
|
auto &saved = _saved[peerId];
|
||||||
|
saved.requestId = 0;
|
||||||
|
|
||||||
|
const auto &data = result.data();
|
||||||
|
const auto now = base::unixtime::now();
|
||||||
|
saved.total = data.vcount().v;
|
||||||
|
for (const auto &story : data.vstories().v) {
|
||||||
|
const auto id = story.match([&](const auto &id) {
|
||||||
|
return id.vid().v;
|
||||||
|
});
|
||||||
|
saved.ids.list.emplace(id);
|
||||||
|
saved.lastId = id;
|
||||||
|
if (!parseAndApply(peer, story, now)) {
|
||||||
|
saved.ids.list.remove(id);
|
||||||
|
if (saved.total > 0) {
|
||||||
|
--saved.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saved.total = std::max(saved.total, int(saved.ids.list.size()));
|
||||||
|
saved.loaded = data.vstories().v.empty();
|
||||||
|
_savedChanged.fire_copy(peerId);
|
||||||
|
}).fail([=] {
|
||||||
|
auto &saved = _saved[peerId];
|
||||||
|
saved.requestId = 0;
|
||||||
|
saved.loaded = true;
|
||||||
|
saved.total = int(saved.ids.list.size());
|
||||||
|
_savedChanged.fire_copy(peerId);
|
||||||
|
}).send();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
bool Stories::isQuitPrevent() {
|
bool Stories::isQuitPrevent() {
|
||||||
if (!_markReadPending.empty()) {
|
if (!_markReadPending.empty()) {
|
||||||
sendMarkAsReadRequests();
|
sendMarkAsReadRequests();
|
||||||
|
|
|
@ -39,6 +39,14 @@ struct StoryIdDates {
|
||||||
friend inline bool operator==(StoryIdDates, StoryIdDates) = default;
|
friend inline bool operator==(StoryIdDates, StoryIdDates) = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StoriesIds {
|
||||||
|
base::flat_set<StoryId, std::greater<>> list;
|
||||||
|
|
||||||
|
friend inline bool operator==(
|
||||||
|
const StoriesIds&,
|
||||||
|
const StoriesIds&) = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct StoryMedia {
|
struct StoryMedia {
|
||||||
std::variant<not_null<PhotoData*>, not_null<DocumentData*>> data;
|
std::variant<not_null<PhotoData*>, not_null<DocumentData*>> data;
|
||||||
|
|
||||||
|
@ -192,19 +200,24 @@ public:
|
||||||
|
|
||||||
void loadMore(StorySourcesList list);
|
void loadMore(StorySourcesList list);
|
||||||
void apply(const MTPDupdateStory &data);
|
void apply(const MTPDupdateStory &data);
|
||||||
|
void apply(not_null<PeerData*> peer, const MTPUserStories *data);
|
||||||
void loadAround(FullStoryId id, StoriesContext context);
|
void loadAround(FullStoryId id, StoriesContext context);
|
||||||
|
|
||||||
[[nodiscard]] const base::flat_map<PeerId, StoriesSource> &all() const;
|
const StoriesSource *source(PeerId id) const;
|
||||||
[[nodiscard]] const std::vector<StoriesSourceInfo> &sources(
|
[[nodiscard]] const std::vector<StoriesSourceInfo> &sources(
|
||||||
StorySourcesList list) const;
|
StorySourcesList list) const;
|
||||||
[[nodiscard]] bool sourcesLoaded(StorySourcesList list) const;
|
[[nodiscard]] bool sourcesLoaded(StorySourcesList list) const;
|
||||||
[[nodiscard]] rpl::producer<> sourcesChanged(
|
[[nodiscard]] rpl::producer<> sourcesChanged(
|
||||||
StorySourcesList list) const;
|
StorySourcesList list) const;
|
||||||
|
[[nodiscard]] rpl::producer<PeerId> sourceChanged() const;
|
||||||
[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;
|
[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;
|
||||||
|
|
||||||
[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
|
[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
|
||||||
FullStoryId id) const;
|
FullStoryId id) const;
|
||||||
void resolve(FullStoryId id, Fn<void()> done);
|
void resolve(FullStoryId id, Fn<void()> done);
|
||||||
|
[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(FullStoryId id);
|
||||||
|
[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(
|
||||||
|
not_null<Story*> story);
|
||||||
|
|
||||||
[[nodiscard]] bool isQuitPrevent();
|
[[nodiscard]] bool isQuitPrevent();
|
||||||
void markAsRead(FullStoryId id, bool viewed);
|
void markAsRead(FullStoryId id, bool viewed);
|
||||||
|
@ -217,14 +230,29 @@ public:
|
||||||
std::optional<StoryView> offset,
|
std::optional<StoryView> offset,
|
||||||
Fn<void(std::vector<StoryView>)> done);
|
Fn<void(std::vector<StoryView>)> done);
|
||||||
|
|
||||||
[[nodiscard]] const base::flat_set<StoryId> &expiredMine() const;
|
[[nodiscard]] const StoriesIds &expiredMine() const;
|
||||||
[[nodiscard]] rpl::producer<> expiredMineChanged() const;
|
[[nodiscard]] rpl::producer<> expiredMineChanged() const;
|
||||||
[[nodiscard]] int expiredMineCount() const;
|
[[nodiscard]] int expiredMineCount() const;
|
||||||
[[nodiscard]] bool expiredMineCountKnown() const;
|
[[nodiscard]] bool expiredMineCountKnown() const;
|
||||||
[[nodiscard]] bool expiredMineLoaded() const;
|
[[nodiscard]] bool expiredMineLoaded() const;
|
||||||
[[nodiscard]] void expiredMineLoadMore();
|
void expiredMineLoadMore();
|
||||||
|
|
||||||
|
[[nodiscard]] const StoriesIds *saved(PeerId peerId) const;
|
||||||
|
[[nodiscard]] rpl::producer<PeerId> savedChanged() const;
|
||||||
|
[[nodiscard]] int savedCount(PeerId peerId) const;
|
||||||
|
[[nodiscard]] bool savedCountKnown(PeerId peerId) const;
|
||||||
|
[[nodiscard]] bool savedLoaded(PeerId peerId) const;
|
||||||
|
void savedLoadMore(PeerId peerId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Saved {
|
||||||
|
StoriesIds ids;
|
||||||
|
int total = -1;
|
||||||
|
StoryId lastId = 0;
|
||||||
|
bool loaded = false;
|
||||||
|
mtpRequestId requestId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
void parseAndApply(const MTPUserStories &stories);
|
void parseAndApply(const MTPUserStories &stories);
|
||||||
[[nodiscard]] Story *parseAndApply(
|
[[nodiscard]] Story *parseAndApply(
|
||||||
not_null<PeerData*> peer,
|
not_null<PeerData*> peer,
|
||||||
|
@ -247,20 +275,25 @@ private:
|
||||||
void removeDependencyStory(not_null<Story*> story);
|
void removeDependencyStory(not_null<Story*> story);
|
||||||
void sort(StorySourcesList list);
|
void sort(StorySourcesList list);
|
||||||
|
|
||||||
void addToExpiredMine(not_null<Story*> story);
|
[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
|
||||||
|
not_null<Story*> story);
|
||||||
|
|
||||||
void sendMarkAsReadRequests();
|
void sendMarkAsReadRequests();
|
||||||
void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
|
void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
|
||||||
|
|
||||||
void requestUserStories(not_null<UserData*> user);
|
void requestUserStories(not_null<UserData*> user);
|
||||||
|
void addToExpiredMine(not_null<Story*> story);
|
||||||
void registerExpiring(TimeId expires, FullStoryId id);
|
void registerExpiring(TimeId expires, FullStoryId id);
|
||||||
void scheduleExpireTimer();
|
void scheduleExpireTimer();
|
||||||
void processExpired();
|
void processExpired();
|
||||||
|
|
||||||
const not_null<Session*> _owner;
|
const not_null<Session*> _owner;
|
||||||
base::flat_map<
|
std::unordered_map<
|
||||||
PeerId,
|
PeerId,
|
||||||
base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
|
base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
|
||||||
|
std::unordered_map<
|
||||||
|
PeerId,
|
||||||
|
base::flat_map<StoryId, std::weak_ptr<HistoryItem>>> _items;
|
||||||
base::flat_multi_map<TimeId, FullStoryId> _expiring;
|
base::flat_multi_map<TimeId, FullStoryId> _expiring;
|
||||||
base::flat_set<FullStoryId> _deleted;
|
base::flat_set<FullStoryId> _deleted;
|
||||||
base::Timer _expireTimer;
|
base::Timer _expireTimer;
|
||||||
|
@ -273,11 +306,11 @@ private:
|
||||||
PeerId,
|
PeerId,
|
||||||
base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolveSent;
|
base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolveSent;
|
||||||
|
|
||||||
std::map<
|
std::unordered_map<
|
||||||
not_null<Data::Story*>,
|
not_null<Data::Story*>,
|
||||||
base::flat_set<not_null<HistoryItem*>>> _dependentMessages;
|
base::flat_set<not_null<HistoryItem*>>> _dependentMessages;
|
||||||
|
|
||||||
base::flat_map<PeerId, StoriesSource> _all;
|
std::unordered_map<PeerId, StoriesSource> _all;
|
||||||
std::vector<StoriesSourceInfo> _sources[kStorySourcesListCount];
|
std::vector<StoriesSourceInfo> _sources[kStorySourcesListCount];
|
||||||
rpl::event_stream<> _sourcesChanged[kStorySourcesListCount];
|
rpl::event_stream<> _sourcesChanged[kStorySourcesListCount];
|
||||||
bool _sourcesLoaded[kStorySourcesListCount] = { false };
|
bool _sourcesLoaded[kStorySourcesListCount] = { false };
|
||||||
|
@ -285,15 +318,19 @@ private:
|
||||||
|
|
||||||
mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };
|
mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };
|
||||||
|
|
||||||
|
rpl::event_stream<PeerId> _sourceChanged;
|
||||||
rpl::event_stream<PeerId> _itemsChanged;
|
rpl::event_stream<PeerId> _itemsChanged;
|
||||||
|
|
||||||
base::flat_set<StoryId> _expiredMine;
|
StoriesIds _expiredMine;
|
||||||
int _expiredMineTotal = -1;
|
int _expiredMineTotal = -1;
|
||||||
StoryId _expiredMineLastId = 0;
|
StoryId _expiredMineLastId = 0;
|
||||||
bool _expiredMineLoaded = false;
|
bool _expiredMineLoaded = false;
|
||||||
rpl::event_stream<> _expiredMineChanged;
|
rpl::event_stream<> _expiredMineChanged;
|
||||||
mtpRequestId _expiredMineRequestId = 0;
|
mtpRequestId _expiredMineRequestId = 0;
|
||||||
|
|
||||||
|
std::unordered_map<PeerId, Saved> _saved;
|
||||||
|
rpl::event_stream<PeerId> _savedChanged;
|
||||||
|
|
||||||
base::flat_set<PeerId> _markReadPending;
|
base::flat_set<PeerId> _markReadPending;
|
||||||
base::Timer _markReadTimer;
|
base::Timer _markReadTimer;
|
||||||
base::flat_set<PeerId> _markReadRequests;
|
base::flat_set<PeerId> _markReadRequests;
|
||||||
|
|
142
Telegram/SourceFiles/data/data_stories_ids.cpp
Normal file
142
Telegram/SourceFiles/data/data_stories_ids.cpp
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
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_ids.h"
|
||||||
|
|
||||||
|
#include "data/data_peer.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_stories.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
rpl::producer<StoriesIdsSlice> SavedStoriesIds(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
StoryId aroundId,
|
||||||
|
int limit) {
|
||||||
|
return [=](auto consumer) {
|
||||||
|
auto lifetime = rpl::lifetime();
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
StoriesIdsSlice slice;
|
||||||
|
};
|
||||||
|
const auto state = lifetime.make_state<State>();
|
||||||
|
|
||||||
|
const auto push = [=] {
|
||||||
|
const auto stories = &peer->owner().stories();
|
||||||
|
if (!stories->savedCountKnown(peer->id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto source = stories->source(peer->id);
|
||||||
|
const auto saved = stories->saved(peer->id);
|
||||||
|
const auto count = stories->savedCount(peer->id);
|
||||||
|
Assert(saved != nullptr);
|
||||||
|
auto ids = base::flat_set<StoryId>();
|
||||||
|
ids.reserve(saved->list.size() + 1);
|
||||||
|
auto total = count;
|
||||||
|
if (source && !source->ids.empty()) {
|
||||||
|
++total;
|
||||||
|
const auto current = source->ids.front().id;
|
||||||
|
for (const auto id : ranges::views::reverse(saved->list)) {
|
||||||
|
const auto i = source->ids.lower_bound(
|
||||||
|
StoryIdDates{ id });
|
||||||
|
if (i != end(source->ids) && i->id == id) {
|
||||||
|
--total;
|
||||||
|
} else {
|
||||||
|
ids.emplace(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids.emplace(current);
|
||||||
|
} else {
|
||||||
|
auto all = saved->list | ranges::views::reverse;
|
||||||
|
ids = { begin(all), end(all) };
|
||||||
|
}
|
||||||
|
const auto added = int(ids.size());
|
||||||
|
state->slice = StoriesIdsSlice(
|
||||||
|
std::move(ids),
|
||||||
|
total,
|
||||||
|
0,
|
||||||
|
total - added);
|
||||||
|
consumer.put_next_copy(state->slice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto stories = &peer->owner().stories();
|
||||||
|
stories->sourceChanged(
|
||||||
|
) | rpl::filter(
|
||||||
|
rpl::mappers::_1 == peer->id
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
push();
|
||||||
|
}, lifetime);
|
||||||
|
|
||||||
|
stories->savedChanged(
|
||||||
|
) | rpl::filter(
|
||||||
|
rpl::mappers::_1 == peer->id
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
push();
|
||||||
|
}, lifetime);
|
||||||
|
|
||||||
|
if (!stories->savedCountKnown(peer->id)) {
|
||||||
|
stories->savedLoadMore(peer->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
push();
|
||||||
|
|
||||||
|
return lifetime;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
StoryId aroundId,
|
||||||
|
int limit) {
|
||||||
|
return [=](auto consumer) {
|
||||||
|
auto lifetime = rpl::lifetime();
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
StoriesIdsSlice slice;
|
||||||
|
};
|
||||||
|
const auto state = lifetime.make_state<State>();
|
||||||
|
|
||||||
|
const auto push = [=] {
|
||||||
|
const auto stories = &session->data().stories();
|
||||||
|
if (!stories->expiredMineCountKnown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto expired = stories->expiredMine();
|
||||||
|
const auto count = stories->expiredMineCount();
|
||||||
|
auto ids = base::flat_set<StoryId>();
|
||||||
|
ids.reserve(expired.list.size() + 1);
|
||||||
|
auto all = expired.list | ranges::views::reverse;
|
||||||
|
ids = { begin(all), end(all) };
|
||||||
|
const auto added = int(ids.size());
|
||||||
|
state->slice = StoriesIdsSlice(
|
||||||
|
std::move(ids),
|
||||||
|
count,
|
||||||
|
0,
|
||||||
|
count - added);
|
||||||
|
consumer.put_next_copy(state->slice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto stories = &session->data().stories();
|
||||||
|
stories->expiredMineChanged(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
push();
|
||||||
|
}, lifetime);
|
||||||
|
|
||||||
|
if (!stories->expiredMineCountKnown()) {
|
||||||
|
stories->expiredMineLoadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
push();
|
||||||
|
|
||||||
|
return lifetime;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Data
|
32
Telegram/SourceFiles/data/data_stories_ids.h
Normal file
32
Telegram/SourceFiles/data/data_stories_ids.h
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
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 "data/data_abstract_sparse_ids.h"
|
||||||
|
|
||||||
|
class PeerData;
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
using StoriesIdsSlice = AbstractSparseIds<base::flat_set<StoryId>>;
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<StoriesIdsSlice> SavedStoriesIds(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
StoryId aroundId,
|
||||||
|
int limit);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
StoryId aroundId,
|
||||||
|
int limit);
|
||||||
|
|
||||||
|
} // namespace Data
|
|
@ -301,6 +301,8 @@ enum class MessageFlag : uint64 {
|
||||||
|
|
||||||
// Fake message with bot cover and information.
|
// Fake message with bot cover and information.
|
||||||
FakeBotAbout = (1ULL << 36),
|
FakeBotAbout = (1ULL << 36),
|
||||||
|
|
||||||
|
StoryItem = (1ULL << 37),
|
||||||
};
|
};
|
||||||
inline constexpr bool is_flag_type(MessageFlag) { return true; }
|
inline constexpr bool is_flag_type(MessageFlag) { return true; }
|
||||||
using MessageFlags = base::flags<MessageFlag>;
|
using MessageFlags = base::flags<MessageFlag>;
|
||||||
|
|
|
@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
#include "data/data_peer_bot_command.h"
|
#include "data/data_peer_bot_command.h"
|
||||||
#include "data/data_photo.h"
|
#include "data/data_photo.h"
|
||||||
|
#include "data/data_stories.h"
|
||||||
#include "data/data_emoji_statuses.h"
|
#include "data/data_emoji_statuses.h"
|
||||||
#include "data/data_user_names.h"
|
#include "data/data_user_names.h"
|
||||||
#include "data/data_wall_paper.h"
|
#include "data/data_wall_paper.h"
|
||||||
|
@ -474,6 +475,8 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
|
||||||
user->setWallPaper({});
|
user->setWallPaper({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user->owner().stories().apply(user, update.vstories());
|
||||||
|
|
||||||
user->fullUpdated();
|
user->fullUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,16 +128,14 @@ State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
|
||||||
|
|
||||||
Content State::next() {
|
Content State::next() {
|
||||||
auto result = Content{ .full = (_list == Data::StorySourcesList::All) };
|
auto result = Content{ .full = (_list == Data::StorySourcesList::All) };
|
||||||
const auto &all = _data->all();
|
|
||||||
const auto &sources = _data->sources(_list);
|
const auto &sources = _data->sources(_list);
|
||||||
result.users.reserve(sources.size());
|
result.users.reserve(sources.size());
|
||||||
for (const auto &info : sources) {
|
for (const auto &info : sources) {
|
||||||
const auto i = all.find(info.id);
|
const auto source = _data->source(info.id);
|
||||||
Assert(i != end(all));
|
Assert(source != nullptr);
|
||||||
const auto &source = i->second;
|
|
||||||
|
|
||||||
auto userpic = std::shared_ptr<Userpic>();
|
auto userpic = std::shared_ptr<Userpic>();
|
||||||
const auto user = source.user;
|
const auto user = source->user;
|
||||||
if (const auto i = _userpics.find(user); i != end(_userpics)) {
|
if (const auto i = _userpics.find(user); i != end(_userpics)) {
|
||||||
userpic = i->second;
|
userpic = i->second;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -954,6 +954,8 @@ Media ParseMedia(
|
||||||
result.content = ParsePoll(data);
|
result.content = ParsePoll(data);
|
||||||
}, [](const MTPDmessageMediaDice &data) {
|
}, [](const MTPDmessageMediaDice &data) {
|
||||||
// #TODO dice
|
// #TODO dice
|
||||||
|
}, [](const MTPDmessageMediaStory &data) {
|
||||||
|
// #TODO stories export
|
||||||
}, [](const MTPDmessageMediaEmpty &data) {});
|
}, [](const MTPDmessageMediaEmpty &data) {});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_group_call.h" // Data::GroupCall::id().
|
#include "data/data_group_call.h" // Data::GroupCall::id().
|
||||||
#include "data/data_poll.h" // PollData::publicVotes.
|
#include "data/data_poll.h" // PollData::publicVotes.
|
||||||
#include "data/data_sponsored_messages.h"
|
#include "data/data_sponsored_messages.h"
|
||||||
|
#include "data/data_stories.h"
|
||||||
#include "data/data_wall_paper.h"
|
#include "data/data_wall_paper.h"
|
||||||
#include "data/data_web_page.h"
|
#include "data/data_web_page.h"
|
||||||
#include "chat_helpers/stickers_gift_box_pack.h"
|
#include "chat_helpers/stickers_gift_box_pack.h"
|
||||||
|
@ -289,6 +290,8 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
|
||||||
item,
|
item,
|
||||||
qs(media.vemoticon()),
|
qs(media.vemoticon()),
|
||||||
media.vvalue().v);
|
media.vvalue().v);
|
||||||
|
}, [&](const MTPDmessageMediaStory &media) -> Result {
|
||||||
|
return nullptr; // #TODO stories
|
||||||
}, [](const MTPDmessageMediaEmpty &) -> Result {
|
}, [](const MTPDmessageMediaEmpty &) -> Result {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}, [](const MTPDmessageMediaUnsupported &) -> Result {
|
}, [](const MTPDmessageMediaUnsupported &) -> Result {
|
||||||
|
@ -673,6 +676,17 @@ HistoryItem::HistoryItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HistoryItem::HistoryItem(
|
||||||
|
not_null<History*> history,
|
||||||
|
not_null<Data::Story*> story)
|
||||||
|
: id(StoryIdToMsgId(story->id()))
|
||||||
|
, _history(history)
|
||||||
|
, _from(history->peer)
|
||||||
|
, _flags(MessageFlag::Local | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem | MessageFlag::StoryItem)
|
||||||
|
, _date(story->date()) {
|
||||||
|
setStoryFields(story);
|
||||||
|
}
|
||||||
|
|
||||||
HistoryItem::~HistoryItem() {
|
HistoryItem::~HistoryItem() {
|
||||||
_media = nullptr;
|
_media = nullptr;
|
||||||
clearSavedMedia();
|
clearSavedMedia();
|
||||||
|
@ -1491,6 +1505,31 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
|
||||||
finishEdition(keyboardTop);
|
finishEdition(keyboardTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HistoryItem::applyChanges(not_null<Data::Story*> story) {
|
||||||
|
Expects(_flags & MessageFlag::StoryItem);
|
||||||
|
Expects(StoryIdFromMsgId(id) == story->id());
|
||||||
|
|
||||||
|
_media = nullptr;
|
||||||
|
setStoryFields(story);
|
||||||
|
|
||||||
|
finishEdition(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HistoryItem::setStoryFields(not_null<Data::Story*> story) {
|
||||||
|
const auto spoiler = false;
|
||||||
|
if (const auto photo = story->photo()) {
|
||||||
|
_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
|
||||||
|
} else {
|
||||||
|
const auto document = story->document();
|
||||||
|
_media = std::make_unique<Data::MediaFile>(
|
||||||
|
this,
|
||||||
|
document,
|
||||||
|
/*skipPremiumEffect=*/false,
|
||||||
|
spoiler);
|
||||||
|
}
|
||||||
|
setText(story->caption());
|
||||||
|
}
|
||||||
|
|
||||||
void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
||||||
if (message.vaction().type() == mtpc_messageActionHistoryClear) {
|
if (message.vaction().type() == mtpc_messageActionHistoryClear) {
|
||||||
const auto wasGrouped = history()->owner().groups().isGrouped(this);
|
const auto wasGrouped = history()->owner().groups().isGrouped(this);
|
||||||
|
|
|
@ -179,6 +179,7 @@ public:
|
||||||
const QString &postAuthor,
|
const QString &postAuthor,
|
||||||
not_null<GameData*> game,
|
not_null<GameData*> game,
|
||||||
HistoryMessageMarkupData &&markup);
|
HistoryMessageMarkupData &&markup);
|
||||||
|
HistoryItem(not_null<History*> history, not_null<Data::Story*> story);
|
||||||
~HistoryItem();
|
~HistoryItem();
|
||||||
|
|
||||||
struct Destroyer {
|
struct Destroyer {
|
||||||
|
@ -332,6 +333,7 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool isService() const;
|
[[nodiscard]] bool isService() const;
|
||||||
void applyEdition(HistoryMessageEdition &&edition);
|
void applyEdition(HistoryMessageEdition &&edition);
|
||||||
|
void applyChanges(not_null<Data::Story*> story);
|
||||||
|
|
||||||
void applyEdition(const MTPDmessageService &message);
|
void applyEdition(const MTPDmessageService &message);
|
||||||
void applyEdition(const MTPMessageExtendedMedia &media);
|
void applyEdition(const MTPMessageExtendedMedia &media);
|
||||||
|
@ -559,6 +561,7 @@ private:
|
||||||
bool updateServiceDependent(bool force = false);
|
bool updateServiceDependent(bool force = false);
|
||||||
void setServiceText(PreparedServiceText &&prepared);
|
void setServiceText(PreparedServiceText &&prepared);
|
||||||
|
|
||||||
|
void setStoryFields(not_null<Data::Story*> story);
|
||||||
void finishEdition(int oldKeyboardTop);
|
void finishEdition(int oldKeyboardTop);
|
||||||
void finishEditionToEmpty();
|
void finishEditionToEmpty();
|
||||||
|
|
||||||
|
|
|
@ -443,6 +443,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) {
|
||||||
return Result::Good;
|
return Result::Good;
|
||||||
}, [](const MTPDmessageMediaDice &) {
|
}, [](const MTPDmessageMediaDice &) {
|
||||||
return Result::Good;
|
return Result::Good;
|
||||||
|
}, [](const MTPDmessageMediaStory &data) {
|
||||||
|
return Result::Good;
|
||||||
}, [](const MTPDmessageMediaUnsupported &) {
|
}, [](const MTPDmessageMediaUnsupported &) {
|
||||||
return Result::Unsupported;
|
return Result::Unsupported;
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,7 +26,7 @@ Memento::Memento(not_null<Controller*> controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
Memento::Memento(not_null<UserData*> self)
|
Memento::Memento(not_null<UserData*> self)
|
||||||
: ContentMemento(Downloads::Tag{})
|
: ContentMemento(Tag{})
|
||||||
, _media(self, 0, Media::Type::File) {
|
, _media(self, 0, Media::Type::File) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -332,6 +332,8 @@ Key ContentMemento::key() const {
|
||||||
return Key(poll, pollContextId());
|
return Key(poll, pollContextId());
|
||||||
} else if (const auto self = settingsSelf()) {
|
} else if (const auto self = settingsSelf()) {
|
||||||
return Settings::Tag{ self };
|
return Settings::Tag{ self };
|
||||||
|
} else if (const auto peer = storiesPeer()) {
|
||||||
|
return Stories::Tag{ peer, storiesTab() };
|
||||||
} else {
|
} else {
|
||||||
return Downloads::Tag();
|
return Downloads::Tag();
|
||||||
}
|
}
|
||||||
|
@ -363,4 +365,9 @@ ContentMemento::ContentMemento(Settings::Tag settings)
|
||||||
ContentMemento::ContentMemento(Downloads::Tag downloads) {
|
ContentMemento::ContentMemento(Downloads::Tag downloads) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentMemento::ContentMemento(Stories::Tag stories)
|
||||||
|
: _storiesPeer(stories.peer)
|
||||||
|
, _storiesTab(stories.tab) {
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Info
|
} // namespace Info
|
||||||
|
|
|
@ -24,14 +24,20 @@ template <typename Widget>
|
||||||
class PaddingWrap;
|
class PaddingWrap;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Info {
|
namespace Info::Settings {
|
||||||
namespace Settings {
|
|
||||||
struct Tag;
|
struct Tag;
|
||||||
} // namespace Settings
|
} // namespace Info::Settings
|
||||||
|
|
||||||
namespace Downloads {
|
namespace Info::Downloads {
|
||||||
struct Tag;
|
struct Tag;
|
||||||
} // namespace Downloads
|
} // namespace Info::Downloads
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
struct Tag;
|
||||||
|
enum class Tab;
|
||||||
|
} // namespace Info::Stories
|
||||||
|
|
||||||
|
namespace Info {
|
||||||
|
|
||||||
class ContentMemento;
|
class ContentMemento;
|
||||||
class Controller;
|
class Controller;
|
||||||
|
@ -150,6 +156,7 @@ public:
|
||||||
PeerId migratedPeerId);
|
PeerId migratedPeerId);
|
||||||
explicit ContentMemento(Settings::Tag settings);
|
explicit ContentMemento(Settings::Tag settings);
|
||||||
explicit ContentMemento(Downloads::Tag downloads);
|
explicit ContentMemento(Downloads::Tag downloads);
|
||||||
|
explicit ContentMemento(Stories::Tag stories);
|
||||||
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
|
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
|
||||||
: _poll(poll)
|
: _poll(poll)
|
||||||
, _pollContextId(contextId) {
|
, _pollContextId(contextId) {
|
||||||
|
@ -172,6 +179,12 @@ public:
|
||||||
UserData *settingsSelf() const {
|
UserData *settingsSelf() const {
|
||||||
return _settingsSelf;
|
return _settingsSelf;
|
||||||
}
|
}
|
||||||
|
PeerData *storiesPeer() const {
|
||||||
|
return _storiesPeer;
|
||||||
|
}
|
||||||
|
Stories::Tab storiesTab() const {
|
||||||
|
return _storiesTab;
|
||||||
|
}
|
||||||
PollData *poll() const {
|
PollData *poll() const {
|
||||||
return _poll;
|
return _poll;
|
||||||
}
|
}
|
||||||
|
@ -214,6 +227,8 @@ private:
|
||||||
const PeerId _migratedPeerId = 0;
|
const PeerId _migratedPeerId = 0;
|
||||||
Data::ForumTopic *_topic = nullptr;
|
Data::ForumTopic *_topic = nullptr;
|
||||||
UserData * const _settingsSelf = nullptr;
|
UserData * const _settingsSelf = nullptr;
|
||||||
|
PeerData * const _storiesPeer = nullptr;
|
||||||
|
Stories::Tab _storiesTab = {};
|
||||||
PollData * const _poll = nullptr;
|
PollData * const _poll = nullptr;
|
||||||
const FullMsgId _pollContextId;
|
const FullMsgId _pollContextId;
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,9 @@ Key::Key(Settings::Tag settings) : _value(settings) {
|
||||||
Key::Key(Downloads::Tag downloads) : _value(downloads) {
|
Key::Key(Downloads::Tag downloads) : _value(downloads) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Key::Key(Stories::Tag stories) : _value(stories) {
|
||||||
|
}
|
||||||
|
|
||||||
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
|
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
|
||||||
: _value(PollKey{ poll, contextId }) {
|
: _value(PollKey{ poll, contextId }) {
|
||||||
}
|
}
|
||||||
|
@ -72,6 +75,20 @@ bool Key::isDownloads() const {
|
||||||
return v::is<Downloads::Tag>(_value);
|
return v::is<Downloads::Tag>(_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PeerData *Key::storiesPeer() const {
|
||||||
|
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
|
||||||
|
return tag->peer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stories::Tab Key::storiesTab() const {
|
||||||
|
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
|
||||||
|
return tag->tab;
|
||||||
|
}
|
||||||
|
return Stories::Tab();
|
||||||
|
}
|
||||||
|
|
||||||
PollData *Key::poll() const {
|
PollData *Key::poll() const {
|
||||||
if (const auto data = std::get_if<PollKey>(&_value)) {
|
if (const auto data = std::get_if<PollKey>(&_value)) {
|
||||||
return data->poll;
|
return data->poll;
|
||||||
|
|
|
@ -36,6 +36,25 @@ struct Tag {
|
||||||
|
|
||||||
} // namespace Info::Downloads
|
} // namespace Info::Downloads
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
enum class Tab {
|
||||||
|
Saved,
|
||||||
|
Archive,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Tag {
|
||||||
|
explicit Tag(not_null<PeerData*> peer, Tab tab = {})
|
||||||
|
: peer(peer)
|
||||||
|
, tab(tab) {
|
||||||
|
}
|
||||||
|
|
||||||
|
not_null<PeerData*> peer;
|
||||||
|
Tab tab = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
||||||
|
|
||||||
namespace Info {
|
namespace Info {
|
||||||
|
|
||||||
class Key {
|
class Key {
|
||||||
|
@ -44,12 +63,15 @@ public:
|
||||||
explicit Key(not_null<Data::ForumTopic*> topic);
|
explicit Key(not_null<Data::ForumTopic*> topic);
|
||||||
Key(Settings::Tag settings);
|
Key(Settings::Tag settings);
|
||||||
Key(Downloads::Tag downloads);
|
Key(Downloads::Tag downloads);
|
||||||
|
Key(Stories::Tag stories);
|
||||||
Key(not_null<PollData*> poll, FullMsgId contextId);
|
Key(not_null<PollData*> poll, FullMsgId contextId);
|
||||||
|
|
||||||
PeerData *peer() const;
|
PeerData *peer() const;
|
||||||
Data::ForumTopic *topic() const;
|
Data::ForumTopic *topic() const;
|
||||||
UserData *settingsSelf() const;
|
UserData *settingsSelf() const;
|
||||||
bool isDownloads() const;
|
bool isDownloads() const;
|
||||||
|
PeerData *storiesPeer() const;
|
||||||
|
Stories::Tab storiesTab() const;
|
||||||
PollData *poll() const;
|
PollData *poll() const;
|
||||||
FullMsgId pollContextId() const;
|
FullMsgId pollContextId() const;
|
||||||
|
|
||||||
|
@ -63,6 +85,7 @@ private:
|
||||||
not_null<Data::ForumTopic*>,
|
not_null<Data::ForumTopic*>,
|
||||||
Settings::Tag,
|
Settings::Tag,
|
||||||
Downloads::Tag,
|
Downloads::Tag,
|
||||||
|
Stories::Tag,
|
||||||
PollKey> _value;
|
PollKey> _value;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -81,6 +104,7 @@ public:
|
||||||
Members,
|
Members,
|
||||||
Settings,
|
Settings,
|
||||||
Downloads,
|
Downloads,
|
||||||
|
Stories,
|
||||||
PollResults,
|
PollResults,
|
||||||
};
|
};
|
||||||
using SettingsType = ::Settings::Type;
|
using SettingsType = ::Settings::Type;
|
||||||
|
@ -123,23 +147,29 @@ class AbstractController : public Window::SessionNavigation {
|
||||||
public:
|
public:
|
||||||
AbstractController(not_null<Window::SessionController*> parent);
|
AbstractController(not_null<Window::SessionController*> parent);
|
||||||
|
|
||||||
virtual Key key() const = 0;
|
[[nodiscard]] virtual Key key() const = 0;
|
||||||
virtual PeerData *migrated() const = 0;
|
[[nodiscard]] virtual PeerData *migrated() const = 0;
|
||||||
virtual Section section() const = 0;
|
[[nodiscard]] virtual Section section() const = 0;
|
||||||
|
|
||||||
PeerData *peer() const;
|
[[nodiscard]] PeerData *peer() const;
|
||||||
PeerId migratedPeerId() const;
|
[[nodiscard]] PeerId migratedPeerId() const;
|
||||||
Data::ForumTopic *topic() const {
|
[[nodiscard]] Data::ForumTopic *topic() const {
|
||||||
return key().topic();
|
return key().topic();
|
||||||
}
|
}
|
||||||
UserData *settingsSelf() const {
|
[[nodiscard]] UserData *settingsSelf() const {
|
||||||
return key().settingsSelf();
|
return key().settingsSelf();
|
||||||
}
|
}
|
||||||
bool isDownloads() const {
|
[[nodiscard]] bool isDownloads() const {
|
||||||
return key().isDownloads();
|
return key().isDownloads();
|
||||||
}
|
}
|
||||||
PollData *poll() const;
|
[[nodiscard]] PeerData *storiesPeer() const {
|
||||||
FullMsgId pollContextId() const {
|
return key().storiesPeer();
|
||||||
|
}
|
||||||
|
[[nodiscard]] Stories::Tab storiesTab() const {
|
||||||
|
return key().storiesTab();
|
||||||
|
}
|
||||||
|
[[nodiscard]] PollData *poll() const;
|
||||||
|
[[nodiscard]] FullMsgId pollContextId() const {
|
||||||
return key().pollContextId();
|
return key().pollContextId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include <rpl/mappers.h>
|
#include <rpl/mappers.h>
|
||||||
#include <rpl/map.h>
|
#include <rpl/map.h>
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
|
#include "data/data_stories_ids.h"
|
||||||
#include "storage/storage_shared_media.h"
|
#include "storage/storage_shared_media.h"
|
||||||
#include "info/info_memento.h"
|
#include "info/info_memento.h"
|
||||||
#include "info/info_controller.h"
|
#include "info/info_controller.h"
|
||||||
#include "info/profile/info_profile_values.h"
|
#include "info/profile/info_profile_values.h"
|
||||||
|
#include "info/stories/info_stories_widget.h"
|
||||||
#include "ui/wrap/slide_wrap.h"
|
#include "ui/wrap/slide_wrap.h"
|
||||||
#include "ui/wrap/vertical_layout.h"
|
#include "ui/wrap/vertical_layout.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
|
@ -124,4 +126,29 @@ inline auto AddCommonGroupsButton(
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline auto AddStoriesButton(
|
||||||
|
Ui::VerticalLayout *parent,
|
||||||
|
not_null<Window::SessionNavigation*> navigation,
|
||||||
|
not_null<UserData*> user,
|
||||||
|
Ui::MultiSlideTracker &tracker) {
|
||||||
|
auto count = Data::SavedStoriesIds(
|
||||||
|
user,
|
||||||
|
ServerMaxStoryId - 1,
|
||||||
|
0
|
||||||
|
) | rpl::map([](const Data::StoriesIdsSlice &slice) {
|
||||||
|
return slice.fullCount();
|
||||||
|
}) | rpl::filter_optional();
|
||||||
|
auto result = AddCountedButton(
|
||||||
|
parent,
|
||||||
|
std::move(count),
|
||||||
|
[](int count) {
|
||||||
|
return tr::lng_stories_row_count(tr::now, lt_count, count);
|
||||||
|
},
|
||||||
|
tracker)->entity();
|
||||||
|
result->addClickHandler([=] {
|
||||||
|
navigation->showSection(Info::Stories::Make(user));
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Info::Media
|
} // namespace Info::Media
|
||||||
|
|
|
@ -338,6 +338,7 @@ void ListSection::resizeToWidth(int newWidth) {
|
||||||
switch (_type) {
|
switch (_type) {
|
||||||
case Type::Photo:
|
case Type::Photo:
|
||||||
case Type::Video:
|
case Type::Video:
|
||||||
|
case Type::PhotoVideo: // #TODO stories
|
||||||
case Type::RoundFile: {
|
case Type::RoundFile: {
|
||||||
_itemsLeft = st::infoMediaSkip;
|
_itemsLeft = st::infoMediaSkip;
|
||||||
_itemsTop = st::infoMediaSkip;
|
_itemsTop = st::infoMediaSkip;
|
||||||
|
@ -375,6 +376,7 @@ int ListSection::recountHeight() {
|
||||||
switch (_type) {
|
switch (_type) {
|
||||||
case Type::Photo:
|
case Type::Photo:
|
||||||
case Type::Video:
|
case Type::Video:
|
||||||
|
case Type::PhotoVideo: // #TODO stories
|
||||||
case Type::RoundFile: {
|
case Type::RoundFile: {
|
||||||
auto itemHeight = _itemWidth + st::infoMediaSkip;
|
auto itemHeight = _itemWidth + st::infoMediaSkip;
|
||||||
auto index = 0;
|
auto index = 0;
|
||||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "info/media/info_media_provider.h"
|
#include "info/media/info_media_provider.h"
|
||||||
#include "info/media/info_media_list_section.h"
|
#include "info/media/info_media_list_section.h"
|
||||||
#include "info/downloads/info_downloads_provider.h"
|
#include "info/downloads/info_downloads_provider.h"
|
||||||
|
#include "info/stories/info_stories_provider.h"
|
||||||
#include "info/info_controller.h"
|
#include "info/info_controller.h"
|
||||||
#include "layout/layout_mosaic.h"
|
#include "layout/layout_mosaic.h"
|
||||||
#include "layout/layout_selection.h"
|
#include "layout/layout_selection.h"
|
||||||
|
@ -88,6 +89,8 @@ struct ListWidget::DateBadge {
|
||||||
not_null<AbstractController*> controller) {
|
not_null<AbstractController*> controller) {
|
||||||
if (controller->isDownloads()) {
|
if (controller->isDownloads()) {
|
||||||
return std::make_unique<Downloads::Provider>(controller);
|
return std::make_unique<Downloads::Provider>(controller);
|
||||||
|
} else if (controller->storiesPeer()) {
|
||||||
|
return std::make_unique<Stories::Provider>(controller);
|
||||||
}
|
}
|
||||||
return std::make_unique<Provider>(controller);
|
return std::make_unique<Provider>(controller);
|
||||||
}
|
}
|
||||||
|
@ -126,6 +129,7 @@ ListWidget::DateBadge::DateBadge(
|
||||||
, hideTimer(std::move(hideCallback))
|
, hideTimer(std::move(hideCallback))
|
||||||
, goodType(type == Type::Photo
|
, goodType(type == Type::Photo
|
||||||
|| type == Type::Video
|
|| type == Type::Video
|
||||||
|
|| type == Type::PhotoVideo
|
||||||
|| type == Type::GIF) {
|
|| type == Type::GIF) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +175,9 @@ void ListWidget::start() {
|
||||||
) | rpl::start_with_next([this](QString &&query) {
|
) | rpl::start_with_next([this](QString &&query) {
|
||||||
_provider->setSearchQuery(std::move(query));
|
_provider->setSearchQuery(std::move(query));
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
} else if (_controller->storiesPeer()) {
|
||||||
|
trackSession(&session());
|
||||||
|
restart();
|
||||||
} else {
|
} else {
|
||||||
trackSession(&session());
|
trackSession(&session());
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,6 @@ private:
|
||||||
void itemRemoved(not_null<const HistoryItem*> item);
|
void itemRemoved(not_null<const HistoryItem*> item);
|
||||||
void itemLayoutChanged(not_null<const HistoryItem*> item);
|
void itemLayoutChanged(not_null<const HistoryItem*> item);
|
||||||
|
|
||||||
void refreshViewer();
|
|
||||||
void refreshRows();
|
void refreshRows();
|
||||||
void trackSession(not_null<Main::Session*> session);
|
void trackSession(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,15 @@ Memento::Memento(not_null<Controller*> controller)
|
||||||
: Memento(
|
: Memento(
|
||||||
(controller->peer()
|
(controller->peer()
|
||||||
? controller->peer()
|
? controller->peer()
|
||||||
|
: controller->storiesPeer()
|
||||||
|
? controller->storiesPeer()
|
||||||
: controller->parentController()->session().user()),
|
: controller->parentController()->session().user()),
|
||||||
controller->topic(),
|
controller->topic(),
|
||||||
controller->migratedPeerId(),
|
controller->migratedPeerId(),
|
||||||
(controller->section().type() == Section::Type::Downloads
|
(controller->section().type() == Section::Type::Downloads
|
||||||
? Type::File
|
? Type::File
|
||||||
|
: controller->section().type() == Section::Type::Stories
|
||||||
|
? Type::PhotoVideo
|
||||||
: controller->section().mediaType())) {
|
: controller->section().mediaType())) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -186,7 +186,23 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
|
||||||
icon,
|
icon,
|
||||||
st::infoSharedMediaButtonIconPosition);
|
st::infoSharedMediaButtonIconPosition);
|
||||||
};
|
};
|
||||||
|
auto addStoriesButton = [&](
|
||||||
|
not_null<UserData*> user,
|
||||||
|
const style::icon &icon) {
|
||||||
|
auto result = Media::AddStoriesButton(
|
||||||
|
content,
|
||||||
|
_controller,
|
||||||
|
user,
|
||||||
|
tracker);
|
||||||
|
object_ptr<Profile::FloatingIcon>(
|
||||||
|
result,
|
||||||
|
icon,
|
||||||
|
st::infoSharedMediaButtonIconPosition);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto user = _peer->asUser()) {
|
||||||
|
addStoriesButton(user, st::infoIconMediaGroup);
|
||||||
|
}
|
||||||
addMediaButton(MediaType::Photo, st::infoIconMediaPhoto);
|
addMediaButton(MediaType::Photo, st::infoIconMediaPhoto);
|
||||||
addMediaButton(MediaType::Video, st::infoIconMediaVideo);
|
addMediaButton(MediaType::Video, st::infoIconMediaVideo);
|
||||||
addMediaButton(MediaType::File, st::infoIconMediaFile);
|
addMediaButton(MediaType::File, st::infoIconMediaFile);
|
||||||
|
|
191
Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
Normal file
191
Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
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 "info/stories/info_stories_inner_widget.h"
|
||||||
|
|
||||||
|
#include "info/stories/info_stories_widget.h"
|
||||||
|
#include "info/media/info_media_list_widget.h"
|
||||||
|
#include "info/info_controller.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "styles/style_info.h"
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
class EmptyWidget : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
EmptyWidget(QWidget *parent);
|
||||||
|
|
||||||
|
void setFullHeight(rpl::producer<int> fullHeightValue);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int resizeGetHeight(int newWidth) override;
|
||||||
|
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
object_ptr<Ui::FlatLabel> _text;
|
||||||
|
int _height = 0;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
EmptyWidget::EmptyWidget(QWidget *parent)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _text(this, st::infoEmptyLabel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {
|
||||||
|
std::move(
|
||||||
|
fullHeightValue
|
||||||
|
) | rpl::start_with_next([this](int fullHeight) {
|
||||||
|
// Make icon center be on 1/3 height.
|
||||||
|
auto iconCenter = fullHeight / 3;
|
||||||
|
auto iconHeight = st::infoEmptyFile.height();
|
||||||
|
auto iconTop = iconCenter - iconHeight / 2;
|
||||||
|
_height = iconTop + st::infoEmptyIconTop;
|
||||||
|
resizeToWidth(width());
|
||||||
|
}, lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmptyWidget::resizeGetHeight(int newWidth) {
|
||||||
|
auto labelTop = _height - st::infoEmptyLabelTop;
|
||||||
|
auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;
|
||||||
|
_text->resizeToNaturalWidth(labelWidth);
|
||||||
|
|
||||||
|
auto labelLeft = (newWidth - _text->width()) / 2;
|
||||||
|
_text->moveToLeft(labelLeft, labelTop, newWidth);
|
||||||
|
|
||||||
|
update();
|
||||||
|
return _height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmptyWidget::paintEvent(QPaintEvent *e) {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
|
||||||
|
const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2;
|
||||||
|
const auto iconTop = height() - st::infoEmptyIconTop;
|
||||||
|
st::infoEmptyFile.paint(p, iconLeft, iconTop, width());
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerWidget::InnerWidget(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Controller*> controller)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _controller(controller)
|
||||||
|
, _empty(this) {
|
||||||
|
_empty->heightValue(
|
||||||
|
) | rpl::start_with_next(
|
||||||
|
[this] { refreshHeight(); },
|
||||||
|
_empty->lifetime());
|
||||||
|
_list = setupList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::visibleTopBottomUpdated(
|
||||||
|
int visibleTop,
|
||||||
|
int visibleBottom) {
|
||||||
|
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InnerWidget::showInternal(not_null<Memento*> memento) {
|
||||||
|
if (memento->section().type() == Section::Type::Stories) {
|
||||||
|
restoreState(memento);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
object_ptr<Media::ListWidget> InnerWidget::setupList() {
|
||||||
|
auto result = object_ptr<Media::ListWidget>(
|
||||||
|
this,
|
||||||
|
_controller);
|
||||||
|
result->heightValue(
|
||||||
|
) | rpl::start_with_next(
|
||||||
|
[this] { refreshHeight(); },
|
||||||
|
result->lifetime());
|
||||||
|
using namespace rpl::mappers;
|
||||||
|
result->scrollToRequests(
|
||||||
|
) | rpl::map([widget = result.data()](int to) {
|
||||||
|
return Ui::ScrollToRequest {
|
||||||
|
widget->y() + to,
|
||||||
|
-1
|
||||||
|
};
|
||||||
|
}) | rpl::start_to_stream(
|
||||||
|
_scrollToRequests,
|
||||||
|
result->lifetime());
|
||||||
|
_selectedLists.fire(result->selectedListValue());
|
||||||
|
_listTops.fire(result->topValue());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::saveState(not_null<Memento*> memento) {
|
||||||
|
_list->saveState(&memento->media());
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::restoreState(not_null<Memento*> memento) {
|
||||||
|
_list->restoreState(&memento->media());
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
|
||||||
|
return _selectedLists.events_starting_with(
|
||||||
|
_list->selectedListValue()
|
||||||
|
) | rpl::flatten_latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::selectionAction(SelectionAction action) {
|
||||||
|
_list->selectionAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerWidget::~InnerWidget() = default;
|
||||||
|
|
||||||
|
int InnerWidget::resizeGetHeight(int newWidth) {
|
||||||
|
_inResize = true;
|
||||||
|
auto guard = gsl::finally([this] { _inResize = false; });
|
||||||
|
|
||||||
|
_list->resizeToWidth(newWidth);
|
||||||
|
_empty->resizeToWidth(newWidth);
|
||||||
|
return recountHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::refreshHeight() {
|
||||||
|
if (_inResize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resize(width(), recountHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
int InnerWidget::recountHeight() {
|
||||||
|
auto top = 0;
|
||||||
|
auto listHeight = 0;
|
||||||
|
if (_list) {
|
||||||
|
_list->moveToLeft(0, top);
|
||||||
|
listHeight = _list->heightNoMargins();
|
||||||
|
top += listHeight;
|
||||||
|
}
|
||||||
|
if (listHeight > 0) {
|
||||||
|
_empty->hide();
|
||||||
|
} else {
|
||||||
|
_empty->show();
|
||||||
|
_empty->moveToLeft(0, top);
|
||||||
|
top += _empty->heightNoMargins();
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
|
||||||
|
using namespace rpl::mappers;
|
||||||
|
_empty->setFullHeight(rpl::combine(
|
||||||
|
std::move(value),
|
||||||
|
_listTops.events_starting_with(
|
||||||
|
_list->topValue()
|
||||||
|
) | rpl::flatten_latest(),
|
||||||
|
_1 - _2));
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
|
||||||
|
return _scrollToRequests.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
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 "ui/rp_widget.h"
|
||||||
|
#include "ui/widgets/scroll_area.h"
|
||||||
|
#include "base/unique_qptr.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class SettingsSlider;
|
||||||
|
class VerticalLayout;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Info {
|
||||||
|
class Controller;
|
||||||
|
struct SelectedItems;
|
||||||
|
enum class SelectionAction;
|
||||||
|
} // namespace Info
|
||||||
|
|
||||||
|
namespace Info::Media {
|
||||||
|
class ListWidget;
|
||||||
|
} // namespace Info::Media
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
class Memento;
|
||||||
|
class EmptyWidget;
|
||||||
|
|
||||||
|
class InnerWidget final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
InnerWidget(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Controller*> controller);
|
||||||
|
|
||||||
|
bool showInternal(not_null<Memento*> memento);
|
||||||
|
|
||||||
|
void saveState(not_null<Memento*> memento);
|
||||||
|
void restoreState(not_null<Memento*> memento);
|
||||||
|
|
||||||
|
void setScrollHeightValue(rpl::producer<int> value);
|
||||||
|
|
||||||
|
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
|
||||||
|
rpl::producer<SelectedItems> selectedListValue() const;
|
||||||
|
void selectionAction(SelectionAction action);
|
||||||
|
|
||||||
|
~InnerWidget();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int resizeGetHeight(int newWidth) override;
|
||||||
|
void visibleTopBottomUpdated(
|
||||||
|
int visibleTop,
|
||||||
|
int visibleBottom) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int recountHeight();
|
||||||
|
void refreshHeight();
|
||||||
|
|
||||||
|
object_ptr<Media::ListWidget> setupList();
|
||||||
|
|
||||||
|
const not_null<Controller*> _controller;
|
||||||
|
|
||||||
|
object_ptr<Media::ListWidget> _list = { nullptr };
|
||||||
|
object_ptr<EmptyWidget> _empty;
|
||||||
|
|
||||||
|
bool _inResize = false;
|
||||||
|
|
||||||
|
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||||
|
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
|
||||||
|
rpl::event_stream<rpl::producer<int>> _listTops;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
407
Telegram/SourceFiles/info/stories/info_stories_provider.cpp
Normal file
407
Telegram/SourceFiles/info/stories/info_stories_provider.cpp
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
/*
|
||||||
|
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 "info/stories/info_stories_provider.h"
|
||||||
|
|
||||||
|
#include "info/media/info_media_widget.h"
|
||||||
|
#include "info/media/info_media_list_section.h"
|
||||||
|
#include "info/info_controller.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
|
#include "data/data_media_types.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_stories.h"
|
||||||
|
#include "data/data_stories_ids.h"
|
||||||
|
#include "main/main_account.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "history/history_item.h"
|
||||||
|
#include "history/history_item_helpers.h"
|
||||||
|
#include "history/history.h"
|
||||||
|
#include "core/application.h"
|
||||||
|
#include "storage/storage_shared_media.h"
|
||||||
|
#include "layout/layout_selection.h"
|
||||||
|
#include "styles/style_info.h"
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace Media;
|
||||||
|
|
||||||
|
constexpr auto kPreloadedScreensCount = 4;
|
||||||
|
constexpr auto kPreloadedScreensCountFull
|
||||||
|
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
||||||
|
|
||||||
|
[[nodiscard]] int MinStoryHeight(int width) {
|
||||||
|
auto itemsLeft = st::infoMediaSkip;
|
||||||
|
auto itemsInRow = (width - itemsLeft)
|
||||||
|
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
|
||||||
|
return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Provider::Provider(not_null<AbstractController*> controller)
|
||||||
|
: _controller(controller)
|
||||||
|
, _peer(controller->key().storiesPeer())
|
||||||
|
, _history(_peer->owner().history(_peer))
|
||||||
|
, _tab(controller->key().storiesTab()) {
|
||||||
|
style::PaletteChanged(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
for (auto &layout : _layouts) {
|
||||||
|
layout.second.item->invalidateCache();
|
||||||
|
}
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type Provider::type() {
|
||||||
|
return Type::PhotoVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::hasSelectRestriction() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
|
||||||
|
return rpl::never<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::sectionHasFloatingHeader() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::sectionItemBelongsHere(
|
||||||
|
not_null<const BaseLayout*> item,
|
||||||
|
not_null<const BaseLayout*> previous) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> Provider::fullCount() {
|
||||||
|
return _slice.fullCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::restart() {
|
||||||
|
_layouts.clear();
|
||||||
|
_aroundId = kDefaultAroundId;
|
||||||
|
_idsLimit = kMinimalIdsLimit;
|
||||||
|
_slice = Data::StoriesIdsSlice();
|
||||||
|
refreshViewer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::checkPreload(
|
||||||
|
QSize viewport,
|
||||||
|
not_null<BaseLayout*> topLayout,
|
||||||
|
not_null<BaseLayout*> bottomLayout,
|
||||||
|
bool preloadTop,
|
||||||
|
bool preloadBottom) {
|
||||||
|
const auto visibleWidth = viewport.width();
|
||||||
|
const auto visibleHeight = viewport.height();
|
||||||
|
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
|
||||||
|
const auto minItemHeight = MinStoryHeight(visibleWidth);
|
||||||
|
const auto preloadedCount = preloadedHeight / minItemHeight;
|
||||||
|
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
|
||||||
|
const auto preloadIdsLimit = preloadIdsLimitMin
|
||||||
|
+ (visibleHeight / minItemHeight);
|
||||||
|
const auto after = _slice.skippedAfter();
|
||||||
|
const auto topLoaded = after && (*after == 0);
|
||||||
|
const auto before = _slice.skippedBefore();
|
||||||
|
const auto bottomLoaded = before && (*before == 0);
|
||||||
|
|
||||||
|
const auto minScreenDelta = kPreloadedScreensCount
|
||||||
|
- kPreloadIfLessThanScreens;
|
||||||
|
const auto minIdDelta = (minScreenDelta * visibleHeight)
|
||||||
|
/ minItemHeight;
|
||||||
|
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
|
||||||
|
auto preloadRequired = false;
|
||||||
|
const auto id = StoryIdFromMsgId(layout->getItem()->id);
|
||||||
|
if (!preloadRequired) {
|
||||||
|
preloadRequired = (_idsLimit < preloadIdsLimitMin);
|
||||||
|
}
|
||||||
|
if (!preloadRequired) {
|
||||||
|
auto delta = _slice.distance(_aroundId, id);
|
||||||
|
Assert(delta != std::nullopt);
|
||||||
|
preloadRequired = (qAbs(*delta) >= minIdDelta);
|
||||||
|
}
|
||||||
|
if (preloadRequired) {
|
||||||
|
_idsLimit = preloadIdsLimit;
|
||||||
|
_aroundId = id;
|
||||||
|
refreshViewer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preloadTop && !topLoaded) {
|
||||||
|
preloadAroundItem(topLayout);
|
||||||
|
} else if (preloadBottom && !bottomLoaded) {
|
||||||
|
preloadAroundItem(bottomLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::setSearchQuery(QString query) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::refreshViewer() {
|
||||||
|
_viewerLifetime.destroy();
|
||||||
|
const auto idForViewer = _aroundId;
|
||||||
|
const auto session = &_peer->session();
|
||||||
|
auto ids = (_tab == Tab::Saved)
|
||||||
|
? Data::SavedStoriesIds(_peer, idForViewer, _idsLimit)
|
||||||
|
: Data::ArchiveStoriesIds(session, idForViewer, _idsLimit);
|
||||||
|
std::move(
|
||||||
|
ids
|
||||||
|
) | rpl::start_with_next([=](Data::StoriesIdsSlice &&slice) {
|
||||||
|
if (!slice.fullCount()) {
|
||||||
|
// Don't display anything while full count is unknown.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_slice = std::move(slice);
|
||||||
|
if (const auto nearest = _slice.nearest(idForViewer)) {
|
||||||
|
_aroundId = *nearest;
|
||||||
|
}
|
||||||
|
_refreshed.fire({});
|
||||||
|
}, _viewerLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> Provider::refreshed() {
|
||||||
|
return _refreshed.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListSection> Provider::fillSections(
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate) {
|
||||||
|
markLayoutsStale();
|
||||||
|
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
|
||||||
|
|
||||||
|
auto result = std::vector<ListSection>();
|
||||||
|
auto section = ListSection(Type::PhotoVideo, sectionDelegate());
|
||||||
|
auto count = _slice.size();
|
||||||
|
for (auto i = count; i != 0;) {
|
||||||
|
const auto storyId = _slice[--i];
|
||||||
|
if (const auto layout = getLayout(storyId, delegate)) {
|
||||||
|
if (!section.addItem(layout)) {
|
||||||
|
section.finishSection();
|
||||||
|
result.push_back(std::move(section));
|
||||||
|
section = ListSection(Type::PhotoVideo, sectionDelegate());
|
||||||
|
section.addItem(layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!section.empty()) {
|
||||||
|
section.finishSection();
|
||||||
|
result.push_back(std::move(section));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::markLayoutsStale() {
|
||||||
|
for (auto &layout : _layouts) {
|
||||||
|
layout.second.stale = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::clearStaleLayouts() {
|
||||||
|
for (auto i = _layouts.begin(); i != _layouts.end();) {
|
||||||
|
if (i->second.stale) {
|
||||||
|
_layoutRemoved.fire(i->second.item.get());
|
||||||
|
const auto taken = _items.take(i->first);
|
||||||
|
i = _layouts.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
|
||||||
|
return _layoutRemoved.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseLayout *Provider::lookupLayout(const HistoryItem *item) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
|
||||||
|
return IsStoryMsgId(item->id) && (item->history()->peer == _peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::isAfter(
|
||||||
|
not_null<const HistoryItem*> a,
|
||||||
|
not_null<const HistoryItem*> b) {
|
||||||
|
return (a->id < b->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
|
||||||
|
const auto id = StoryIdFromMsgId(item->id);
|
||||||
|
if (const auto i = _layouts.find(id); i != end(_layouts)) {
|
||||||
|
_layoutRemoved.fire(i->second.item.get());
|
||||||
|
_layouts.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseLayout *Provider::getLayout(
|
||||||
|
StoryId id,
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate) {
|
||||||
|
auto it = _layouts.find(id);
|
||||||
|
if (it == _layouts.end()) {
|
||||||
|
if (auto layout = createLayout(id, delegate)) {
|
||||||
|
layout->initDimensions();
|
||||||
|
it = _layouts.emplace(id, std::move(layout)).first;
|
||||||
|
} else {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it->second.stale = false;
|
||||||
|
return it->second.item.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryItem *Provider::ensureItem(StoryId id) {
|
||||||
|
const auto i = _items.find(id);
|
||||||
|
if (i != end(_items)) {
|
||||||
|
return i->second.get();
|
||||||
|
}
|
||||||
|
auto item = _peer->owner().stories().resolveItem({ _peer->id, id });
|
||||||
|
if (!item) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return _items.emplace(id, std::move(item)).first->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<BaseLayout> Provider::createLayout(
|
||||||
|
StoryId id,
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate) {
|
||||||
|
const auto item = ensureItem(id);
|
||||||
|
if (!item) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto getPhoto = [&]() -> PhotoData* {
|
||||||
|
if (const auto media = item->media()) {
|
||||||
|
return media->photo();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
const auto getFile = [&]() -> DocumentData* {
|
||||||
|
if (const auto media = item->media()) {
|
||||||
|
return media->document();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
// #TODO stories
|
||||||
|
const auto maybeStory = item->history()->owner().stories().lookup(
|
||||||
|
{ item->history()->peer->id, StoryIdFromMsgId(item->id) });
|
||||||
|
const auto spoiler = maybeStory && !(*maybeStory)->expired();
|
||||||
|
|
||||||
|
using namespace Overview::Layout;
|
||||||
|
if (const auto photo = getPhoto()) {
|
||||||
|
return std::make_unique<Photo>(delegate, item, photo, spoiler);
|
||||||
|
} else if (const auto file = getFile()) {
|
||||||
|
return std::make_unique<Video>(delegate, item, file, spoiler);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItemSelectionData Provider::computeSelectionData(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
TextSelection selection) {
|
||||||
|
auto result = ListItemSelectionData(selection);
|
||||||
|
result.canDelete = true;
|
||||||
|
result.canForward = item->allowsForward()
|
||||||
|
&& (&item->history()->session() == &_controller->session());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::applyDragSelection(
|
||||||
|
ListSelectedMap &selected,
|
||||||
|
not_null<const HistoryItem*> fromItem,
|
||||||
|
bool skipFrom,
|
||||||
|
not_null<const HistoryItem*> tillItem,
|
||||||
|
bool skipTill) {
|
||||||
|
const auto fromId = fromItem->id - (skipFrom ? 1 : 0);
|
||||||
|
const auto tillId = tillItem->id - (skipTill ? 0 : 1);
|
||||||
|
for (auto i = selected.begin(); i != selected.end();) {
|
||||||
|
const auto itemId = i->first->id;
|
||||||
|
if (itemId > fromId || itemId <= tillId) {
|
||||||
|
i = selected.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto &layoutItem : _layouts) {
|
||||||
|
const auto id = StoryIdToMsgId(layoutItem.first);
|
||||||
|
if (id <= fromId && id > tillId) {
|
||||||
|
const auto i = _items.find(id);
|
||||||
|
Assert(i != end(_items));
|
||||||
|
const auto item = i->second.get();
|
||||||
|
ChangeItemSelection(
|
||||||
|
selected,
|
||||||
|
item,
|
||||||
|
computeSelectionData(item, FullSelection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Provider::allowSaveFileAs(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Provider::showInFolderPath(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
|
||||||
|
return StoryIdFromMsgId(item->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
|
||||||
|
if (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) {
|
||||||
|
return state.item;
|
||||||
|
} else if (const auto id = _slice.nearest(state.position)) {
|
||||||
|
const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id));
|
||||||
|
if (const auto item = _controller->session().data().message(full)) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::saveState(
|
||||||
|
not_null<Media::Memento*> memento,
|
||||||
|
ListScrollTopState scrollState) {
|
||||||
|
if (_aroundId != kDefaultAroundId && scrollState.item) {
|
||||||
|
memento->setAroundId({ _peer->id, _aroundId });
|
||||||
|
memento->setIdsLimit(_idsLimit);
|
||||||
|
memento->setScrollTopItem(scrollState.item->globalId());
|
||||||
|
memento->setScrollTopItemPosition(scrollState.position);
|
||||||
|
memento->setScrollTopShift(scrollState.shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Provider::restoreState(
|
||||||
|
not_null<Media::Memento*> memento,
|
||||||
|
Fn<void(ListScrollTopState)> restoreScrollState) {
|
||||||
|
if (const auto limit = memento->idsLimit()) {
|
||||||
|
const auto wasAroundId = memento->aroundId();
|
||||||
|
if (wasAroundId.peer == _peer->id) {
|
||||||
|
_idsLimit = limit;
|
||||||
|
_aroundId = StoryIdFromMsgId(wasAroundId.msg);
|
||||||
|
restoreScrollState({
|
||||||
|
.position = memento->scrollTopItemPosition(),
|
||||||
|
.item = MessageByGlobalId(memento->scrollTopItem()),
|
||||||
|
.shift = memento->scrollTopShift(),
|
||||||
|
});
|
||||||
|
refreshViewer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
132
Telegram/SourceFiles/info/stories/info_stories_provider.h
Normal file
132
Telegram/SourceFiles/info/stories/info_stories_provider.h
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
#include "data/data_stories_ids.h"
|
||||||
|
#include "info/media/info_media_common.h"
|
||||||
|
|
||||||
|
class DocumentData;
|
||||||
|
class HistoryItem;
|
||||||
|
class PeerData;
|
||||||
|
class History;
|
||||||
|
|
||||||
|
namespace Info {
|
||||||
|
class AbstractController;
|
||||||
|
} // namespace Info
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
enum class Tab;
|
||||||
|
|
||||||
|
class Provider final
|
||||||
|
: public Media::ListProvider
|
||||||
|
, private Media::ListSectionDelegate
|
||||||
|
, public base::has_weak_ptr {
|
||||||
|
public:
|
||||||
|
explicit Provider(not_null<AbstractController*> controller);
|
||||||
|
|
||||||
|
Media::Type type() override;
|
||||||
|
bool hasSelectRestriction() override;
|
||||||
|
rpl::producer<bool> hasSelectRestrictionChanges() override;
|
||||||
|
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
|
||||||
|
|
||||||
|
std::optional<int> fullCount() override;
|
||||||
|
|
||||||
|
void restart() override;
|
||||||
|
void checkPreload(
|
||||||
|
QSize viewport,
|
||||||
|
not_null<Media::BaseLayout*> topLayout,
|
||||||
|
not_null<Media::BaseLayout*> bottomLayout,
|
||||||
|
bool preloadTop,
|
||||||
|
bool preloadBottom) override;
|
||||||
|
void refreshViewer() override;
|
||||||
|
rpl::producer<> refreshed() override;
|
||||||
|
|
||||||
|
void setSearchQuery(QString query) override;
|
||||||
|
|
||||||
|
std::vector<Media::ListSection> fillSections(
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate) override;
|
||||||
|
rpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;
|
||||||
|
Media::BaseLayout *lookupLayout(const HistoryItem *item) override;
|
||||||
|
bool isMyItem(not_null<const HistoryItem*> item) override;
|
||||||
|
bool isAfter(
|
||||||
|
not_null<const HistoryItem*> a,
|
||||||
|
not_null<const HistoryItem*> b) override;
|
||||||
|
|
||||||
|
Media::ListItemSelectionData computeSelectionData(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
TextSelection selection) override;
|
||||||
|
void applyDragSelection(
|
||||||
|
Media::ListSelectedMap &selected,
|
||||||
|
not_null<const HistoryItem*> fromItem,
|
||||||
|
bool skipFrom,
|
||||||
|
not_null<const HistoryItem*> tillItem,
|
||||||
|
bool skipTill) override;
|
||||||
|
|
||||||
|
bool allowSaveFileAs(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
not_null<DocumentData*> document) override;
|
||||||
|
QString showInFolderPath(
|
||||||
|
not_null<const HistoryItem*> item,
|
||||||
|
not_null<DocumentData*> document) override;
|
||||||
|
|
||||||
|
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
|
||||||
|
HistoryItem *scrollTopStateItem(
|
||||||
|
Media::ListScrollTopState state) override;
|
||||||
|
void saveState(
|
||||||
|
not_null<Media::Memento*> memento,
|
||||||
|
Media::ListScrollTopState scrollState) override;
|
||||||
|
void restoreState(
|
||||||
|
not_null<Media::Memento*> memento,
|
||||||
|
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr auto kMinimalIdsLimit = 16;
|
||||||
|
static constexpr auto kDefaultAroundId = ServerMaxStoryId - 1;
|
||||||
|
|
||||||
|
bool sectionHasFloatingHeader() override;
|
||||||
|
QString sectionTitle(not_null<const Media::BaseLayout*> item) override;
|
||||||
|
bool sectionItemBelongsHere(
|
||||||
|
not_null<const Media::BaseLayout*> item,
|
||||||
|
not_null<const Media::BaseLayout*> previous) override;
|
||||||
|
|
||||||
|
void itemRemoved(not_null<const HistoryItem*> item);
|
||||||
|
void markLayoutsStale();
|
||||||
|
void clearStaleLayouts();
|
||||||
|
|
||||||
|
[[nodiscard]] HistoryItem *ensureItem(StoryId id);
|
||||||
|
[[nodiscard]] Media::BaseLayout *getLayout(
|
||||||
|
StoryId id,
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate);
|
||||||
|
[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(
|
||||||
|
StoryId id,
|
||||||
|
not_null<Overview::Layout::Delegate*> delegate);
|
||||||
|
|
||||||
|
const not_null<AbstractController*> _controller;
|
||||||
|
const not_null<PeerData*> _peer;
|
||||||
|
const not_null<History*> _history;
|
||||||
|
const Tab _tab;
|
||||||
|
|
||||||
|
StoryId _aroundId = kDefaultAroundId;
|
||||||
|
int _idsLimit = kMinimalIdsLimit;
|
||||||
|
Data::StoriesIdsSlice _slice;
|
||||||
|
|
||||||
|
base::flat_map<StoryId, std::shared_ptr<HistoryItem>> _items;
|
||||||
|
std::unordered_map<StoryId, Media::CachedItem> _layouts;
|
||||||
|
rpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;
|
||||||
|
rpl::event_stream<> _refreshed;
|
||||||
|
|
||||||
|
bool _started = false;
|
||||||
|
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
rpl::lifetime _viewerLifetime;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
112
Telegram/SourceFiles/info/stories/info_stories_widget.cpp
Normal file
112
Telegram/SourceFiles/info/stories/info_stories_widget.cpp
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
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 "info/stories/info_stories_widget.h"
|
||||||
|
|
||||||
|
#include "info/stories/info_stories_inner_widget.h"
|
||||||
|
#include "info/info_controller.h"
|
||||||
|
#include "info/info_memento.h"
|
||||||
|
#include "ui/widgets/scroll_area.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
Memento::Memento(not_null<Controller*> controller)
|
||||||
|
: ContentMemento(Tag{ controller->storiesPeer(), controller->storiesTab() })
|
||||||
|
, _media(controller) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Memento::Memento(not_null<PeerData*> peer)
|
||||||
|
: ContentMemento(Tag{ peer })
|
||||||
|
, _media(peer, 0, Media::Type::PhotoVideo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Memento::~Memento() = default;
|
||||||
|
|
||||||
|
Section Memento::section() const {
|
||||||
|
return Section(Section::Type::Stories);
|
||||||
|
}
|
||||||
|
|
||||||
|
object_ptr<ContentWidget> Memento::createWidget(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Controller*> controller,
|
||||||
|
const QRect &geometry) {
|
||||||
|
auto result = object_ptr<Widget>(parent, controller);
|
||||||
|
result->setInternalState(geometry, this);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget::Widget(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Controller*> controller)
|
||||||
|
: ContentWidget(parent, controller) {
|
||||||
|
_inner = setInnerWidget(object_ptr<InnerWidget>(
|
||||||
|
this,
|
||||||
|
controller));
|
||||||
|
_inner->setScrollHeightValue(scrollHeightValue());
|
||||||
|
_inner->scrollToRequests(
|
||||||
|
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
|
||||||
|
scrollTo(request);
|
||||||
|
}, _inner->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Widget::showInternal(not_null<ContentMemento*> memento) {
|
||||||
|
if (!controller()->validateMementoPeer(memento)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (auto storiesMemento = dynamic_cast<Memento*>(memento.get())) {
|
||||||
|
restoreState(storiesMemento);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Widget::setInternalState(
|
||||||
|
const QRect &geometry,
|
||||||
|
not_null<Memento*> memento) {
|
||||||
|
setGeometry(geometry);
|
||||||
|
Ui::SendPendingMoveResizeEvents(this);
|
||||||
|
restoreState(memento);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
|
||||||
|
auto result = std::make_shared<Memento>(controller());
|
||||||
|
saveState(result.get());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Widget::saveState(not_null<Memento*> memento) {
|
||||||
|
memento->setScrollTop(scrollTopSave());
|
||||||
|
_inner->saveState(memento);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Widget::restoreState(not_null<Memento*> memento) {
|
||||||
|
_inner->restoreState(memento);
|
||||||
|
scrollTopRestore(memento->scrollTop());
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<SelectedItems> Widget::selectedListValue() const {
|
||||||
|
return _inner->selectedListValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Widget::selectionAction(SelectionAction action) {
|
||||||
|
_inner->selectionAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QString> Widget::title() {
|
||||||
|
return tr::lng_menu_my_stories(); // #TODO stories
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
|
||||||
|
return std::make_shared<Info::Memento>(
|
||||||
|
std::vector<std::shared_ptr<ContentMemento>>(
|
||||||
|
1,
|
||||||
|
std::make_shared<Memento>(peer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
||||||
|
|
70
Telegram/SourceFiles/info/stories/info_stories_widget.h
Normal file
70
Telegram/SourceFiles/info/stories/info_stories_widget.h
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
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 "info/info_content_widget.h"
|
||||||
|
#include "info/media/info_media_widget.h"
|
||||||
|
|
||||||
|
namespace Info::Stories {
|
||||||
|
|
||||||
|
class InnerWidget;
|
||||||
|
|
||||||
|
class Memento final : public ContentMemento {
|
||||||
|
public:
|
||||||
|
Memento(not_null<Controller*> controller);
|
||||||
|
Memento(not_null<PeerData*> peer);
|
||||||
|
~Memento();
|
||||||
|
|
||||||
|
object_ptr<ContentWidget> createWidget(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Controller*> controller,
|
||||||
|
const QRect &geometry) override;
|
||||||
|
|
||||||
|
Section section() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] Media::Memento &media() {
|
||||||
|
return _media;
|
||||||
|
}
|
||||||
|
[[nodiscard]] const Media::Memento &media() const {
|
||||||
|
return _media;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Media::Memento _media;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class Widget final : public ContentWidget {
|
||||||
|
public:
|
||||||
|
Widget(QWidget *parent, not_null<Controller*> controller);
|
||||||
|
|
||||||
|
bool showInternal(
|
||||||
|
not_null<ContentMemento*> memento) override;
|
||||||
|
|
||||||
|
void setInternalState(
|
||||||
|
const QRect &geometry,
|
||||||
|
not_null<Memento*> memento);
|
||||||
|
|
||||||
|
rpl::producer<SelectedItems> selectedListValue() const override;
|
||||||
|
void selectionAction(SelectionAction action) override;
|
||||||
|
|
||||||
|
rpl::producer<QString> title() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void saveState(not_null<Memento*> memento);
|
||||||
|
void restoreState(not_null<Memento*> memento);
|
||||||
|
|
||||||
|
std::shared_ptr<ContentMemento> doCreateMemento() override;
|
||||||
|
|
||||||
|
InnerWidget *_inner = nullptr;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
|
||||||
|
|
||||||
|
} // namespace Info::Stories
|
|
@ -393,12 +393,10 @@ void Controller::show(
|
||||||
auto &stories = story->owner().stories();
|
auto &stories = story->owner().stories();
|
||||||
const auto storyId = story->fullId();
|
const auto storyId = story->fullId();
|
||||||
const auto id = storyId.story;
|
const auto id = storyId.story;
|
||||||
const auto &all = stories.all();
|
auto source = stories.source(storyId.peer);
|
||||||
const auto inAll = all.find(storyId.peer);
|
|
||||||
auto source = (inAll != end(all)) ? &inAll->second : nullptr;
|
|
||||||
auto single = StoriesSource{ story->peer()->asUser() };
|
auto single = StoriesSource{ story->peer()->asUser() };
|
||||||
v::match(context.data, [&](StoriesContextSingle) {
|
v::match(context.data, [&](StoriesContextSingle) {
|
||||||
source = &single;
|
source = nullptr;
|
||||||
hideSiblings();
|
hideSiblings();
|
||||||
}, [&](StoriesContextPeer) {
|
}, [&](StoriesContextPeer) {
|
||||||
hideSiblings();
|
hideSiblings();
|
||||||
|
@ -423,17 +421,11 @@ void Controller::show(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const auto idDates = story->idDates();
|
const auto idDates = story->idDates();
|
||||||
if (!source) {
|
_index = source ? (source->ids.find(idDates) - begin(source->ids)) : 0;
|
||||||
return;
|
if (!source || _index == source->ids.size()) {
|
||||||
} else if (source == &single) {
|
source = &single;
|
||||||
single.ids.emplace(idDates);
|
single.ids.emplace(idDates);
|
||||||
_index = 0;
|
_index = 0;
|
||||||
} else {
|
|
||||||
const auto k = source->ids.find(idDates);
|
|
||||||
if (k == end(source->ids)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_index = (k - begin(source->ids));
|
|
||||||
}
|
}
|
||||||
const auto guard = gsl::finally([&] {
|
const auto guard = gsl::finally([&] {
|
||||||
_paused = false;
|
_paused = false;
|
||||||
|
@ -543,12 +535,11 @@ void Controller::showSibling(
|
||||||
sibling = nullptr;
|
sibling = nullptr;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto &all = session->data().stories().all();
|
const auto source = session->data().stories().source(peerId);
|
||||||
const auto i = all.find(peerId);
|
if (!source) {
|
||||||
if (i == end(all)) {
|
|
||||||
sibling = nullptr;
|
sibling = nullptr;
|
||||||
} else if (!sibling || !sibling->shows(i->second)) {
|
} else if (!sibling || !sibling->shows(*source)) {
|
||||||
sibling = std::make_unique<Sibling>(this, i->second);
|
sibling = std::make_unique<Sibling>(this, *source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0
|
||||||
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
|
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
|
||||||
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
|
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
|
||||||
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
|
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
|
||||||
|
inputMediaStory#9a86b58f user_id:InputUser id:int = InputMedia;
|
||||||
|
|
||||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||||
inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;
|
inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;
|
||||||
|
@ -128,6 +129,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes
|
||||||
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
|
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
|
||||||
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
|
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
|
||||||
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
|
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
|
||||||
|
messageMediaStory#c79aee11 user_id:long id:int = MessageMedia;
|
||||||
|
|
||||||
messageActionEmpty#b6aef7b0 = MessageAction;
|
messageActionEmpty#b6aef7b0 = MessageAction;
|
||||||
messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
|
messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
|
||||||
|
|
|
@ -39,6 +39,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "settings/settings_information.h"
|
#include "settings/settings_information.h"
|
||||||
#include "info/profile/info_profile_badge.h"
|
#include "info/profile/info_profile_badge.h"
|
||||||
#include "info/profile/info_profile_emoji_status_panel.h"
|
#include "info/profile/info_profile_emoji_status_panel.h"
|
||||||
|
#include "info/stories/info_stories_widget.h"
|
||||||
|
#include "info/info_memento.h"
|
||||||
#include "base/qt_signal_producer.h"
|
#include "base/qt_signal_producer.h"
|
||||||
#include "boxes/about_box.h"
|
#include "boxes/about_box.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
|
@ -766,16 +768,16 @@ void MainMenu::setupMenu() {
|
||||||
_menu,
|
_menu,
|
||||||
CreateButton(
|
CreateButton(
|
||||||
_menu,
|
_menu,
|
||||||
rpl::single(u"My Stories"_q),
|
tr::lng_menu_my_stories(),
|
||||||
st::mainMenuButton,
|
st::mainMenuButton,
|
||||||
IconDescriptor{
|
IconDescriptor{
|
||||||
&st::settingsIconSavedMessages,
|
&st::settingsIconSavedMessages,
|
||||||
kIconLightOrange
|
kIconLightOrange
|
||||||
})));
|
})));
|
||||||
const auto stories = &controller->session().data().stories();
|
const auto stories = &controller->session().data().stories();
|
||||||
const auto &all = stories->all();
|
const auto mine = stories->source(
|
||||||
const auto mine = all.find(controller->session().userPeerId());
|
controller->session().userPeerId());
|
||||||
if ((mine != end(all) && !mine->second.ids.empty())
|
if ((mine && !mine->ids.empty())
|
||||||
|| stories->expiredMineCount() > 0) {
|
|| stories->expiredMineCount() > 0) {
|
||||||
wrap->toggle(true, anim::type::instant);
|
wrap->toggle(true, anim::type::instant);
|
||||||
} else {
|
} else {
|
||||||
|
@ -789,7 +791,8 @@ void MainMenu::setupMenu() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wrap->entity()->setClickedCallback([=] {
|
wrap->entity()->setClickedCallback([=] {
|
||||||
controller->showToast(u"My Stories"_q);
|
controller->showSection(
|
||||||
|
Info::Stories::Make(controller->session().user()));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addAction(
|
addAction(
|
||||||
|
|
|
@ -2143,6 +2143,9 @@ void SessionController::openPhoto(
|
||||||
not_null<PhotoData*> photo,
|
not_null<PhotoData*> photo,
|
||||||
FullMsgId contextId,
|
FullMsgId contextId,
|
||||||
MsgId topicRootId) {
|
MsgId topicRootId) {
|
||||||
|
if (openStory(contextId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_window->openInMediaView(Media::View::OpenRequest(
|
_window->openInMediaView(Media::View::OpenRequest(
|
||||||
this,
|
this,
|
||||||
photo,
|
photo,
|
||||||
|
@ -2161,7 +2164,9 @@ void SessionController::openDocument(
|
||||||
FullMsgId contextId,
|
FullMsgId contextId,
|
||||||
MsgId topicRootId,
|
MsgId topicRootId,
|
||||||
bool showInMediaView) {
|
bool showInMediaView) {
|
||||||
if (showInMediaView) {
|
if (openStory(contextId)) {
|
||||||
|
return;
|
||||||
|
} else if (showInMediaView) {
|
||||||
_window->openInMediaView(Media::View::OpenRequest(
|
_window->openInMediaView(Media::View::OpenRequest(
|
||||||
this,
|
this,
|
||||||
document,
|
document,
|
||||||
|
@ -2176,6 +2181,31 @@ void SessionController::openDocument(
|
||||||
topicRootId);
|
topicRootId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SessionController::openStory(
|
||||||
|
FullMsgId fakeItemId,
|
||||||
|
bool forceArchiveContext) {
|
||||||
|
if (!peerIsUser(fakeItemId.peer)
|
||||||
|
|| !IsStoryMsgId(fakeItemId.msg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto maybeStory = session().data().stories().lookup({
|
||||||
|
fakeItemId.peer,
|
||||||
|
StoryIdFromMsgId(fakeItemId.msg),
|
||||||
|
});
|
||||||
|
if (maybeStory) {
|
||||||
|
using namespace Data;
|
||||||
|
const auto story = *maybeStory;
|
||||||
|
const auto context = !story->expired()
|
||||||
|
? StoriesContext{ StoriesContextPeer() }
|
||||||
|
: (story->pinned() && !forceArchiveContext)
|
||||||
|
? StoriesContext{ StoriesContextSaved() }
|
||||||
|
: StoriesContext{ StoriesContextArchive() };
|
||||||
|
_window->openInMediaView(
|
||||||
|
::Media::View::OpenRequest(this, *maybeStory, context));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
auto SessionController::cachedChatThemeValue(
|
auto SessionController::cachedChatThemeValue(
|
||||||
const Data::CloudTheme &data,
|
const Data::CloudTheme &data,
|
||||||
const Data::WallPaper &paper,
|
const Data::WallPaper &paper,
|
||||||
|
@ -2484,14 +2514,12 @@ void SessionController::openPeerStories(
|
||||||
using namespace Data;
|
using namespace Data;
|
||||||
|
|
||||||
auto &stories = session().data().stories();
|
auto &stories = session().data().stories();
|
||||||
const auto &all = stories.all();
|
if (const auto source = stories.source(peerId)) {
|
||||||
const auto i = all.find(peerId);
|
const auto j = source->ids.lower_bound(
|
||||||
if (i != end(all)) {
|
StoryIdDates{ source->readTill + 1 });
|
||||||
const auto j = i->second.ids.lower_bound(
|
|
||||||
StoryIdDates{ i->second.readTill + 1 });
|
|
||||||
openPeerStory(
|
openPeerStory(
|
||||||
i->second.user,
|
source->user,
|
||||||
j != i->second.ids.end() ? j->id : i->second.ids.front().id,
|
j != source->ids.end() ? j->id : source->ids.front().id,
|
||||||
{ list });
|
{ list });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -491,6 +491,7 @@ public:
|
||||||
FullMsgId contextId,
|
FullMsgId contextId,
|
||||||
MsgId topicRootId,
|
MsgId topicRootId,
|
||||||
bool showInMediaView = false);
|
bool showInMediaView = false);
|
||||||
|
bool openStory(FullMsgId fakeItemId, bool forceArchiveContext = false);
|
||||||
|
|
||||||
void showChooseReportMessages(
|
void showChooseReportMessages(
|
||||||
not_null<PeerData*> peer,
|
not_null<PeerData*> peer,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user