diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 2fb2f8c28..dbddd0da0 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -26,7 +26,78 @@ namespace { } // namespace bool StoriesList::unread() const { - return !items.empty() && readTill < items.front().id; + return !ids.empty() && readTill < ids.front(); +} + +Story::Story( + StoryId id, + not_null peer, + StoryMedia media, + TimeId date) +: _id(id) +, _peer(peer) +, _media(std::move(media)) +, _date(date) { +} + +Session &Story::owner() const { + return _peer->owner(); +} + +Main::Session &Story::session() const { + return _peer->session(); +} + +not_null Story::peer() const { + return _peer; +} + +StoryId Story::id() const { + return _id; +} + +TimeId Story::date() const { + return _date; +} + +const StoryMedia &Story::media() const { + return _media; +} + +PhotoData *Story::photo() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +DocumentData *Story::document() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +void Story::setPinned(bool pinned) { + _pinned = pinned; +} + +bool Story::pinned() const { + return _pinned; +} + +void Story::setCaption(TextWithEntities &&caption) { + _caption = std::move(caption); +} + +const TextWithEntities &Story::caption() const { + return _caption; +} + +void Story::apply(const MTPDstoryItem &data) { + _pinned = data.is_pinned(); + _caption = TextWithEntities{ + data.vcaption().value_or_empty(), + Api::EntitiesFromMTP( + &owner().session(), + data.ventities().value_or_empty()), + }; } Stories::Stories(not_null owner) : _owner(owner) { @@ -41,6 +112,7 @@ Session &Stories::owner() const { void Stories::apply(const MTPDupdateStories &data) { pushToFront(parse(data.vstories())); + _allChanged.fire({}); } StoriesList Stories::parse(const MTPUserStories &stories) { @@ -54,25 +126,36 @@ StoriesList Stories::parse(const MTPUserStories &stories) { .total = count, }; const auto &list = data.vstories().v; - result.items.reserve(list.size()); + result.ids.reserve(list.size()); for (const auto &story : list) { story.match([&](const MTPDstoryItem &data) { - if (auto entry = parse(data)) { - result.items.push_back(std::move(*entry)); + if (const auto story = parse(result.user, data)) { + result.ids.push_back(story->id()); } else { --result.total; } - }, [&](const MTPDstoryItemSkipped &) { - }, [&](const MTPDstoryItemDeleted &) { + }, [&](const MTPDstoryItemSkipped &data) { + result.ids.push_back(data.vid().v); + }, [&](const MTPDstoryItemDeleted &data) { + _deleted.emplace(FullStoryId{ + .peer = peerFromUser(userId), + .story = data.vid().v, + }); --result.total; }); } - result.total = std::min(result.total, int(result.items.size())); + result.total = std::max(result.total, int(result.ids.size())); return result; } -std::optional Stories::parse(const MTPDstoryItem &data) { +Story *Stories::parse(not_null peer, const MTPDstoryItem &data) { const auto id = data.vid().v; + auto &stories = _stories[peer->id]; + const auto i = stories.find(id); + if (i != end(stories)) { + i->second->apply(data); + return i->second.get(); + } using MaybeMedia = std::optional< std::variant, not_null>>; const auto media = data.vmedia().match([&]( @@ -95,24 +178,15 @@ std::optional Stories::parse(const MTPDstoryItem &data) { return {}; }, [](const auto &) { return MaybeMedia(); }); if (!media) { - return {}; + return nullptr; } - auto caption = TextWithEntities{ - data.vcaption().value_or_empty(), - Api::EntitiesFromMTP( - &_owner->session(), - data.ventities().value_or_empty()), - }; - auto privacy = StoryPrivacy(); - - const auto date = data.vdate().v; - return StoryItem{ - .id = data.vid().v, - .media = { *media }, - .caption = std::move(caption), - .date = date, - .privacy = privacy, - }; + const auto result = stories.emplace(id, std::make_unique( + id, + peer, + StoryMedia{ *media }, + data.vdate().v)).first->second.get(); + result->apply(data); + return result; } void Stories::loadMore() { @@ -134,6 +208,7 @@ void Stories::loadMore() { for (const auto &single : data.vuser_stories().v) { pushToBack(parse(single)); } + _allChanged.fire({}); }, [](const MTPDstories_allStoriesNotModified &) { }); }).fail([=] { @@ -153,96 +228,20 @@ rpl::producer<> Stories::allChanged() const { return _allChanged.events(); } -// #TODO stories testing -StoryId Stories::generate( - not_null item, - std::variant< - v::null_t, - not_null, - not_null> media) { - if (v::is_null(media) - || !item->from()->isUser() - || !item->isRegular()) { - return {}; +base::expected, NoStory> Stories::lookup( + FullStoryId id) const { + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + return j->second.get(); + } } - const auto document = v::is>(media) - ? v::get>(media).get() - : nullptr; - if (document && !document->isVideoFile()) { - return {}; - } - using namespace Storage; - auto resultId = StoryId(); - const auto listType = SharedMediaType::PhotoVideo; - const auto itemId = item->id; - const auto peer = item->history()->peer; - const auto session = &peer->session(); - auto full = std::vector(); - const auto lifetime = session->storage().query(SharedMediaQuery( - SharedMediaKey(peer->id, MsgId(0), listType, itemId), - 32, - 32 - )) | rpl::start_with_next([&](SharedMediaResult &&result) { - if (!result.messageIds.contains(itemId)) { - result.messageIds.emplace(itemId); - } - auto index = StoryId(); - const auto owner = &peer->owner(); - for (const auto id : result.messageIds) { - if (const auto item = owner->message(peer, id)) { - const auto user = item->from()->asUser(); - if (!user) { - continue; - } - const auto i = ranges::find( - full, - not_null(user), - &StoriesList::user); - auto &stories = (i == end(full)) - ? full.emplace_back(StoriesList{ .user = user }) - : *i; - if (id == itemId) { - resultId = ++index; - stories.items.push_back({ - .id = resultId, - .media = (document - ? StoryMedia{ not_null(document) } - : StoryMedia{ - v::get>(media) }), - .caption = item->originalText(), - .date = item->date(), - }); - ++stories.total; - } else if (const auto media = item->media()) { - const auto photo = media->photo(); - const auto document = media->document(); - if (photo || (document && document->isVideoFile())) { - stories.items.push_back({ - .id = ++index, - .media = (document - ? StoryMedia{ not_null(document) } - : StoryMedia{ not_null(photo) }), - .caption = item->originalText(), - .date = item->date(), - }); - ++stories.total; - } - } - } - } - for (auto &stories : full) { - const auto i = ranges::find( - _all, - stories.user, - &StoriesList::user); - if (i != end(_all)) { - *i = std::move(stories); - } else { - _all.push_back(std::move(stories)); - } - } - }); - return resultId; + return base::make_unexpected( + _deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown); +} + +void Stories::resolve(FullStoryId id, Fn done) { } void Stories::pushToBack(StoriesList &&list) { @@ -255,7 +254,6 @@ void Stories::pushToBack(StoriesList &&list) { } else { _all.push_back(std::move(list)); } - _allChanged.fire({}); } void Stories::pushToFront(StoriesList &&list) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 6c3aaa56d..e5f3b49db 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -7,37 +7,64 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/expected.h" + class PhotoData; class DocumentData; +namespace Main { +class Session; +} // namespace Main + namespace Data { class Session; -struct StoryPrivacy { - friend inline bool operator==(StoryPrivacy, StoryPrivacy) = default; -}; - struct StoryMedia { std::variant, not_null> data; friend inline bool operator==(StoryMedia, StoryMedia) = default; }; -struct StoryItem { - StoryId id = 0; - StoryMedia media; - TextWithEntities caption; - TimeId date = 0; - StoryPrivacy privacy; - bool pinned = false; +class Story { +public: + Story( + StoryId id, + not_null peer, + StoryMedia media, + TimeId date); + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null peer() const; + + [[nodiscard]] StoryId id() const; + [[nodiscard]] TimeId date() const; + [[nodiscard]] const StoryMedia &media() const; + [[nodiscard]] PhotoData *photo() const; + [[nodiscard]] DocumentData *document() const; + + void setPinned(bool pinned); + [[nodiscard]] bool pinned() const; + + void setCaption(TextWithEntities &&caption); + [[nodiscard]] const TextWithEntities &caption() const; + + void apply(const MTPDstoryItem &data); + +private: + const StoryId _id = 0; + const not_null _peer; + const StoryMedia _media; + TextWithEntities _caption; + const TimeId _date = 0; + bool _pinned = false; - friend inline bool operator==(StoryItem, StoryItem) = default; }; struct StoriesList { not_null user; - std::vector items; + std::vector ids; StoryId readTill = 0; int total = 0; @@ -46,6 +73,11 @@ struct StoriesList { friend inline bool operator==(StoriesList, StoriesList) = default; }; +enum class NoStory : uchar { + Unknown, + Deleted, +}; + class Stories final { public: explicit Stories(not_null owner); @@ -60,22 +92,24 @@ public: [[nodiscard]] bool allLoaded() const; [[nodiscard]] rpl::producer<> allChanged() const; - // #TODO stories testing - [[nodiscard]] StoryId generate( - not_null item, - std::variant< - v::null_t, - not_null, - not_null> media); + [[nodiscard]] base::expected, NoStory> lookup( + FullStoryId id) const; + void resolve(FullStoryId id, Fn done); private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); - [[nodiscard]] std::optional parse(const MTPDstoryItem &data); + [[nodiscard]] Story *parse( + not_null peer, + const MTPDstoryItem &data); void pushToBack(StoriesList &&list); void pushToFront(StoriesList &&list); const not_null _owner; + base::flat_map< + PeerId, + base::flat_map>> _stories; + base::flat_set _deleted; std::vector _all; rpl::event_stream<> _allChanged; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index a4c0bb267..33a2a93cd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" #include "base/unixtime.h" @@ -335,6 +336,11 @@ InnerWidget::InnerWidget( clearSelection(); }, lifetime()); + _stories->clicks( + ) | rpl::start_with_next([=](uint64 id) { + _controller->openPeerStories(PeerId(int64(id))); + }, lifetime()); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -590,6 +596,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e)) { return; } + const auto fillGuard = gsl::finally([&] { + // We translate painter down, but it'll be cropped below rect. + p.fillRect(rect(), st::dialogsBg); + }); + const auto activeEntry = _controller->activeChatEntryCurrent(); const auto videoPaused = _controller->isGifPausedAtLeastFor( Window::GifPauseReason::Any); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 021d07328..b1262388f 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -128,13 +128,13 @@ State::State(not_null data) Content State::next() { auto result = Content(); -#if 0 // #TODO stories testing +#if 1 // #TODO stories testing const auto &all = _data->all(); result.users.reserve(all.size()); for (const auto &list : all) { auto userpic = std::shared_ptr(); const auto user = list.user; -#endif +#else const auto list = _data->owner().chatsList(); const auto &all = list->indexed()->all(); result.users.reserve(all.size()); @@ -142,6 +142,7 @@ Content State::next() { if (const auto history = entry->history()) { if (const auto user = history->peer->asUser(); user && !user->isBot()) { auto userpic = std::shared_ptr(); +#endif if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; } else { @@ -152,10 +153,14 @@ Content State::next() { .id = uint64(user->id.value), .name = user->shortName(), .userpic = std::move(userpic), - .unread = history->chatListBadgesState().unread// list.unread(), +#if 1 // #TODO stories testing + .unread = list.unread(), +#else + .unread = history->chatListBadgesState().unread }); } - } +#endif + }); } return result; } @@ -170,15 +175,16 @@ rpl::producer ContentForSession(not_null session) { rpl::single( rpl::empty ) | rpl::then( -#if 0 // #TODO stories testing +#if 1 // #TODO stories testing stories->allChanged() -#endif +#else rpl::merge( session->data().chatsListChanges( ) | rpl::filter( rpl::mappers::_1 == nullptr ) | rpl::to_empty, session->data().unreadBadgeChanges()) +#endif ) | rpl::start_with_next([=] { consumer.put_next(state->next()); }, result); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index d5d1f3d3c..3a8cdfd0a 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -29,6 +29,20 @@ constexpr auto kSummaryExpandLeft = 1.5; } // namespace +struct List::Layout { + int itemsCount = 0; + int shownHeight = 0; + float64 ratio = 0.; + float64 userpicLeft = 0.; + float64 photoLeft = 0.; + float64 left = 0.; + int startIndexSmall = 0; + int endIndexSmall = 0; + int startIndexFull = 0; + int endIndexFull = 0; + int singleFull = 0; +}; + List::List( not_null parent, rpl::producer content, @@ -42,6 +56,7 @@ List::List( }, lifetime()); _shownAnimation.stop(); + setMouseTracking(true); resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); } @@ -214,30 +229,29 @@ void List::resizeEvent(QResizeEvent *e) { updateScrollMax(); } -void List::paintEvent(QPaintEvent *e) { +List::Layout List::computeLayout() const { const auto &st = st::dialogsStories; const auto &full = st::dialogsStoriesFull; const auto shownHeight = std::max(_shownHeight(), st.height); const auto ratio = float64(shownHeight - st.height) / (full.height - st.height); - const auto lerp = [=](float64 a, float64 b) { + const auto lerp = [&](float64 a, float64 b) { return a + (b - a) * ratio; }; auto &rendering = _data.empty() ? _hidingData : _data; - const auto photo = lerp(st.photo, full.photo); - const auto photoTopSmall = (st.height - st.photo) / 2.; - const auto photoTop = lerp(photoTopSmall, full.photoTop); - const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; - const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto summaryTop = st.nameTop - - (st.photoTop + (st.photo / 2.)) - + (photoTop + (photo / 2.)); - const auto singleSmall = st.shift; const auto singleFull = full.photoLeft * 2 + full.photo; - const auto single = lerp(singleSmall, singleFull); const auto itemsCount = int(rendering.items.size()); - const auto leftSmall = st.left; - const auto leftFull = full.left - _scrollLeft; + const auto narrowWidth = st::defaultDialogRow.padding.left() + + st::defaultDialogRow.photoSize + + st::defaultDialogRow.padding.left(); + const auto narrow = (width() <= narrowWidth); + const auto smallWidth = st.photo + (itemsCount - 1) * st.shift; + const auto leftSmall = narrow + ? ((narrowWidth - smallWidth) / 2 - st.photoLeft) + : st.left; + const auto leftFull = (narrow + ? ((narrowWidth - full.photo) / 2 - full.photoLeft) + : full.left) - _scrollLeft; const auto startIndexFull = std::max(-leftFull, 0) / singleFull; const auto cellLeftFull = leftFull + (startIndexFull * singleFull); const auto endIndexFull = std::min( @@ -250,18 +264,51 @@ void List::paintEvent(QPaintEvent *e) { const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); const auto photoLeft = lerp(st.photoLeft, full.photoLeft); - const auto left = userpicLeft - photoLeft; - const auto nameScale = shownHeight / float64(full.height); + return Layout{ + .itemsCount = itemsCount, + .shownHeight = shownHeight, + .ratio = ratio, + .userpicLeft = userpicLeft, + .photoLeft = photoLeft, + .left = userpicLeft - photoLeft, + .startIndexSmall = startIndexSmall, + .endIndexSmall = endIndexSmall, + .startIndexFull = startIndexFull, + .endIndexFull = endIndexFull, + .singleFull = singleFull, + }; +} + +void List::paintEvent(QPaintEvent *e) { + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto layout = computeLayout(); + const auto ratio = layout.ratio; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + auto &rendering = _data.empty() ? _hidingData : _data; + const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; + const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto singleSmall = st.shift; + const auto single = lerp(singleSmall, layout.singleFull); + const auto photoTopSmall = (st.height - st.photo) / 2.; + const auto photoTop = lerp(photoTopSmall, full.photoTop); + const auto photo = lerp(st.photo, full.photo); + const auto summaryTop = st.nameTop + - (st.photoTop + (st.photo / 2.)) + + (photoTop + (photo / 2.)); + const auto nameScale = layout.shownHeight / float64(full.height); const auto nameTop = nameScale * full.nameTop; const auto nameWidth = nameScale * AvailableNameWidth(); const auto nameHeight = nameScale * full.nameStyle.font->height; - const auto nameLeft = photoLeft + (photo - nameWidth) / 2.; + const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); auto p = QPainter(this); p.fillRect(e->rect(), st::dialogsBg); - p.translate(0, height() - shownHeight); + p.translate(0, height() - layout.shownHeight); const auto drawSmall = (ratio < 1.); const auto drawFull = (ratio > 0.); @@ -270,8 +317,8 @@ void List::paintEvent(QPaintEvent *e) { paintSummary(p, rendering, summaryTop, ratio); const auto count = std::max( - endIndexFull - startIndexFull, - endIndexSmall - startIndexSmall); + layout.endIndexFull - layout.startIndexFull, + layout.endIndexSmall - layout.startIndexSmall); struct Single { float64 x = 0.; @@ -285,15 +332,15 @@ void List::paintEvent(QPaintEvent *e) { } }; const auto lookup = [&](int index) { - const auto indexSmall = startIndexSmall + index; - const auto indexFull = startIndexFull + index; - const auto small = (drawSmall && indexSmall < endIndexSmall) + const auto indexSmall = layout.startIndexSmall + index; + const auto indexFull = layout.startIndexFull + index; + const auto small = (drawSmall && indexSmall < layout.endIndexSmall) ? &rendering.items[indexSmall] : nullptr; - const auto full = (drawFull && indexFull < endIndexFull) + const auto full = (drawFull && indexFull < layout.endIndexFull) ? &rendering.items[indexFull] : nullptr; - const auto x = left + single * index; + const auto x = layout.left + single * index; return Single{ x, indexSmall, small, indexFull, full }; }; const auto hasUnread = [&](const Single &single) { @@ -334,7 +381,7 @@ void List::paintEvent(QPaintEvent *e) { // Unread gradient. const auto x = single.x; - const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; const auto smallUnread = small && small->user.unread; @@ -367,7 +414,7 @@ void List::paintEvent(QPaintEvent *e) { Expects(single.itemSmall || single.itemFull); const auto x = single.x; - const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; const auto smallUnread = small && small->user.unread; @@ -549,7 +596,7 @@ void List::wheelEvent(QWheelEvent *e) { if (next != now) { _expandRequests.fire({}); _scrollLeft = next; - //updateSelected(); + updateSelected(); update(); } e->accept(); @@ -559,13 +606,16 @@ void List::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; } - _mouseDownPosition = _lastMousePosition = e->globalPos(); - //updateSelected(); + _lastMousePosition = e->globalPos(); + updateSelected(); + + _mouseDownPosition = _lastMousePosition; + _pressed = _selected; } void List::mouseMoveEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); - //updateSelected(); + updateSelected(); if (!_dragging && _mouseDownPosition) { if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() @@ -601,11 +651,18 @@ void List::mouseReleaseEvent(QMouseEvent *e) { _mouseDownPosition = std::nullopt; }); - //const auto wasDown = std::exchange(_pressed, SpecialOver::None); + const auto pressed = std::exchange(_pressed, -1); if (finishDragging()) { return; } - //updateSelected(); + updateSelected(); + if (_selected == pressed) { + if (_selected < 0) { + _expandRequests.fire({}); + } else if (_selected < _data.items.size()) { + _clicks.fire_copy(_data.items[_selected].user.id); + } + } } bool List::finishDragging() { @@ -614,8 +671,62 @@ bool List::finishDragging() { } checkDragging(); _dragging = false; - //updateSelected(); + updateSelected(); return true; } +void List::updateSelected() { + if (_pressed >= 0) { + return; + } + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto p = mapFromGlobal(_lastMousePosition); + const auto layout = computeLayout(); + const auto firstRightFull = full.left + layout.singleFull; + const auto firstRightSmall = st.left + + st.photoLeft + + st.photo; + const auto stepFull = layout.singleFull; + const auto stepSmall = st.shift; + const auto lastRightAddFull = 0; + const auto lastRightAddSmall = st.photoLeft; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * layout.ratio; + }; + const auto firstRight = lerp(firstRightSmall, firstRightFull); + const auto step = lerp(stepSmall, stepFull); + const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull); + const auto activateFull = (layout.ratio >= 0.5); + const auto startIndex = activateFull + ? layout.startIndexFull + : layout.startIndexSmall; + const auto endIndex = activateFull + ? layout.endIndexFull + : layout.endIndexSmall; + const auto x = p.x(); + const auto infiniteIndex = (x < firstRight) + ? 0 + : int(std::floor(((x - firstRight) / step) + 1)); + const auto index = (endIndex == startIndex) + ? -1 + : (infiniteIndex == endIndex - startIndex + && x < firstRight + + (endIndex - startIndex - 1) * step + + lastRightAdd) + ? (infiniteIndex - 1) // Last small part should still be clickable. + : infiniteIndex; + const auto selected = (index < 0 + || startIndex + index >= layout.itemsCount) + ? -1 + : (startIndex + index); + if (_selected != selected) { + const auto over = (selected >= 0); + if (over != (_selected >= 0)) { + setCursor(over ? style::cur_pointer : style::cur_default); + } + _selected = selected; + } +} + } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 636f855f7..1286db93e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -49,6 +49,7 @@ public: [[nodiscard]] rpl::producer<> entered() const; private: + struct Layout; struct Item { User user; QImage nameCache; @@ -106,6 +107,7 @@ private: void validateName(not_null item); void updateScrollMax(); void updateSummary(Data &data); + void updateSelected(); void checkDragging(); bool finishDragging(); @@ -117,6 +119,8 @@ private: float64 summaryTop, float64 hidden); + [[nodiscard]] Layout computeLayout() const; + Content _content; Data _data; Data _hidingData; @@ -134,6 +138,9 @@ private: int _scrollLeftMax = 0; bool _dragging = false; + int _selected = -1; + int _pressed = -1; + }; } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 82d1696e1..f68680c43 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" #include "media/stories/media_stories_caption_full_view.h" @@ -127,7 +128,9 @@ Controller::Controller(not_null delegate) focused ? 0. : 1., focused ? 1. : 0., st::fadeWrapDuration); - togglePaused(focused); + if (_started) { + togglePaused(focused); + } }, _lifetime); _contentFadeAnimation.stop(); @@ -344,15 +347,23 @@ void Controller::show( int index, int subindex) { Expects(index >= 0 && index < lists.size()); - Expects(subindex >= 0 && subindex < lists[index].items.size()); + Expects(subindex >= 0 && subindex < lists[index].ids.size()); showSiblings(lists, index); const auto &list = lists[index]; - const auto &item = list.items[subindex]; + const auto id = list.ids[subindex]; + const auto maybeStory = list.user->owner().stories().lookup({ + .peer = list.user->id, + .story = id, + }); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; const auto guard = gsl::finally([&] { _started = false; - if (v::is>(item.media.data)) { + if (story->photo()) { _photoPlayback = std::make_unique(this); } else { _photoPlayback = nullptr; @@ -363,20 +374,20 @@ void Controller::show( } _index = subindex; - const auto id = FullStoryId{ + const auto storyId = FullStoryId{ .peer = list.user->id, - .story = item.id, + .story = id, }; - if (_shown == id) { + if (_shown == storyId) { return; } - _shown = id; - _captionText = item.caption; + _shown = storyId; + _captionText = story->caption(); _captionFullView = nullptr; - _header->show({ .user = list.user, .date = item.date }); + _header->show({ .user = list.user, .date = story->date() }); _slider->show({ .index = _index, .total = list.total }); - _replyArea->show({ .user = list.user, .id = id.story }); + _replyArea->show({ .user = list.user, .id = id }); if (_contentFaded) { togglePaused(true); @@ -395,7 +406,7 @@ void Controller::showSiblings( void Controller::showSibling( std::unique_ptr &sibling, const Data::StoriesList *list) { - if (!list || list->items.empty()) { + if (!list || list->ids.empty()) { sibling = nullptr; } else if (!sibling || !sibling->shows(*list)) { sibling = std::make_unique(this, *list); @@ -407,7 +418,7 @@ void Controller::ready() { return; } _started = true; - if (_photoPlayback) { + if (!_contentFaded && _photoPlayback) { _photoPlayback->togglePaused(false); } } @@ -445,23 +456,23 @@ bool Controller::subjumpFor(int delta) { if (index < 0) { if (_siblingLeft && _siblingLeft->shownId().valid()) { return jumpFor(-1); - } else if (!_list || _list->items.empty()) { + } else if (!_list || _list->ids.empty()) { return false; } _delegate->storiesJumpTo(&_list->user->session(), { .peer = _list->user->id, - .story = _list->items.front().id + .story = _list->ids.front() }); return true; } else if (index >= _list->total) { return _siblingRight && _siblingRight->shownId().valid() && jumpFor(1); - } else if (index < _list->items.size()) { + } else if (index < _list->ids.size()) { // #TODO stories load more _delegate->storiesJumpTo(&_list->user->session(), { .peer = _list->user->id, - .story = _list->items[index].id + .story = _list->ids[index] }); } return true; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 4b5e09bde..ac3c43258 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -217,29 +217,47 @@ Sibling::Sibling( not_null controller, const Data::StoriesList &list) : _controller(controller) -, _id{ list.user->id, list.items.front().id } +, _id{ list.user->id, list.ids.front() } , _peer(list.user) { - const auto &item = list.items.front(); - const auto &data = item.media.data; - const auto origin = Data::FileOrigin(); - if (const auto video = std::get_if>(&data)) { - _loader = std::make_unique((*video), origin, [=] { - check(); - }); - } else if (const auto photo = std::get_if>(&data)) { - _loader = std::make_unique((*photo), origin, [=] { - check(); - }); - } else { - Unexpected("Media type in stories list."); - } - _blurred = _loader->blurred(); - check(); + checkStory(); _goodShown.stop(); } Sibling::~Sibling() = default; +void Sibling::checkStory() { + const auto maybeStory = _peer->owner().stories().lookup(_id); + if (!maybeStory) { + if (_blurred.isNull()) { + _blurred = QImage( + st::storiesMaxSize, + QImage::Format_ARGB32_Premultiplied); + _blurred.fill(Qt::black); + + if (maybeStory.error() == Data::NoStory::Unknown) { + _peer->owner().stories().resolve(_id, crl::guard(this, [=] { + checkStory(); + })); + } + } + return; + } + const auto story = *maybeStory; + const auto &data = story->media().data; + const auto origin = Data::FileOrigin(); + v::match(story->media().data, [&](not_null photo) { + _loader = std::make_unique(photo, origin, [=] { + check(); + }); + }, [&](not_null document) { + _loader = std::make_unique(document, origin, [=] { + check(); + }); + }); + _blurred = _loader->blurred(); + check(); +} + FullStoryId Sibling::shownId() const { return _id; } @@ -249,9 +267,9 @@ not_null Sibling::peer() const { } bool Sibling::shows(const Data::StoriesList &list) const { - Expects(!list.items.empty()); + Expects(!list.ids.empty()); - return _id == FullStoryId{ list.user->id, list.items.front().id }; + return _id == FullStoryId{ list.user->id, list.ids.front() }; } SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h index dc355fec6..f6eb4edc7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -7,8 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" #include "data/data_stories.h" - #include "ui/effects/animations.h" #include "ui/userpic_view.h" @@ -22,7 +22,7 @@ class Controller; struct SiblingView; struct SiblingLayout; -class Sibling final { +class Sibling final : public base::has_weak_ptr { public: Sibling( not_null controller, @@ -42,6 +42,7 @@ private: class LoaderPhoto; class LoaderVideo; + void checkStory(); void check(); [[nodiscard]] QImage userpicImage(const SiblingLayout &layout); diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index ad9101976..79b835461 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -14,6 +14,10 @@ class PeerData; class PhotoData; class HistoryItem; +namespace Data { +class Story; +} // namespace Data + namespace Window { class SessionController; } // namespace Window @@ -67,6 +71,17 @@ public: , _cloudTheme(cloudTheme) { } + OpenRequest( + Window::SessionController *controller, + not_null story, + bool continueStreaming = false, + crl::time startTime = 0) + : _controller(controller) + , _story(story) + , _continueStreaming(continueStreaming) + , _startTime(startTime) { + } + [[nodiscard]] PeerData *peer() const { return _peer; } @@ -87,6 +102,10 @@ public: return _document; } + [[nodiscard]] Data::Story *story() const { + return _story; + } + [[nodiscard]] std::optional cloudTheme() const { return _cloudTheme; } @@ -107,6 +126,7 @@ private: Window::SessionController *_controller = nullptr; DocumentData *_document = nullptr; PhotoData *_photo = nullptr; + Data::Story *_story = nullptr; PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1bd915558..c80d42841 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3036,8 +3036,9 @@ void OverlayWidget::activate() { } void OverlayWidget::show(OpenRequest request) { - const auto document = request.document(); - const auto photo = request.photo(); + const auto story = request.story(); + const auto document = story ? story->document() : request.document(); + const auto photo = story ? story->photo() : request.photo(); const auto contextItem = request.item(); const auto contextPeer = request.peer(); const auto contextTopicRootId = request.topicRootId(); @@ -3057,15 +3058,9 @@ void OverlayWidget::show(OpenRequest request) { } setSession(&photo->session()); - // #TODO stories testing - if (const auto storyId = (!contextPeer && contextItem) - ? contextItem->history()->owner().stories().generate( - contextItem, - photo) - : StoryId()) { - setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); - } else - if (contextPeer) { + if (story) { + setContext(StoriesContext{ story->peer(), story->id() }); + } else if (contextPeer) { setContext(contextPeer); } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); @@ -3083,15 +3078,9 @@ void OverlayWidget::show(OpenRequest request) { } else if (document) { setSession(&document->session()); - // #TODO stories testing - if (const auto storyId = contextItem - ? contextItem->history()->owner().stories().generate( - contextItem, - document) - : StoryId()) { - setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); - } else - if (contextItem) { + if (story) { + setContext(StoriesContext{ story->peer(), story->id() }); + } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); } else { setContext(v::null); @@ -4034,30 +4023,20 @@ void OverlayWidget::storiesJumpTo( Expects(_stories != nullptr); Expects(id.valid()); - const auto &all = session->data().stories().all(); - const auto i = ranges::find( - all, - id.peer, - [](const Data::StoriesList &list) { return list.user->id; }); - if (i == end(all)) { + const auto maybeStory = session->data().stories().lookup(id); + if (!maybeStory) { close(); return; } - const auto j = ranges::find(i->items, id.story, &Data::StoryItem::id); - if (j == end(i->items)) { - close(); - return; - } - setContext(StoriesContext{ i->user, id.story }); + const auto story = *maybeStory; + setContext(StoriesContext{ story->peer(), story->id() }); clearStreaming(); _streamingStartPaused = false; - const auto &data = j->media.data; - const auto activation = anim::activation::background; - if (const auto photo = std::get_if>(&data)) { - displayPhoto(*photo, activation); - } else { - displayDocument(v::get>(data), activation); - } + v::match(story->media().data, [&](not_null photo) { + displayPhoto(photo, anim::activation::background); + }, [&](not_null document) { + displayDocument(document, anim::activation::background); + }); } void OverlayWidget::storiesClose() { @@ -4976,36 +4955,33 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } else if (const auto peer = std::get_if>(&context)) { _peer = *peer; _history = _peer->owner().history(_peer); _message = nullptr; _topicRootId = MsgId(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } else if (const auto story = std::get_if(&context)) { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - const auto &all = story->user->owner().stories().all(); + const auto &all = story->peer->owner().stories().all(); const auto i = ranges::find( all, - story->user, + story->peer, &Data::StoriesList::user); Assert(i != end(all)); - const auto j = ranges::find( - i->items, - story->id, - &Data::StoryItem::id); - setStoriesUser(story->user); - _stories->show(all, (i - begin(all)), j - begin(i->items)); + const auto j = ranges::find(i->ids, story->id); + setStoriesPeer(story->peer); + _stories->show(all, (i - begin(all)), j - begin(i->ids)); } else { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } _migrated = nullptr; if (_history) { @@ -5020,11 +4996,11 @@ void OverlayWidget::setContext( _user = _peer ? _peer->asUser() : nullptr; } -void OverlayWidget::setStoriesUser(UserData *user) { - const auto session = user ? &user->session() : nullptr; +void OverlayWidget::setStoriesPeer(PeerData *peer) { + const auto session = peer ? &peer->session() : nullptr; if (!session && !_storiesSession) { Assert(!_stories); - } else if (!user) { + } else if (!peer) { _stories = nullptr; _storiesSession = nullptr; _storiesChanged.fire({}); @@ -5099,14 +5075,6 @@ bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) { if (v::is_null(entity.data) && !entity.item) { return false; } - // #TODO stories testing - if (const auto storyId = entity.item - ? entity.item->history()->owner().stories().generate( - entity.item, - entity.data) - : StoryId()) { - setContext(StoriesContext{ entity.item->from()->asUser(), storyId }); - } else if (const auto item = entity.item) { setContext(ItemContext{ item, entity.topicRootId }); } else if (_peer) { @@ -5765,7 +5733,7 @@ void OverlayWidget::clearBeforeHide() { _collage = nullptr; _collageData = std::nullopt; clearStreaming(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); _layerBg->hideAll(anim::type::instant); assignMediaPointer(nullptr); _preloadPhotos.clear(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index a82aa1f3c..2bc572a91 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -303,7 +303,7 @@ private: MsgId topicRootId = 0; }; struct StoriesContext { - not_null user; + not_null peer; StoryId id = 0; }; void setContext(std::variant< @@ -311,7 +311,7 @@ private: ItemContext, not_null, StoriesContext> context); - void setStoriesUser(UserData *user); + void setStoriesPeer(PeerData *peer); void refreshLang(); void showSaveMsgFile(); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 34d9ca1be..7ca01634e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_replies_list.h" #include "data/data_peer_values.h" +#include "data/data_stories.h" #include "passport/passport_form_controller.h" #include "chat_helpers/tabbed_selector.h" #include "chat_helpers/emoji_interactions.h" @@ -2463,6 +2464,22 @@ Ui::ChatThemeBackgroundData SessionController::backgroundData( }; } +void SessionController::openPeerStories(PeerId peerId) { + using namespace Media::View; + using namespace Data; + + auto &stories = session().data().stories(); + const auto &all = stories.all(); + const auto i = ranges::find(all, peerId, [](const StoriesList &list) { + return list.user->id; + }); + if (i != end(all) && !i->ids.empty()) { + if (const auto from = stories.lookup({ peerId, i->ids.front() })) { + window().openInMediaView(OpenRequest(this, *from)); + } + } +} + HistoryView::PaintContext SessionController::preparePaintContext( PaintContextArgs &&args) { const auto visibleAreaTopLocal = content()->mapFromGlobal( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 5258e7b8e..8f6ed90ec 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -564,6 +564,8 @@ public: return _peerThemeOverride.value(); } + void openPeerStories(PeerId peerId); + struct PaintContextArgs { not_null theme; int visibleAreaTop = 0;