diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1a76befc09..5a095f698b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -970,6 +970,8 @@ PRIVATE media/stories/media_stories_header.h media/stories/media_stories_reply.cpp media/stories/media_stories_reply.h + media/stories/media_stories_sibling.cpp + media/stories/media_stories_sibling.h media/stories/media_stories_slider.cpp media/stories/media_stories_slider.h media/stories/media_stories_view.cpp diff --git a/Telegram/Resources/icons/mediaview/stories_next.png b/Telegram/Resources/icons/mediaview/stories_next.png new file mode 100644 index 0000000000..dd997b2d08 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next@2x.png b/Telegram/Resources/icons/mediaview/stories_next@2x.png new file mode 100644 index 0000000000..82b819e9e5 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next@3x.png b/Telegram/Resources/icons/mediaview/stories_next@3x.png new file mode 100644 index 0000000000..3550c8cce4 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next@3x.png differ diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index c5634e4ac5..e7274fb04e 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -173,7 +173,7 @@ StoryId Stories::generate( const auto itemId = item->id; const auto peer = item->history()->peer; const auto session = &peer->session(); - auto stories = StoriesList{ .user = item->from()->asUser() }; + auto full = std::vector(); const auto lifetime = session->storage().query(SharedMediaQuery( SharedMediaKey(peer->id, MsgId(0), listType, itemId), 32, @@ -182,21 +182,33 @@ StoryId Stories::generate( if (!result.messageIds.contains(itemId)) { result.messageIds.emplace(itemId); } - stories.items.reserve(result.messageIds.size()); 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) }), + : 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(); @@ -209,18 +221,21 @@ StoryId Stories::generate( .caption = item->originalText(), .date = item->date(), }); + ++stories.total; } } } } - stories.total = std::max( - result.count.value_or(1), - int(result.messageIds.size())); - 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)); + 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; diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 26f1f76a2f..373f90b95e 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -46,9 +46,12 @@ struct FullStoryId { UserData *user = nullptr; StoryId id = 0; - explicit operator bool() const { + [[nodiscard]] bool valid() const { return user != nullptr && id != 0; } + explicit operator bool() const { + return valid(); + } friend inline auto operator<=>(FullStoryId, FullStoryId) = default; friend inline bool operator==(FullStoryId, FullStoryId) = default; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index f88ebf8d39..c653ffb7fb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" +#include "media/stories/media_stories_sibling.h" #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_reply.h" +#include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" #include "ui/rp_widget.h" #include "styles/style_media_view.h" @@ -25,6 +27,7 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); +constexpr auto kSiblingMultiplier = 0.448; } // namespace @@ -115,12 +118,15 @@ void Controller::initLayout() { const auto sliderHeight = st::storiesSliderMargin.top() + st::storiesSliderWidth + st::storiesSliderMargin.bottom(); - const auto outsideHeaderHeight = headerHeight + sliderHeight; + const auto outsideHeaderHeight = headerHeight + + sliderHeight + + st::storiesSliderOutsideSkip; const auto fieldMinHeight = st::storiesFieldMargin.top() + st::storiesAttach.height + st::storiesFieldMargin.bottom(); - const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + const auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom() + outsideHeaderHeight + + st::storiesMaxSize.height() + fieldMinHeight; _layout = _wrap->sizeValue( @@ -134,9 +140,10 @@ void Controller::initLayout() { ? HeaderLayout::Outside : HeaderLayout::Normal; - const auto topSkip = (layout.headerLayout == HeaderLayout::Outside) - ? outsideHeaderHeight - : st::storiesFieldMargin.bottom(); + const auto topSkip = st::storiesFieldMargin.bottom() + + (layout.headerLayout == HeaderLayout::Outside + ? outsideHeaderHeight + : 0); const auto bottomSkip = fieldMinHeight; const auto maxWidth = size.width() - 2 * st::storiesSideSkip; const auto availableHeight = size.height() - topSkip - bottomSkip; @@ -187,6 +194,16 @@ void Controller::initLayout() { layout.controlsWidth, layout.controlsBottomPosition.y()); + const auto siblingSize = layout.content.size() * kSiblingMultiplier; + const auto siblingTop = layout.content.y() + + (layout.content.height() - siblingSize.height()) / 2; + layout.siblingLeft = QRect( + { -siblingSize.width() / 3, siblingTop }, + siblingSize); + layout.siblingRight = QRect( + { size.width() - (2 * siblingSize.width() / 3), siblingTop }, + siblingSize); + return layout; }); } @@ -214,11 +231,19 @@ auto Controller::stickerOrEmojiChosen() const return _delegate->storiesStickerOrEmojiChosen(); } -void Controller::show(const Data::StoriesList &list, int index) { - Expects(index < list.items.size()); +void Controller::show( + const std::vector &lists, + int index, + int subindex) { + Expects(index >= 0 && index < lists.size()); + Expects(subindex >= 0 && subindex < lists[index].items.size()); - const auto &item = list.items[index]; + showSiblings(lists, index); + + const auto &list = lists[index]; + const auto &item = list.items[subindex]; const auto guard = gsl::finally([&] { + _started = false; if (v::is>(item.media.data)) { _photoPlayback = std::make_unique(this); } else { @@ -228,7 +253,7 @@ void Controller::show(const Data::StoriesList &list, int index) { if (_list != list) { _list = list; } - _index = index; + _index = subindex; const auto id = Data::FullStoryId{ .user = list.user, @@ -240,11 +265,34 @@ void Controller::show(const Data::StoriesList &list, int index) { _shown = id; _header->show({ .user = list.user, .date = item.date }); - _slider->show({ .index = index, .total = int(list.items.size()) }); + _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user }); } +void Controller::showSiblings( + const std::vector &lists, + int index) { + showSibling(_siblingLeft, (index > 0) ? &lists[index - 1] : nullptr); + showSibling( + _siblingRight, + (index + 1 < lists.size()) ? &lists[index + 1] : nullptr); +} + +void Controller::showSibling( + std::unique_ptr &sibling, + const Data::StoriesList *list) { + if (!list || list->items.empty()) { + sibling = nullptr; + } else if (!sibling || !sibling->shows(*list)) { + sibling = std::make_unique(this, *list); + } +} + void Controller::ready() { + if (_started) { + return; + } + _started = true; if (_photoPlayback) { _photoPlayback->togglePaused(false); } @@ -262,25 +310,28 @@ void Controller::updatePlayback(const Player::TrackState &state) { _slider->updatePlayback(state); updatePowerSaveBlocker(state); if (Player::IsStoppedAtEnd(state.state)) { - if (!jumpFor(1)) { + if (!subjumpFor(1)) { _delegate->storiesJumpTo({}); } } } -bool Controller::jumpAvailable(int delta) const { - if (delta == -1) { - // Always allow to jump back for one. - // In case of the first story just jump to the beginning. - return _list && !_list->items.empty(); - } +bool Controller::subjumpAvailable(int delta) const { const auto index = _index + delta; + if (index < 0) { + return _siblingLeft && _siblingLeft->shownId().valid(); + } else if (index >= _list->total) { + return _siblingRight && _siblingRight->shownId().valid(); + } return index >= 0 && index < _list->total; } -bool Controller::jumpFor(int delta) { - if (!_index && delta == -1) { - if (!_list || _list->items.empty()) { +bool Controller::subjumpFor(int delta) { + const auto index = _index + delta; + if (index < 0) { + if (_siblingLeft->shownId().valid()) { + return jumpFor(-1); + } else if (!_list || _list->items.empty()) { return false; } _delegate->storiesJumpTo({ @@ -288,10 +339,8 @@ bool Controller::jumpFor(int delta) { .id = _list->items.front().id }); return true; - } - const auto index = _index + delta; - if (index < 0 || index >= _list->total) { - return false; + } else if (index >= _list->total) { + return _siblingRight->shownId().valid() && jumpFor(1); } else if (index < _list->items.size()) { // #TODO stories load more _delegate->storiesJumpTo({ @@ -302,6 +351,22 @@ bool Controller::jumpFor(int delta) { return true; } + +bool Controller::jumpFor(int delta) { + if (delta == -1) { + if (const auto left = _siblingLeft.get()) { + _delegate->storiesJumpTo(left->shownId()); + return true; + } + } else if (delta == 1) { + if (const auto right = _siblingRight.get()) { + _delegate->storiesJumpTo(right->shownId()); + return true; + } + } + return false; +} + bool Controller::paused() const { return _photoPlayback ? _photoPlayback->paused() @@ -316,6 +381,30 @@ void Controller::togglePaused(bool paused) { } } +void Controller::repaintSibling(not_null sibling) { + if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) { + _delegate->storiesRepaint(); + } +} + +SiblingView Controller::siblingLeft() const { + if (const auto value = _siblingLeft.get()) { + return { value->image(), _layout.current()->siblingLeft }; + } + return {}; +} + +SiblingView Controller::siblingRight() const { + if (const auto value = _siblingRight.get()) { + return { value->image(), _layout.current()->siblingRight }; + } + return {}; +} + +rpl::lifetime &Controller::lifetime() { + return _lifetime; +} + void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { const auto block = !Player::IsPausedOrPausing(state.state) && !Player::IsStoppedOrStopping(state.state); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 2d9abb80c5..dc80c46674 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -35,7 +35,9 @@ namespace Media::Stories { class Header; class Slider; class ReplyArea; +class Sibling; class Delegate; +struct SiblingView; enum class HeaderLayout { Normal, @@ -50,6 +52,8 @@ struct Layout { QPoint controlsBottomPosition; QRect autocompleteRect; HeaderLayout headerLayout = HeaderLayout::Normal; + QRect siblingLeft; + QRect siblingRight; friend inline auto operator<=>(Layout, Layout) = default; friend inline bool operator==(Layout, Layout) = default; @@ -68,16 +72,26 @@ public: [[nodiscard]] auto stickerOrEmojiChosen() const -> rpl::producer; - void show(const Data::StoriesList &list, int index); + void show( + const std::vector &lists, + int index, + int subindex); void ready(); void updateVideoPlayback(const Player::TrackState &state); - [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool subjumpAvailable(int delta) const; + [[nodiscard]] bool subjumpFor(int delta); [[nodiscard]] bool jumpFor(int delta); [[nodiscard]] bool paused() const; void togglePaused(bool paused); + void repaintSibling(not_null sibling); + [[nodiscard]] SiblingView siblingLeft() const; + [[nodiscard]] SiblingView siblingRight() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + private: class PhotoPlayback; @@ -86,6 +100,13 @@ private: void updatePlayback(const Player::TrackState &state); void updatePowerSaveBlocker(const Player::TrackState &state); + void showSiblings( + const std::vector &lists, + int index); + void showSibling( + std::unique_ptr &sibling, + const Data::StoriesList *list); + const not_null _delegate; rpl::variable> _layout; @@ -94,11 +115,15 @@ private: const std::unique_ptr
_header; const std::unique_ptr _slider; const std::unique_ptr _replyArea; + std::unique_ptr _photoPlayback; Data::FullStoryId _shown; std::optional _list; int _index = 0; - std::unique_ptr _photoPlayback; + bool _started = false; + + std::unique_ptr _siblingLeft; + std::unique_ptr _siblingRight; std::unique_ptr _powerSaveBlocker; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index 3a2d700f63..256aff2f2a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -37,6 +37,7 @@ public: virtual void storiesJumpTo(Data::FullStoryId id) = 0; [[nodiscard]] virtual bool storiesPaused() = 0; virtual void storiesTogglePaused(bool paused) = 0; + virtual void storiesRepaint() = 0; }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp new file mode 100644 index 0000000000..0064850882 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -0,0 +1,262 @@ +/* +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 "media/stories/media_stories_sibling.h" + +#include "base/weak_ptr.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "media/stories/media_stories_controller.h" +#include "media/streaming/media_streaming_instance.h" +#include "media/streaming/media_streaming_player.h" + +namespace Media::Stories { +namespace { + +constexpr auto kGoodFadeDuration = crl::time(200); + +} // namespace + +class Sibling::Loader { +public: + virtual ~Loader() = default; + + virtual QImage blurred() = 0; + virtual QImage good() = 0; +}; + +class Sibling::LoaderPhoto final : public Sibling::Loader { +public: + LoaderPhoto( + not_null photo, + Data::FileOrigin origin, + Fn update); + + QImage blurred() override; + QImage good() override; + +private: + const not_null _photo; + const Fn _update; + std::shared_ptr _media; + rpl::lifetime _waitingLoading; + +}; + +class Sibling::LoaderVideo final + : public Sibling::Loader + , public base::has_weak_ptr { +public: + LoaderVideo( + not_null video, + Data::FileOrigin origin, + Fn update); + + QImage blurred() override; + QImage good() override; + +private: + void waitForGoodThumbnail(); + bool updateAfterGoodCheck(); + void streamedFailed(); + + const not_null _video; + const Data::FileOrigin _origin; + const Fn _update; + std::shared_ptr _media; + std::unique_ptr _streamed; + rpl::lifetime _waitingGoodGeneration; + bool _checkingGoodInCache = false; + bool _failed = false; + +}; + +Sibling::LoaderPhoto::LoaderPhoto( + not_null photo, + Data::FileOrigin origin, + Fn update) +: _photo(photo) +, _update(std::move(update)) +, _media(_photo->createMediaView()) { + _photo->load(origin, LoadFromCloudOrLocal, true); +} + +QImage Sibling::LoaderPhoto::blurred() { + if (const auto image = _media->thumbnailInline()) { + return image->original(); + } + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::black); + result.setDevicePixelRatio(ratio); + return result; +} + +QImage Sibling::LoaderPhoto::good() { + if (const auto image = _media->image(Data::PhotoSize::Large)) { + return image->original(); + } else if (!_waitingLoading) { + _photo->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + if (_media->loaded()) { + _update(); + } + }, _waitingLoading); + } + return QImage(); +} + +Sibling::LoaderVideo::LoaderVideo( + not_null video, + Data::FileOrigin origin, + Fn update) +: _video(video) +, _origin(origin) +, _update(std::move( update)) +, _media(_video->createMediaView()) { + _media->goodThumbnailWanted(); +} + +QImage Sibling::LoaderVideo::blurred() { + if (const auto image = _media->thumbnailInline()) { + return image->original(); + } + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::black); + result.setDevicePixelRatio(ratio); + return result; +} + +QImage Sibling::LoaderVideo::good() { + if (const auto image = _media->goodThumbnail()) { + return image->original(); + } else if (!_video->goodThumbnailChecked()) { + if (!_checkingGoodInCache) { + waitForGoodThumbnail(); + } + } else if (_failed) { + return QImage(); + } else if (!_streamed) { + _streamed = std::make_unique( + _video, + _origin, + [] {}); // waitingCallback + _streamed->lockPlayer(); + _streamed->player().updates( + ) | rpl::start_with_next_error([=](Streaming::Update &&update) { + v::match(update.data, [&](Streaming::Information &update) { + _update(); + }, [](const auto &update) { + }); + }, [=](Streaming::Error &&error) { + streamedFailed(); + }, _streamed->lifetime()); + if (_streamed->ready()) { + _update(); + } else if (!_streamed->valid()) { + streamedFailed(); + } + } else if (_streamed->ready()) { + return _streamed->info().video.cover; + } + return QImage(); +} + +void Sibling::LoaderVideo::streamedFailed() { + _failed = true; + _streamed = nullptr; + _update(); +} + +void Sibling::LoaderVideo::waitForGoodThumbnail() { + _checkingGoodInCache = true; + const auto weak = make_weak(this); + _video->owner().cache().get({}, [=](const auto &) { + crl::on_main([=] { + if (const auto strong = weak.get()) { + if (!strong->updateAfterGoodCheck()) { + strong->_video->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + strong->updateAfterGoodCheck(); + }, strong->_waitingGoodGeneration); + } + } + }); + }); +} + +bool Sibling::LoaderVideo::updateAfterGoodCheck() { + if (!_video->goodThumbnailChecked()) { + return false; + } + _checkingGoodInCache = false; + _waitingGoodGeneration.destroy(); + _update(); + return true; +} + +Sibling::Sibling( + not_null controller, + const Data::StoriesList &list) +: _controller(controller) +, _id{ list.user, list.items.front().id } { + 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(); + _goodShown.stop(); +} + +Sibling::~Sibling() = default; + +Data::FullStoryId Sibling::shownId() const { + return _id; +} + +bool Sibling::shows(const Data::StoriesList &list) const { + Expects(!list.items.empty()); + + return _id == Data::FullStoryId{ list.user, list.items.front().id }; +} + +QImage Sibling::image() const { + return _good.isNull() ? _blurred : _good; +} + +void Sibling::check() { + Expects(_loader != nullptr); + + auto good = _loader->good(); + if (good.isNull()) { + return; + } + _loader = nullptr; + _good = std::move(good); + _goodShown.start([=] { + _controller->repaintSibling(this); + }, 0., 1., kGoodFadeDuration, anim::linear); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h new file mode 100644 index 0000000000..ad3b961d9f --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -0,0 +1,48 @@ +/* +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_stories.h" + +#include "ui/effects/animations.h" + +namespace Media::Stories { + +class Controller; + +class Sibling final { +public: + Sibling( + not_null controller, + const Data::StoriesList &list); + ~Sibling(); + + [[nodiscard]] Data::FullStoryId shownId() const; + [[nodiscard]] bool shows(const Data::StoriesList &list) const; + + [[nodiscard]] QImage image() const; + +private: + class Loader; + class LoaderPhoto; + class LoaderVideo; + + void check(); + + const not_null _controller; + + Data::FullStoryId _id; + QImage _blurred; + QImage _good; + Ui::Animations::Simple _goodShown; + + std::unique_ptr _loader; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp index 57f16ddd10..f4dc579562 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -136,7 +136,9 @@ void Slider::paint(QRectF clip) { radius, radius); } else { - p.setOpacity(kOpacityInactive); + p.setOpacity((i < _data.index) + ? kOpacityActive + : kOpacityInactive); p.drawRoundedRect(_rects[i], radius, radius); } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index cfe82693cb..19b4fddfe1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -21,8 +21,11 @@ View::View(not_null delegate) View::~View() = default; -void View::show(const Data::StoriesList &list, int index) { - _controller->show(list, index); +void View::show( + const std::vector &lists, + int index, + int subindex) { + _controller->show(lists, index, subindex); } void View::ready() { @@ -33,12 +36,23 @@ QRect View::contentGeometry() const { return _controller->layout().content; } +rpl::producer View::contentGeometryValue() const { + return _controller->layoutValue( + ) | rpl::map([=](const Layout &layout) { + return layout.content; + }) | rpl::distinct_until_changed(); +} + void View::updatePlayback(const Player::TrackState &state) { _controller->updateVideoPlayback(state); } -bool View::jumpAvailable(int delta) const { - return _controller->jumpAvailable(delta); +bool View::subjumpAvailable(int delta) const { + return _controller->subjumpAvailable(delta); +} + +bool View::subjumpFor(int delta) const { + return _controller->subjumpFor(delta); } bool View::jumpFor(int delta) const { @@ -53,4 +67,16 @@ void View::togglePaused(bool paused) { _controller->togglePaused(paused); } +SiblingView View::siblingLeft() const { + return _controller->siblingLeft(); +} + +SiblingView View::siblingRight() const { + return _controller->siblingRight(); +} + +rpl::lifetime &View::lifetime() { + return _controller->lifetime(); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 96f4e00427..f225d42adb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -20,24 +20,45 @@ namespace Media::Stories { class Delegate; class Controller; +struct SiblingView { + QImage image; + QRect geometry; + + [[nodiscard]] bool valid() const { + return !image.isNull(); + } + explicit operator bool() const { + return valid(); + } +}; + class View final { public: explicit View(not_null delegate); ~View(); - void show(const Data::StoriesList &list, int index); + void show( + const std::vector &lists, + int index, + int subindex); void ready(); [[nodiscard]] QRect contentGeometry() const; + [[nodiscard]] rpl::producer contentGeometryValue() const; + [[nodiscard]] SiblingView siblingLeft() const; + [[nodiscard]] SiblingView siblingRight() const; void updatePlayback(const Player::TrackState &state); - [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool subjumpAvailable(int delta) const; + [[nodiscard]] bool subjumpFor(int delta) const; [[nodiscard]] bool jumpFor(int delta) const; [[nodiscard]] bool paused() const; void togglePaused(bool paused); + [[nodiscard]] rpl::lifetime &lifetime(); + private: const std::unique_ptr _controller; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 19dd467781..3f3c9f5951 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,10 +406,14 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); +storiesControlSize: 64px; +storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; +storiesRight: icon {{ "mediaview/stories_next", mediaviewControlFg }}; storiesSliderWidth: 2px; -storiesSliderMargin: margins(8px, 7px, 8px, 11px); +storiesSliderMargin: margins(8px, 7px, 8px, 6px); storiesSliderSkip: 4px; -storiesHeaderMargin: margins(12px, 3px, 12px, 8px); +storiesSliderOutsideSkip: 4px; +storiesHeaderMargin: margins(12px, 4px, 12px, 8px); storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { size: size(28px, 28px); photoSize: 28px; @@ -422,7 +426,7 @@ storiesHeaderNamePosition: point(50px, 0px); storiesHeaderDate: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; } -storiesHeaderDatePosition: point(50px, 16px); +storiesHeaderDatePosition: point(50px, 17px); storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesAttach: IconButton(defaultIconButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 1af0ea39be..2e037a46c9 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -112,6 +112,11 @@ OverlayWidget::RendererGL::RendererGL(not_null owner) _captionImage.invalidate(); invalidateControls(); }, _lifetime); + + _owner->_storiesChanged.events( + ) | rpl::start_with_next([=] { + invalidateControls(); + }, _lifetime); } void OverlayWidget::RendererGL::init( @@ -568,7 +573,8 @@ void OverlayWidget::RendererGL::paintControl( QRect inner, float64 innerOpacity, const style::icon &icon) { - const auto meta = ControlMeta(control); + const auto stories = (_owner->_stories != nullptr); + const auto meta = ControlMeta(control, stories); Assert(meta.icon == &icon); const auto overAlpha = overOpacity * kOverBackgroundOpacity; @@ -626,11 +632,17 @@ void OverlayWidget::RendererGL::paintControl( FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); } -auto OverlayWidget::RendererGL::ControlMeta(OverState control) +auto OverlayWidget::RendererGL::ControlMeta(OverState control, bool stories) -> Control { switch (control) { - case OverLeftNav: return { 0, &st::mediaviewLeft }; - case OverRightNav: return { 1, &st::mediaviewRight }; + case OverLeftNav: return { + 0, + stories ? &st::storiesLeft : &st::mediaviewLeft + }; + case OverRightNav: return { + 1, + stories ? &st::storiesRight : &st::mediaviewRight + }; case OverSave: return { 2, &st::mediaviewSave }; case OverRotate: return { 3, &st::mediaviewRotate }; case OverMore: return { 4, &st::mediaviewMore }; @@ -642,12 +654,13 @@ void OverlayWidget::RendererGL::validateControls() { if (!_controlsImage.image().isNull()) { return; } + const auto stories = (_owner->_stories != nullptr); const auto metas = { - ControlMeta(OverLeftNav), - ControlMeta(OverRightNav), - ControlMeta(OverSave), - ControlMeta(OverRotate), - ControlMeta(OverMore), + ControlMeta(OverLeftNav, stories), + ControlMeta(OverRightNav, stories), + ControlMeta(OverSave, stories), + ControlMeta(OverRotate, stories), + ControlMeta(OverMore, stories), }; auto maxWidth = 0; auto fullHeight = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index effab47ad1..88d66e2d46 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -134,7 +134,9 @@ private: Ui::GL::Image _controlsImage; static constexpr auto kControlsCount = 5; - [[nodiscard]] static Control ControlMeta(OverState control); + [[nodiscard]] static Control ControlMeta( + OverState control, + bool stories); // Last one is for the over circle image. std::array _controlsTextures; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 13615c184a..1514a75279 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -813,16 +813,7 @@ void OverlayWidget::updateGeometryToScreen(bool inMove) { } void OverlayWidget::updateControlsGeometry() { - const auto overRect = QRect( - QPoint(), - QSize(st::mediaviewIconOver, st::mediaviewIconOver)); - const auto navSkip = st::mediaviewHeaderTop; - _leftNav = QRect(0, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); - _leftNavOver = style::centerrect(_leftNav, overRect); - _leftNavIcon = style::centerrect(_leftNav, st::mediaviewLeft); - _rightNav = QRect(width() - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); - _rightNavOver = style::centerrect(_rightNav, overRect); - _rightNavIcon = style::centerrect(_rightNav, st::mediaviewRight); + updateNavigationControlsGeometry(); _saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2); _photoRadialRect = QRect(QPoint((width() - st::radialSize.width()) / 2, (height() - st::radialSize.height()) / 2), st::radialSize); @@ -843,6 +834,32 @@ void OverlayWidget::updateControlsGeometry() { update(); } +void OverlayWidget::updateNavigationControlsGeometry() { + const auto overRect = QRect( + QPoint(), + QSize(st::mediaviewIconOver, st::mediaviewIconOver)); + const auto navSize = _stories + ? st::storiesControlSize + : st::mediaviewControlSize; + const auto navSkip = st::mediaviewHeaderTop; + const auto xLeft = _stories ? (_x - navSize) : 0; + const auto xRight = _stories ? (_x + _w) : (width() - navSize); + _leftNav = QRect(xLeft, navSkip, navSize, height() - 2 * navSkip); + _leftNavOver = _stories + ? QRect() + : style::centerrect(_leftNav, overRect); + _leftNavIcon = style::centerrect( + _leftNav, + _stories ? st::storiesLeft : st::mediaviewLeft); + _rightNav = QRect(xRight, navSkip, navSize, height() - 2 * navSkip); + _rightNavOver = _stories + ? QRect() + : style::centerrect(_rightNav, overRect); + _rightNavIcon = style::centerrect( + _rightNav, + _stories ? st::storiesRight : st::mediaviewRight); +} + bool OverlayWidget::topShadowOnTheRight() const { return _topShadowRight.current(); } @@ -1009,8 +1026,8 @@ void OverlayWidget::updateDocSize() { void OverlayWidget::refreshNavVisibility() { if (_stories) { - _leftNavVisible = _stories->jumpAvailable(-1); - _rightNavVisible = _stories->jumpAvailable(1); + _leftNavVisible = _stories->subjumpAvailable(-1); + _rightNavVisible = _stories->subjumpAvailable(1); } else if (_sharedMediaData) { _leftNavVisible = _index && (*_index > 0); _rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size()); @@ -1432,6 +1449,12 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { + (_over == OverSave ? _saveNavOver : _saveNavIcon) + (_over == OverRotate ? _rotateNavOver : _rotateNavIcon) + (_over == OverMore ? _moreNavOver : _moreNavIcon) + + ((_stories && _over == OverLeftStories) + ? _stories->siblingLeft().geometry + : QRect()) + + ((_stories && _over == OverRightStories) + ? _stories->siblingRight().geometry + : QRect()) + _headerNav + _nameNav + _dateNav @@ -1547,6 +1570,7 @@ void OverlayWidget::resizeContentByScreenSize() { _y = content.y(); _w = content.width(); _h = content.height(); + updateNavigationControlsGeometry(); return; } recountSkipTop(); @@ -3861,14 +3885,16 @@ std::shared_ptr OverlayWidget::storiesShow() { return _widget->_body; } bool valid() const override { - return _widget->_storiesUser != nullptr; + return _widget->_storiesSession != nullptr; } operator bool() const override { return valid(); } Main::Session &session() const override { - return _widget->_storiesUser->session(); + Expects(_widget->_storiesSession != nullptr); + + return *_widget->_storiesSession; } bool paused(ChatHelpers::PauseReason reason) const override { if (_widget->isHidden() @@ -3976,6 +4002,10 @@ void OverlayWidget::storiesTogglePaused(bool paused) { } } +void OverlayWidget::storiesRepaint() { + update(); +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); @@ -4131,6 +4161,22 @@ void OverlayWidget::paint(not_null renderer) { fillTransparentBackground); } paintRadialLoading(renderer); + if (_stories) { + if (const auto left = _stories->siblingLeft()) { + renderer->paintTransformedStaticContent( + left.image, + { .rect = left.geometry }, + false, // semi-transparent + false); // fill transparent background + } + if (const auto right = _stories->siblingRight()) { + renderer->paintTransformedStaticContent( + right.image, + { .rect = right.geometry }, + false, // semi-transparent + false); // fill transparent background + } + } } else { if (_themePreviewShown) { renderer->paintThemePreview(_themePreviewRect); @@ -4420,14 +4466,14 @@ void OverlayWidget::paintControls( _leftNavVisible, _leftNavOver, _leftNavIcon, - st::mediaviewLeft, + _stories ? st::storiesLeft : st::mediaviewLeft, true }, { OverRightNav, _rightNavVisible, _rightNavOver, _rightNavIcon, - st::mediaviewRight, + _stories ? st::storiesRight : st::mediaviewRight, true }, { OverSave, @@ -4470,6 +4516,10 @@ void OverlayWidget::paintControls( float64 OverlayWidget::controlOpacity( float64 progress, bool nonbright) const { + if (nonbright && _stories) { + return progress * kStoriesNavOverOpacity + + (1. - progress) * kStoriesNavOpacity; + } const auto normal = _windowed ? kNormalIconOpacity : kMaximizedIconOpacity; @@ -4813,15 +4863,13 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); - _stories = nullptr; - _storiesUser = nullptr; + setStoriesUser(nullptr); } else if (const auto peer = std::get_if>(&context)) { _peer = *peer; _history = _peer->owner().history(_peer); _message = nullptr; _topicRootId = MsgId(); - _stories = nullptr; - _storiesUser = nullptr; + setStoriesUser(nullptr); } else if (const auto story = std::get_if(&context)) { _message = nullptr; _topicRootId = MsgId(); @@ -4837,18 +4885,14 @@ void OverlayWidget::setContext( i->items, story->id, &Data::StoryItem::id); - _storiesUser = story->user; - if (!_stories) { - _stories = std::make_unique( - static_cast(this)); - } - _stories->show(*i, j - begin(i->items)); + setStoriesUser(story->user); + _stories->show(all, (i - begin(all)), j - begin(i->items)); } else { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - _stories = nullptr; + setStoriesUser(nullptr); } _migrated = nullptr; if (_history) { @@ -4863,6 +4907,27 @@ void OverlayWidget::setContext( _user = _peer ? _peer->asUser() : nullptr; } +void OverlayWidget::setStoriesUser(UserData *user) { + const auto session = user ? &user->session() : nullptr; + if (!session && !_storiesSession) { + Assert(!_stories); + } else if (!user) { + _stories = nullptr; + _storiesSession = nullptr; + _storiesChanged.fire({}); + } else if (_storiesSession != session) { + _stories = nullptr; + _storiesSession = session; + const auto delegate = static_cast(this); + _stories = std::make_unique(delegate); + _stories->contentGeometryValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _stories->lifetime()); + _storiesChanged.fire({}); + } +} + void OverlayWidget::setSession(not_null session) { if (_session == session) { return; @@ -4908,7 +4973,7 @@ void OverlayWidget::setSession(not_null session) { bool OverlayWidget::moveToNext(int delta) { if (_stories) { - return _stories->jumpFor(delta); + return _stories->subjumpFor(delta); } else if (!_index) { return false; } @@ -4991,9 +5056,14 @@ void OverlayWidget::handleMousePress( if (button == Qt::LeftButton) { _down = OverNone; if (!ClickHandler::getPressed()) { - if (_over == OverLeftNav && moveToNext(-1)) { - _lastAction = position; - } else if (_over == OverRightNav && moveToNext(1)) { + if ((_over == OverLeftNav && moveToNext(-1)) + || (_over == OverRightNav && moveToNext(1)) + || (_stories + && _over == OverLeftStories + && _stories->jumpFor(-1)) + || (_stories + && _over == OverRightStories + && _stories->jumpFor(1))) { _lastAction = position; } else if (_over == OverName || _over == OverDate @@ -5082,8 +5152,18 @@ void OverlayWidget::handleMouseMove(QPoint position) { void OverlayWidget::updateOverRect(OverState state) { switch (state) { - case OverLeftNav: update(_leftNavOver); break; - case OverRightNav: update(_rightNavOver); break; + case OverLeftNav: + update(_stories ? _leftNavIcon : _leftNavOver); + break; + case OverRightNav: + update(_stories ? _rightNavIcon : _rightNavOver); + break; + case OverLeftStories: + update(_stories ? _stories->siblingLeft().geometry : QRect()); + break; + case OverRightStories: + update(_stories ? _stories->siblingRight().geometry : QRect()); + break; case OverName: update(_nameNav); break; case OverDate: update(_dateNav); break; case OverSave: update(_saveNavOver); break; @@ -5170,6 +5250,10 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverVideo); } else if (_leftNavVisible && _leftNav.contains(pos)) { updateOverState(OverLeftNav); + } else if (_stories && _stories->siblingLeft().geometry.contains(pos)) { + updateOverState(OverLeftStories); + } else if (_stories && _stories->siblingRight().geometry.contains(pos)) { + updateOverState(OverRightStories); } else if (_rightNavVisible && _rightNav.contains(pos)) { updateOverState(OverRightNav); } else if (!_stories && _from && _nameNav.contains(pos)) { @@ -5527,6 +5611,7 @@ void OverlayWidget::clearBeforeHide() { _collage = nullptr; _collageData = std::nullopt; clearStreaming(); + setStoriesUser(nullptr); assignMediaPointer(nullptr); _preloadPhotos.clear(); _preloadDocuments.clear(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index ad7506ee1b..e0bed4d149 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -137,6 +137,8 @@ private: OverNone, OverLeftNav, OverRightNav, + OverLeftStories, + OverRightStories, OverHeader, OverName, OverDate, @@ -231,6 +233,7 @@ private: void storiesJumpTo(Data::FullStoryId id) override; bool storiesPaused() override; void storiesTogglePaused(bool paused) override; + void storiesRepaint() override; void hideControls(bool force = false); void subscribeToScreenGeometry(); @@ -292,6 +295,7 @@ private: ItemContext, not_null, StoriesContext> context); + void setStoriesUser(UserData *user); void refreshLang(); void showSaveMsgFile(); @@ -332,6 +336,7 @@ private: void updateDocSize(); void updateControls(); void updateControlsGeometry(); + void updateNavigationControlsGeometry(); using MenuCallback = Fn _stories; - UserData *_storiesUser = nullptr; + rpl::event_stream<> _storiesChanged; + Main::Session *_storiesSession = nullptr; rpl::event_stream _storiesStickerOrEmojiChosen; std::unique_ptr _layerBg; diff --git a/Telegram/SourceFiles/platform/platform_overlay_widget.h b/Telegram/SourceFiles/platform/platform_overlay_widget.h index 2bc2eea33b..97b6ef7ef2 100644 --- a/Telegram/SourceFiles/platform/platform_overlay_widget.h +++ b/Telegram/SourceFiles/platform/platform_overlay_widget.h @@ -20,6 +20,8 @@ namespace Media::View { inline constexpr auto kMaximizedIconOpacity = 0.6; inline constexpr auto kNormalIconOpacity = 0.9; inline constexpr auto kOverBackgroundOpacity = 0.2775; +inline constexpr auto kStoriesNavOpacity = 0.3; +inline constexpr auto kStoriesNavOverOpacity = 0.7; [[nodiscard]] QColor OverBackgroundColor(); } // namespace Media::View