Allow navigating to stories of sibling users.

This commit is contained in:
John Preston 2023-05-09 14:31:48 +04:00
parent 7717de19ab
commit ae94cd2d42
20 changed files with 699 additions and 93 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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<StoriesList>();
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<not_null<PhotoData*>>(media) }),
: StoryMedia{
v::get<not_null<PhotoData*>>(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;

View File

@ -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;
};

View File

@ -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<Data::StoriesList> &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<not_null<PhotoData*>>(item.media.data)) {
_photoPlayback = std::make_unique<PhotoPlayback>(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<Data::StoriesList> &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> &sibling,
const Data::StoriesList *list) {
if (!list || list->items.empty()) {
sibling = nullptr;
} else if (!sibling || !sibling->shows(*list)) {
sibling = std::make_unique<Sibling>(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*> 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);

View File

@ -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<ChatHelpers::FileChosen>;
void show(const Data::StoriesList &list, int index);
void show(
const std::vector<Data::StoriesList> &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*> 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<Data::StoriesList> &lists,
int index);
void showSibling(
std::unique_ptr<Sibling> &sibling,
const Data::StoriesList *list);
const not_null<Delegate*> _delegate;
rpl::variable<std::optional<Layout>> _layout;
@ -94,11 +115,15 @@ private:
const std::unique_ptr<Header> _header;
const std::unique_ptr<Slider> _slider;
const std::unique_ptr<ReplyArea> _replyArea;
std::unique_ptr<PhotoPlayback> _photoPlayback;
Data::FullStoryId _shown;
std::optional<Data::StoriesList> _list;
int _index = 0;
std::unique_ptr<PhotoPlayback> _photoPlayback;
bool _started = false;
std::unique_ptr<Sibling> _siblingLeft;
std::unique_ptr<Sibling> _siblingRight;
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;

View File

@ -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

View File

@ -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<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> update);
QImage blurred() override;
QImage good() override;
private:
const not_null<PhotoData*> _photo;
const Fn<void()> _update;
std::shared_ptr<Data::PhotoMedia> _media;
rpl::lifetime _waitingLoading;
};
class Sibling::LoaderVideo final
: public Sibling::Loader
, public base::has_weak_ptr {
public:
LoaderVideo(
not_null<DocumentData*> video,
Data::FileOrigin origin,
Fn<void()> update);
QImage blurred() override;
QImage good() override;
private:
void waitForGoodThumbnail();
bool updateAfterGoodCheck();
void streamedFailed();
const not_null<DocumentData*> _video;
const Data::FileOrigin _origin;
const Fn<void()> _update;
std::shared_ptr<Data::DocumentMedia> _media;
std::unique_ptr<Streaming::Instance> _streamed;
rpl::lifetime _waitingGoodGeneration;
bool _checkingGoodInCache = false;
bool _failed = false;
};
Sibling::LoaderPhoto::LoaderPhoto(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> 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<DocumentData*> video,
Data::FileOrigin origin,
Fn<void()> 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<Streaming::Instance>(
_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*> 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<not_null<DocumentData*>>(&data)) {
_loader = std::make_unique<LoaderVideo>((*video), origin, [=] {
check();
});
} else if (const auto photo = std::get_if<not_null<PhotoData*>>(&data)) {
_loader = std::make_unique<LoaderPhoto>((*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

View File

@ -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*> 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*> _controller;
Data::FullStoryId _id;
QImage _blurred;
QImage _good;
Ui::Animations::Simple _goodShown;
std::unique_ptr<Loader> _loader;
};
} // namespace Media::Stories

View File

@ -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);
}
}

View File

@ -21,8 +21,11 @@ View::View(not_null<Delegate*> delegate)
View::~View() = default;
void View::show(const Data::StoriesList &list, int index) {
_controller->show(list, index);
void View::show(
const std::vector<Data::StoriesList> &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<QRect> 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

View File

@ -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*> delegate);
~View();
void show(const Data::StoriesList &list, int index);
void show(
const std::vector<Data::StoriesList> &lists,
int index,
int subindex);
void ready();
[[nodiscard]] QRect contentGeometry() const;
[[nodiscard]] rpl::producer<QRect> 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> _controller;

View File

@ -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) {

View File

@ -112,6 +112,11 @@ OverlayWidget::RendererGL::RendererGL(not_null<OverlayWidget*> 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;

View File

@ -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<QRect, kControlsCount + 1> _controlsTextures;

View File

@ -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<ChatHelpers::Show> 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*> 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<not_null<PeerData*>>(&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<StoriesContext>(&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<Stories::View>(
static_cast<Stories::Delegate*>(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<Stories::Delegate*>(this);
_stories = std::make_unique<Stories::View>(delegate);
_stories->contentGeometryValue(
) | rpl::skip(1) | rpl::start_with_next([=] {
updateControlsGeometry();
}, _stories->lifetime());
_storiesChanged.fire({});
}
}
void OverlayWidget::setSession(not_null<Main::Session*> session) {
if (_session == session) {
return;
@ -4908,7 +4973,7 @@ void OverlayWidget::setSession(not_null<Main::Session*> 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();

View File

@ -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<PeerData*>,
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<void(
const QString &,
@ -572,7 +577,8 @@ private:
bool _showAsPip = false;
std::unique_ptr<Stories::View> _stories;
UserData *_storiesUser = nullptr;
rpl::event_stream<> _storiesChanged;
Main::Session *_storiesSession = nullptr;
rpl::event_stream<ChatHelpers::FileChosen> _storiesStickerOrEmojiChosen;
std::unique_ptr<Ui::LayerManager> _layerBg;

View File

@ -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