init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
80
Telegram/SourceFiles/media/stories/media_stories.style
Normal file
80
Telegram/SourceFiles/media/stories/media_stories.style
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
using "ui/basic.style";
|
||||
using "ui/widgets/widgets.style";
|
||||
using "boxes/boxes.style";
|
||||
|
||||
StealthBoxStyle {
|
||||
box: Box;
|
||||
buttonLabel: FlatLabel;
|
||||
lockIcon: icon;
|
||||
boxClose: IconButton;
|
||||
about: FlatLabel;
|
||||
featureTitle: FlatLabel;
|
||||
featureAbout: FlatLabel;
|
||||
featurePastIcon: icon;
|
||||
featureNextIcon: icon;
|
||||
logoIcon: icon;
|
||||
logoBg: color;
|
||||
}
|
||||
|
||||
storiesStealthStyleDefault: StealthBoxStyle {
|
||||
box: Box(defaultBox) {
|
||||
buttonPadding: margins(10px, 10px, 10px, 10px);
|
||||
buttonHeight: 42px;
|
||||
button: RoundButton(defaultActiveButton) {
|
||||
height: 42px;
|
||||
textTop: 12px;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
margin: margins(0px, 56px, 0px, 10px);
|
||||
bg: windowBg;
|
||||
title: FlatLabel(boxTitle) {
|
||||
align: align(top);
|
||||
}
|
||||
titleAdditionalFg: windowSubTextFg;
|
||||
}
|
||||
buttonLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: activeButtonFg;
|
||||
style: semiboldTextStyle;
|
||||
align: align(top);
|
||||
minWidth: 20px;
|
||||
maxHeight: 20px;
|
||||
}
|
||||
lockIcon: icon {{ "dialogs/dialogs_lock_on", windowFgActive }};
|
||||
boxClose: IconButton(defaultIconButton) {
|
||||
width: boxTitleHeight;
|
||||
height: boxTitleHeight;
|
||||
icon: icon {{ "box_button_close", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "box_button_close", boxTitleCloseFgOver }};
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
}
|
||||
about: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
align: align(top);
|
||||
minWidth: 20px;
|
||||
}
|
||||
featureTitle: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowBoldFg;
|
||||
style: semiboldTextStyle;
|
||||
minWidth: 10px;
|
||||
maxHeight: 20px;
|
||||
}
|
||||
featureAbout: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
minWidth: 20px;
|
||||
}
|
||||
featurePastIcon: icon {{ "stories/stealth_5m", windowActiveTextFg }};
|
||||
featureNextIcon: icon {{ "stories/stealth_25m", windowActiveTextFg }};
|
||||
logoIcon: icon {{ "stories/stealth_logo", windowFgActive }};
|
||||
logoBg: windowBgActive;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
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_caption_full_view.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "media/stories/media_stories_view.h"
|
||||
#include "ui/widgets/elastic_scroll.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
CaptionFullView::CaptionFullView(not_null<Controller*> controller)
|
||||
: _controller(controller)
|
||||
, _scroll(std::make_unique<Ui::ElasticScroll>(controller->wrap()))
|
||||
, _wrap(_scroll->setOwnedWidget(
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
_scroll.get(),
|
||||
object_ptr<Ui::FlatLabel>(_scroll.get(), st::storiesCaptionFull),
|
||||
st::mediaviewCaptionPadding + _controller->repostCaptionPadding())))
|
||||
, _text(_wrap->entity()) {
|
||||
_text->setMarkedText(controller->captionText(), Core::TextContext({
|
||||
.session = &controller->uiShow()->session(),
|
||||
}));
|
||||
|
||||
startAnimation();
|
||||
_controller->layoutValue(
|
||||
) | rpl::on_next([=](const Layout &layout) {
|
||||
if (_outer != layout.content) {
|
||||
const auto skip = layout.header.y()
|
||||
+ layout.header.height()
|
||||
- layout.content.y();
|
||||
_outer = layout.content.marginsRemoved({ 0, skip, 0, 0 });
|
||||
updateGeometry();
|
||||
}
|
||||
}, _scroll->lifetime());
|
||||
|
||||
const auto filter = [=](not_null<QEvent*> e) {
|
||||
const auto mouse = [&] {
|
||||
return static_cast<QMouseEvent*>(e.get());
|
||||
};
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::MouseButtonPress
|
||||
&& mouse()->button() == Qt::LeftButton
|
||||
&& !ClickHandler::getActive()) {
|
||||
_down = true;
|
||||
} else if (type == QEvent::MouseButtonRelease && _down) {
|
||||
_down = false;
|
||||
if (!ClickHandler::getPressed()) {
|
||||
close();
|
||||
}
|
||||
} else if (type == QEvent::KeyPress
|
||||
&& static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {
|
||||
close();
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
};
|
||||
base::install_event_filter(_text.get(), filter);
|
||||
if (_controller->repost()) {
|
||||
_wrap->setMouseTracking(true);
|
||||
base::install_event_filter(_wrap.get(), [=](not_null<QEvent*> e) {
|
||||
const auto mouse = [&] {
|
||||
return static_cast<QMouseEvent*>(e.get());
|
||||
};
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::MouseMove) {
|
||||
const auto handler = _controller->lookupRepostHandler(
|
||||
mouse()->pos() - QPoint(
|
||||
st::mediaviewCaptionPadding.left(),
|
||||
(_wrap->padding().top()
|
||||
- _controller->repostCaptionPadding().top())));
|
||||
ClickHandler::setActive(handler.link, handler.host);
|
||||
_wrap->setCursor(handler.link
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
} else if (type == QEvent::MouseButtonPress
|
||||
&& mouse()->button() == Qt::LeftButton
|
||||
&& ClickHandler::getActive()) {
|
||||
ClickHandler::pressed();
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
if (const auto activated = ClickHandler::unpressed()) {
|
||||
ActivateClickHandler(_wrap.get(), activated, {
|
||||
mouse()->button(), QVariant(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}
|
||||
base::install_event_filter(_wrap.get(), filter);
|
||||
|
||||
using Type = Ui::ElasticScroll::OverscrollType;
|
||||
|
||||
rpl::combine(
|
||||
_scroll->positionValue(),
|
||||
_scroll->movementValue()
|
||||
) | rpl::filter([=] {
|
||||
return !_closing;
|
||||
}) | rpl::on_next([=](
|
||||
Ui::ElasticScrollPosition position,
|
||||
Ui::ElasticScrollMovement movement) {
|
||||
const auto overscrollTop = std::max(-position.overscroll, 0);
|
||||
using Phase = Ui::ElasticScrollMovement;
|
||||
if (movement == Phase::Progress) {
|
||||
if (overscrollTop > 0) {
|
||||
_pulling = true;
|
||||
} else {
|
||||
_pulling = false;
|
||||
}
|
||||
} else if (_pulling
|
||||
&& (movement == Phase::Momentum
|
||||
|| movement == Phase::Returning)) {
|
||||
_pulling = false;
|
||||
if (overscrollTop > st::storiesCaptionPullThreshold) {
|
||||
_closingTopAdded = overscrollTop;
|
||||
_scroll->setOverscrollTypes(Type::None, Type::Real);
|
||||
close();
|
||||
updateGeometry();
|
||||
}
|
||||
}
|
||||
}, _scroll->lifetime());
|
||||
|
||||
_wrap->paintRequest() | rpl::on_next([=] {
|
||||
if (_controller->repost()) {
|
||||
auto p = Painter(_wrap.get());
|
||||
_controller->drawRepostInfo(
|
||||
p,
|
||||
st::mediaviewCaptionPadding.left(),
|
||||
(_wrap->padding().top()
|
||||
- _controller->repostCaptionPadding().top()),
|
||||
_wrap->width());
|
||||
}
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_scroll->show();
|
||||
_scroll->setOverscrollBg(QColor(0, 0, 0, 0));
|
||||
_scroll->setOverscrollTypes(Type::Real, Type::Real);
|
||||
_text->show();
|
||||
_text->setFocus();
|
||||
}
|
||||
|
||||
CaptionFullView::~CaptionFullView() = default;
|
||||
|
||||
bool CaptionFullView::closing() const {
|
||||
return _closing;
|
||||
}
|
||||
|
||||
bool CaptionFullView::focused() const {
|
||||
return Ui::InFocusChain(_scroll.get());
|
||||
}
|
||||
|
||||
void CaptionFullView::close() {
|
||||
if (_closing) {
|
||||
return;
|
||||
}
|
||||
_closing = true;
|
||||
_controller->captionClosing();
|
||||
startAnimation();
|
||||
}
|
||||
|
||||
void CaptionFullView::repaint() {
|
||||
_wrap->update();
|
||||
}
|
||||
|
||||
void CaptionFullView::updateGeometry() {
|
||||
if (_outer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto lineHeight = st::mediaviewCaptionStyle.font->height;
|
||||
const auto padding = st::mediaviewCaptionPadding
|
||||
+ _controller->repostCaptionPadding();
|
||||
_text->resizeToWidth(_outer.width() - padding.left() - padding.right());
|
||||
const auto add = padding.top() + padding.bottom();
|
||||
const auto maxShownHeight = lineHeight * kMaxShownCaptionLines;
|
||||
const auto shownHeight = (_text->height() > maxShownHeight)
|
||||
? (lineHeight * kCollapsedCaptionLines)
|
||||
: _text->height();
|
||||
const auto collapsedHeight = shownHeight + add;
|
||||
const auto addedToBottom = lineHeight;
|
||||
const auto expandedHeight = _text->height() + add + addedToBottom;
|
||||
const auto fullHeight = std::min(expandedHeight, _outer.height());
|
||||
const auto shown = _animation.value(_closing ? 0. : 1.);
|
||||
const auto height = (_closing || _animation.animating())
|
||||
? anim::interpolate(collapsedHeight, fullHeight, shown)
|
||||
: _outer.height();
|
||||
const auto added = anim::interpolate(0, _closingTopAdded, shown);
|
||||
const auto bottomPadding = anim::interpolate(0, addedToBottom, shown);
|
||||
const auto use = padding + ((_closing || _animation.animating())
|
||||
? QMargins(0, 0, 0, bottomPadding)
|
||||
: QMargins(0, height - fullHeight, 0, bottomPadding));
|
||||
_wrap->setPadding(use);
|
||||
_scroll->setGeometry(
|
||||
_outer.x(),
|
||||
added + _outer.y() + _outer.height() - height,
|
||||
_outer.width(),
|
||||
std::max(height - added, 0));
|
||||
if (_closing && !_animation.animating()) {
|
||||
_controller->captionClosed();
|
||||
}
|
||||
}
|
||||
|
||||
void CaptionFullView::startAnimation() {
|
||||
_animation.start(
|
||||
[=] { updateGeometry(); },
|
||||
_closing ? 1. : 0.,
|
||||
_closing ? 0. : 1.,
|
||||
st::fadeWrapDuration,
|
||||
anim::sineInOut);
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class ElasticScroll;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
|
||||
class CaptionFullView final {
|
||||
public:
|
||||
explicit CaptionFullView(not_null<Controller*> controller);
|
||||
~CaptionFullView();
|
||||
|
||||
void close();
|
||||
void repaint();
|
||||
[[nodiscard]] bool closing() const;
|
||||
[[nodiscard]] bool focused() const;
|
||||
|
||||
private:
|
||||
void updateGeometry();
|
||||
void startAnimation();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
const std::unique_ptr<Ui::ElasticScroll> _scroll;
|
||||
const not_null<Ui::PaddingWrap<Ui::FlatLabel>*> _wrap;
|
||||
const not_null<Ui::FlatLabel*> _text;
|
||||
Ui::Animations::Simple _animation;
|
||||
QRect _outer;
|
||||
int _closingTopAdded = 0;
|
||||
bool _pulling = false;
|
||||
bool _closing = false;
|
||||
bool _down = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
2133
Telegram/SourceFiles/media/stories/media_stories_controller.cpp
Normal file
2133
Telegram/SourceFiles/media/stories/media_stories_controller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
406
Telegram/SourceFiles/media/stories/media_stories_controller.h
Normal file
406
Telegram/SourceFiles/media/stories/media_stories_controller.h
Normal file
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace style {
|
||||
struct ReportBox;
|
||||
} // namespace style
|
||||
|
||||
namespace base {
|
||||
class PowerSaveBlocker;
|
||||
} // namespace base
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
struct FileChosen;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct FileOrigin;
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
enum class ToggleCommentsState;
|
||||
struct SendStarButtonEffect;
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
namespace HistoryView::Reactions {
|
||||
struct ChosenReaction;
|
||||
enum class AttachSelectorResult;
|
||||
} // namespace HistoryView::Reactions
|
||||
|
||||
namespace HistoryView {
|
||||
class PaidReactionToast;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class BoxContent;
|
||||
class PopupMenu;
|
||||
struct SendStarButtonState;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Media::Player {
|
||||
struct TrackState;
|
||||
} // namespace Media::Player
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Header;
|
||||
class Slider;
|
||||
class ReplyArea;
|
||||
class Reactions;
|
||||
class RecentViews;
|
||||
class Sibling;
|
||||
class Delegate;
|
||||
struct SiblingView;
|
||||
enum class SiblingType;
|
||||
struct ContentLayout;
|
||||
class CaptionFullView;
|
||||
class RepostView;
|
||||
enum class ReactionsMode;
|
||||
class StoryAreaView;
|
||||
struct RepostClickHandler;
|
||||
|
||||
using CommentsState = HistoryView::Controls::ToggleCommentsState;
|
||||
using PaidReactionToast = HistoryView::PaidReactionToast;
|
||||
using SendStarButtonEffect = HistoryView::Controls::SendStarButtonEffect;
|
||||
|
||||
enum class HeaderLayout {
|
||||
Normal,
|
||||
Outside,
|
||||
};
|
||||
|
||||
enum class PauseState {
|
||||
Playing,
|
||||
Paused,
|
||||
Inactive,
|
||||
InactivePaused,
|
||||
};
|
||||
|
||||
struct SiblingLayout {
|
||||
QRect geometry;
|
||||
QRect userpic;
|
||||
QRect nameBoundingRect;
|
||||
int nameFontSize = 0;
|
||||
|
||||
friend inline bool operator==(SiblingLayout, SiblingLayout) = default;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
QRect content;
|
||||
QRect header;
|
||||
QRect slider;
|
||||
QRect reactions;
|
||||
int controlsWidth = 0;
|
||||
QPoint controlsBottomPosition;
|
||||
QRect views;
|
||||
QRect autocompleteRect;
|
||||
HeaderLayout headerLayout = HeaderLayout::Normal;
|
||||
SiblingLayout siblingLeft;
|
||||
SiblingLayout siblingRight;
|
||||
|
||||
friend inline bool operator==(Layout, Layout) = default;
|
||||
};
|
||||
|
||||
class Controller final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Controller(not_null<Delegate*> delegate);
|
||||
~Controller();
|
||||
|
||||
[[nodiscard]] Data::Story *story() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;
|
||||
[[nodiscard]] Layout layout() const;
|
||||
[[nodiscard]] rpl::producer<Layout> layoutValue() const;
|
||||
[[nodiscard]] ContentLayout contentLayout() const;
|
||||
[[nodiscard]] bool closeByClickAt(QPoint position) const;
|
||||
[[nodiscard]] Data::FileOrigin fileOrigin() const;
|
||||
[[nodiscard]] TextWithEntities captionText() const;
|
||||
[[nodiscard]] bool videoStream() const;
|
||||
[[nodiscard]] bool skipCaption() const;
|
||||
[[nodiscard]] bool repost() const;
|
||||
void toggleLiked();
|
||||
void showFullCaption();
|
||||
void captionClosing();
|
||||
void captionClosed();
|
||||
|
||||
[[nodiscard]] QMargins repostCaptionPadding() const;
|
||||
void drawRepostInfo(Painter &p, int x, int y, int availableWidth) const;
|
||||
[[nodiscard]] RepostClickHandler lookupRepostHandler(
|
||||
QPoint position) const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
|
||||
[[nodiscard]] auto stickerOrEmojiChosen() const
|
||||
-> rpl::producer<ChatHelpers::FileChosen>;
|
||||
|
||||
void show(not_null<Data::Story*> story, Data::StoriesContext context);
|
||||
void jumpTo(not_null<Data::Story*> story, Data::StoriesContext context);
|
||||
void ready();
|
||||
|
||||
void updateVideoPlayback(const Player::TrackState &state);
|
||||
[[nodiscard]] ClickHandlerPtr lookupAreaHandler(QPoint point) 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 contentPressed(bool pressed);
|
||||
void setMenuShown(bool shown);
|
||||
|
||||
[[nodiscard]] PauseState pauseState() const;
|
||||
[[nodiscard]] float64 currentVolume() const;
|
||||
void toggleVolume();
|
||||
void changeVolume(float64 volume);
|
||||
void volumeChangeFinished();
|
||||
|
||||
void repaint();
|
||||
void repaintSibling(not_null<Sibling*> sibling);
|
||||
[[nodiscard]] SiblingView sibling(SiblingType type) const;
|
||||
|
||||
[[nodiscard]] const Data::StoryViews &views(int limit, bool initial);
|
||||
[[nodiscard]] rpl::producer<> moreViewsLoaded() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<CommentsState> commentsStateValue() const;
|
||||
void setCommentsShownToggles(rpl::producer<> toggles);
|
||||
[[nodiscard]] auto starsReactionsValue() const
|
||||
-> rpl::producer<Ui::SendStarButtonState>;
|
||||
[[nodiscard]] auto starsReactionsEffects() const
|
||||
-> rpl::producer<SendStarButtonEffect>;
|
||||
void setStarsReactionIncrements(rpl::producer<int> increments);
|
||||
|
||||
void unfocusReply();
|
||||
void shareRequested();
|
||||
void deleteRequested();
|
||||
void reportRequested();
|
||||
void toggleInProfileRequested(bool inProfile);
|
||||
|
||||
[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
|
||||
void tryProcessKeyInput(not_null<QKeyEvent*> e);
|
||||
|
||||
[[nodiscard]] bool allowStealthMode() const;
|
||||
void setupStealthMode();
|
||||
|
||||
using AttachStripResult = HistoryView::Reactions::AttachSelectorResult;
|
||||
[[nodiscard]] AttachStripResult attachReactionsToMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
QPoint desiredPosition);
|
||||
|
||||
void updateVideoStream(not_null<Calls::GroupCall*> videoStream);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
class PhotoPlayback;
|
||||
class Unsupported;
|
||||
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
|
||||
struct StoriesList {
|
||||
not_null<PeerData*> peer;
|
||||
Data::StoriesIds ids;
|
||||
std::vector<StoryId> sorted;
|
||||
int total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StoriesList &,
|
||||
const StoriesList &) = default;
|
||||
};
|
||||
struct CachedSource {
|
||||
PeerId peerId = 0;
|
||||
StoryId shownId = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return peerId != 0;
|
||||
}
|
||||
};
|
||||
struct ActiveArea {
|
||||
QRectF original;
|
||||
float64 radiusOriginal = 0.;
|
||||
QRect geometry;
|
||||
float64 rotation = 0.;
|
||||
float64 radius = 0.;
|
||||
ClickHandlerPtr handler;
|
||||
std::unique_ptr<StoryAreaView> view;
|
||||
};
|
||||
enum class CommentsHas {
|
||||
None,
|
||||
AllRead,
|
||||
WithUnread,
|
||||
};
|
||||
|
||||
void initLayout();
|
||||
bool changeShown(Data::Story *story);
|
||||
void subscribeToSession();
|
||||
void updatePhotoPlayback(const Player::TrackState &state);
|
||||
void updatePlayback(const Player::TrackState &state);
|
||||
void updatePowerSaveBlocker(const Player::TrackState &state);
|
||||
void maybeMarkAsRead(const Player::TrackState &state);
|
||||
void markAsRead();
|
||||
|
||||
void updateContentFaded();
|
||||
void updatePlayingAllowed();
|
||||
void setPlayingAllowed(bool allowed);
|
||||
void rebuildActiveAreas(const Layout &layout) const;
|
||||
void toggleWeatherMode() const;
|
||||
|
||||
void hideSiblings();
|
||||
void showSiblings(not_null<Main::Session*> session);
|
||||
void showSibling(
|
||||
std::unique_ptr<Sibling> &sibling,
|
||||
not_null<Main::Session*> session,
|
||||
CachedSource cached);
|
||||
|
||||
void subjumpTo(int index);
|
||||
void checkWaitingFor();
|
||||
void moveFromShown();
|
||||
|
||||
void refreshViewsFromData();
|
||||
[[nodiscard]] auto viewsGotMoreCallback()
|
||||
-> Fn<void(Data::StoryViews)>;
|
||||
|
||||
[[nodiscard]] bool shown() const;
|
||||
[[nodiscard]] PeerData *shownPeer() const;
|
||||
[[nodiscard]] int shownCount() const;
|
||||
[[nodiscard]] StoryId shownId(int index) const;
|
||||
[[nodiscard]] std::unique_ptr<RepostView> validateRepostView(
|
||||
not_null<Data::Story*> story);
|
||||
void rebuildFromContext(not_null<PeerData*> peer, FullStoryId storyId);
|
||||
void checkMoveByDelta();
|
||||
void loadMoreToList();
|
||||
void preloadNext();
|
||||
void rebuildCachedSourcesList(
|
||||
const std::vector<Data::StoriesSourceInfo> &lists,
|
||||
int index);
|
||||
|
||||
[[nodiscard]] int repostSkipTop() const;
|
||||
void updateAreas(Data::Story *story);
|
||||
bool reactionChosen(ReactionsMode mode, ChosenReaction chosen);
|
||||
[[nodiscard]] rpl::producer<int> paidReactionToastTopValue() const;
|
||||
void clearVideoStreamCall();
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
|
||||
rpl::variable<std::optional<Layout>> _layout;
|
||||
|
||||
const not_null<Ui::RpWidget*> _wrap;
|
||||
const std::unique_ptr<Header> _header;
|
||||
const std::unique_ptr<Slider> _slider;
|
||||
const std::unique_ptr<ReplyArea> _replyArea;
|
||||
const std::unique_ptr<Reactions> _reactions;
|
||||
const std::unique_ptr<RecentViews> _recentViews;
|
||||
std::unique_ptr<Unsupported> _unsupported;
|
||||
std::unique_ptr<PhotoPlayback> _photoPlayback;
|
||||
std::unique_ptr<CaptionFullView> _captionFullView;
|
||||
std::unique_ptr<RepostView> _repostView;
|
||||
|
||||
std::shared_ptr<Data::GroupCall> _videoStream;
|
||||
base::weak_ptr<Calls::GroupCall> _videoStreamCall;
|
||||
std::unique_ptr<PaidReactionToast> _paidReactionToast;
|
||||
rpl::lifetime _videoStreamLifetime;
|
||||
|
||||
Ui::Animations::Simple _contentFadeAnimation;
|
||||
bool _contentFaded = false;
|
||||
|
||||
bool _windowActive = false;
|
||||
bool _replyActive = false;
|
||||
bool _layerShown = false;
|
||||
bool _menuShown = false;
|
||||
bool _tooltipShown = false;
|
||||
bool _paused = false;
|
||||
|
||||
FullStoryId _shown;
|
||||
TextWithEntities _captionText;
|
||||
Data::StoriesContext _context;
|
||||
std::optional<Data::StoriesSource> _source;
|
||||
std::optional<StoriesList> _list;
|
||||
FullStoryId _waitingForId;
|
||||
int _waitingForDelta = 0;
|
||||
int _index = 0;
|
||||
int _sliderIndex = 0;
|
||||
int _sliderCount = 0;
|
||||
bool _started = false;
|
||||
bool _viewed = false;
|
||||
|
||||
std::vector<Data::StoryLocation> _locations;
|
||||
std::vector<Data::SuggestedReaction> _suggestedReactions;
|
||||
std::vector<Data::ChannelPost> _channelPosts;
|
||||
std::vector<Data::UrlArea> _urlAreas;
|
||||
std::vector<Data::WeatherArea> _weatherAreas;
|
||||
mutable std::vector<ActiveArea> _areas;
|
||||
mutable rpl::variable<bool> _weatherInCelsius;
|
||||
|
||||
rpl::variable<CommentsState> _commentsState;
|
||||
rpl::event_stream<CommentsState> _commentsStateShowFromPinned;
|
||||
rpl::variable<CommentsHas> _commentsHas;
|
||||
MsgId _commentsLastReadId = 0;
|
||||
MsgId _commentsLastId = 0;
|
||||
rpl::variable<int> _starsReactions;
|
||||
rpl::variable<bool> _starsReactionHighlighted;
|
||||
rpl::event_stream<SendStarButtonEffect> _starsReactionEffects;
|
||||
|
||||
std::vector<CachedSource> _cachedSourcesList;
|
||||
int _cachedSourceIndex = -1;
|
||||
bool _showingUnreadSources = false;
|
||||
|
||||
Data::StoryViews _viewsSlice;
|
||||
rpl::event_stream<> _moreViewsLoaded;
|
||||
base::has_weak_ptr _viewsLoadGuard;
|
||||
|
||||
std::unique_ptr<Sibling> _siblingLeft;
|
||||
std::unique_ptr<Sibling> _siblingRight;
|
||||
|
||||
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
|
||||
|
||||
Main::Session *_session = nullptr;
|
||||
rpl::lifetime _sessionLifetime;
|
||||
|
||||
rpl::lifetime _contextLifetime;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Ui::Toast::Config PrepareToggleInProfileToast(
|
||||
bool channel,
|
||||
int count,
|
||||
bool inProfile);
|
||||
[[nodiscard]] Ui::Toast::Config PrepareTogglePinToast(
|
||||
bool channel,
|
||||
int count,
|
||||
bool pin);
|
||||
void ReportRequested(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
FullStoryId id,
|
||||
const style::ReportBox *stOverride = nullptr);
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
|
||||
not_null<PeerData*> peer);
|
||||
[[nodiscard]] ClickHandlerPtr MakeChannelPostHandler(
|
||||
not_null<Main::Session*> session,
|
||||
FullMsgId item);
|
||||
[[nodiscard]] ClickHandlerPtr MakeUrlAreaHandler(
|
||||
base::weak_ptr<Controller> weak,
|
||||
const QString &url);
|
||||
|
||||
} // namespace Media::Stories
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
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_delegate.h"
|
||||
|
||||
64
Telegram/SourceFiles/media/stories/media_stories_delegate.h
Normal file
64
Telegram/SourceFiles/media/stories/media_stories_delegate.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
struct FileChosen;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
class Story;
|
||||
struct StoriesContext;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
enum class JumpReason {
|
||||
Finished,
|
||||
User,
|
||||
};
|
||||
|
||||
enum class SiblingType {
|
||||
Left,
|
||||
Right,
|
||||
};
|
||||
|
||||
class Delegate {
|
||||
public:
|
||||
[[nodiscard]] virtual not_null<Ui::RpWidget*> storiesWrap() = 0;
|
||||
[[nodiscard]] virtual auto storiesShow()
|
||||
-> std::shared_ptr<ChatHelpers::Show> = 0;
|
||||
[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
|
||||
-> rpl::producer<ChatHelpers::FileChosen> = 0;
|
||||
virtual void storiesRedisplay(not_null<Data::Story*> story) = 0;
|
||||
virtual void storiesJumpTo(
|
||||
not_null<Main::Session*> session,
|
||||
FullStoryId id,
|
||||
Data::StoriesContext context) = 0;
|
||||
virtual void storiesClose() = 0;
|
||||
[[nodiscard]] virtual bool storiesPaused() = 0;
|
||||
[[nodiscard]] virtual rpl::producer<bool> storiesLayerShown() = 0;
|
||||
[[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0;
|
||||
virtual void storiesTogglePaused(bool paused) = 0;
|
||||
virtual void storiesRepaint() = 0;
|
||||
virtual void storiesVolumeToggle() = 0;
|
||||
virtual void storiesVolumeChanged(float64 volume) = 0;
|
||||
virtual void storiesVolumeChangeFinished() = 0;
|
||||
[[nodiscard]] virtual int storiesTopNotchSkip() = 0;
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
972
Telegram/SourceFiles/media/stories/media_stories_header.cpp
Normal file
972
Telegram/SourceFiles/media/stories/media_stories_header.cpp
Normal file
@@ -0,0 +1,972 @@
|
||||
/*
|
||||
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_header.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
#include "ui/text/custom_emoji_text_badge.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Media::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kNameOpacity = 1.;
|
||||
constexpr auto kDateOpacity = 0.8;
|
||||
constexpr auto kControlOpacity = 0.65;
|
||||
constexpr auto kControlOpacityOver = 1.;
|
||||
constexpr auto kControlOpacityDisabled = 0.45;
|
||||
constexpr auto kVolumeHideTimeoutShort = crl::time(20);
|
||||
constexpr auto kVolumeHideTimeoutLong = crl::time(200);
|
||||
|
||||
struct Timestamp {
|
||||
QString text;
|
||||
TimeId changes = 0;
|
||||
};
|
||||
|
||||
struct PrivacyBadge {
|
||||
const style::icon *icon = nullptr;
|
||||
const style::color *bg1 = nullptr;
|
||||
const style::color *bg2 = nullptr;
|
||||
};
|
||||
|
||||
class UserpicBadge final : public Ui::RpWidget {
|
||||
public:
|
||||
UserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge);
|
||||
|
||||
[[nodiscard]] QRect badgeGeometry() const;
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void updateGeometry();
|
||||
|
||||
const not_null<QWidget*> _userpic;
|
||||
const PrivacyBadge _badgeData;
|
||||
QRect _badge;
|
||||
QImage _layer;
|
||||
bool _grabbing = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] PrivacyBadge LookupPrivacyBadge(Data::StoryPrivacy privacy) {
|
||||
using namespace Data;
|
||||
static const auto badges = base::flat_map<StoryPrivacy, PrivacyBadge>{
|
||||
{ StoryPrivacy::CloseFriends, PrivacyBadge{
|
||||
&st::storiesBadgeCloseFriends,
|
||||
&st::historyPeer2UserpicBg,
|
||||
&st::historyPeer2UserpicBg2,
|
||||
} },
|
||||
{ StoryPrivacy::Contacts, PrivacyBadge{
|
||||
&st::storiesBadgeContacts,
|
||||
&st::historyPeer5UserpicBg,
|
||||
&st::historyPeer5UserpicBg2,
|
||||
} },
|
||||
{ StoryPrivacy::SelectedContacts, PrivacyBadge{
|
||||
&st::storiesBadgeSelectedContacts,
|
||||
&st::historyPeer8UserpicBg,
|
||||
&st::historyPeer8UserpicBg2,
|
||||
} },
|
||||
};
|
||||
if (const auto i = badges.find(privacy); i != end(badges)) {
|
||||
return i->second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
UserpicBadge::UserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge)
|
||||
: RpWidget(userpic->parentWidget())
|
||||
, _userpic(userpic)
|
||||
, _badgeData(badge) {
|
||||
userpic->installEventFilter(this);
|
||||
updateGeometry();
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
Ui::PostponeCall(this, [=] {
|
||||
_userpic->raise();
|
||||
});
|
||||
show();
|
||||
}
|
||||
|
||||
QRect UserpicBadge::badgeGeometry() const {
|
||||
return _badge;
|
||||
}
|
||||
|
||||
bool UserpicBadge::eventFilter(QObject *o, QEvent *e) {
|
||||
if (o != _userpic) {
|
||||
return false;
|
||||
}
|
||||
const auto type = e->type();
|
||||
switch (type) {
|
||||
case QEvent::Move:
|
||||
case QEvent::Resize:
|
||||
updateGeometry();
|
||||
return false;
|
||||
case QEvent::Paint:
|
||||
return !_grabbing;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UserpicBadge::paintEvent(QPaintEvent *e) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto layerSize = size() * ratio;
|
||||
if (_layer.size() != layerSize) {
|
||||
_layer = QImage(layerSize, QImage::Format_ARGB32_Premultiplied);
|
||||
_layer.setDevicePixelRatio(ratio);
|
||||
}
|
||||
_layer.fill(Qt::transparent);
|
||||
auto q = QPainter(&_layer);
|
||||
|
||||
_grabbing = true;
|
||||
Ui::RenderWidget(q, _userpic);
|
||||
_grabbing = false;
|
||||
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = st::transparent->p;
|
||||
pen.setWidthF(st::storiesBadgeOutline);
|
||||
const auto half = st::storiesBadgeOutline / 2.;
|
||||
auto outer = QRectF(_badge).marginsAdded({ half, half, half, half });
|
||||
auto gradient = QLinearGradient(outer.topLeft(), outer.bottomLeft());
|
||||
gradient.setStops({
|
||||
{ 0., (*_badgeData.bg1)->c },
|
||||
{ 1., (*_badgeData.bg2)->c },
|
||||
});
|
||||
q.setPen(pen);
|
||||
q.setBrush(gradient);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.drawEllipse(outer);
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
_badgeData.icon->paintInCenter(q, _badge);
|
||||
q.end();
|
||||
|
||||
QPainter(this).drawImage(0, 0, _layer);
|
||||
}
|
||||
|
||||
void UserpicBadge::updateGeometry() {
|
||||
const auto width = _userpic->width() + st::storiesBadgeShift.x();
|
||||
const auto height = _userpic->height() + st::storiesBadgeShift.y();
|
||||
setGeometry(QRect(_userpic->pos(), QSize{ width, height }));
|
||||
const auto inner = QRect(QPoint(), _badgeData.icon->size());
|
||||
const auto badge = inner.marginsAdded(st::storiesBadgePadding).size();
|
||||
_badge = QRect(
|
||||
QPoint(width - badge.width(), height - badge.height()),
|
||||
badge);
|
||||
update();
|
||||
}
|
||||
|
||||
struct MadePrivacyBadge {
|
||||
std::unique_ptr<Ui::RpWidget> widget;
|
||||
QRect geometry;
|
||||
};
|
||||
|
||||
[[nodiscard]] MadePrivacyBadge MakePrivacyBadge(
|
||||
not_null<QWidget*> userpic,
|
||||
Data::StoryPrivacy privacy) {
|
||||
const auto badge = LookupPrivacyBadge(privacy);
|
||||
if (!badge.icon) {
|
||||
return {};
|
||||
}
|
||||
auto widget = std::make_unique<UserpicBadge>(userpic, badge);
|
||||
const auto geometry = widget->badgeGeometry();
|
||||
return {
|
||||
.widget = std::move(widget),
|
||||
.geometry = geometry,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) {
|
||||
const auto minutes = (now - when) / 60;
|
||||
if (!minutes) {
|
||||
return { tr::lng_mediaview_just_now(tr::now), 61 - (now - when) };
|
||||
} else if (minutes < 60) {
|
||||
return {
|
||||
tr::lng_mediaview_minutes_ago(tr::now, lt_count, minutes),
|
||||
61 - ((now - when) % 60),
|
||||
};
|
||||
}
|
||||
const auto hours = (now - when) / 3600;
|
||||
if (hours < 12) {
|
||||
return {
|
||||
tr::lng_mediaview_hours_ago(tr::now, lt_count, hours),
|
||||
3601 - ((now - when) % 3600),
|
||||
};
|
||||
}
|
||||
const auto whenFull = base::unixtime::parse(when);
|
||||
const auto nowFull = base::unixtime::parse(now);
|
||||
const auto locale = QLocale();
|
||||
auto tomorrow = nowFull;
|
||||
tomorrow.setDate(nowFull.date().addDays(1));
|
||||
tomorrow.setTime(QTime(0, 0, 1));
|
||||
const auto seconds = int(nowFull.secsTo(tomorrow));
|
||||
if (whenFull.date() == nowFull.date()) {
|
||||
const auto whenTime = locale.toString(
|
||||
whenFull.time(),
|
||||
QLocale::ShortFormat);
|
||||
return {
|
||||
tr::lng_mediaview_today(tr::now, lt_time, whenTime),
|
||||
seconds,
|
||||
};
|
||||
} else if (whenFull.date().addDays(1) == nowFull.date()) {
|
||||
const auto whenTime = locale.toString(
|
||||
whenFull.time(),
|
||||
QLocale::ShortFormat);
|
||||
return {
|
||||
tr::lng_mediaview_yesterday(tr::now, lt_time, whenTime),
|
||||
seconds,
|
||||
};
|
||||
}
|
||||
return { Ui::FormatDateTime(whenFull) };
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ComposeCounter(HeaderData data) {
|
||||
const auto index = data.fullIndex + 1;
|
||||
const auto count = data.fullCount;
|
||||
return count
|
||||
? QString::fromUtf8(" \xE2\x80\xA2 %1/%2").arg(index).arg(count)
|
||||
: QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) {
|
||||
auto result = ComposeTimestamp(data.date, now);
|
||||
if (data.edited) {
|
||||
result.text.append(
|
||||
QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now));
|
||||
}
|
||||
if (data.fromPeer || !data.repostFrom.isEmpty()) {
|
||||
result.text = QString::fromUtf8("\xE2\x80\xA2 ")
|
||||
+ result.text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities FromNameValue(not_null<PeerData*> from) {
|
||||
auto result = Ui::Text::SingleCustomEmoji(
|
||||
from->owner().customEmojiManager().peerUserpicEmojiData(
|
||||
from,
|
||||
st::storiesRepostUserpicPadding));
|
||||
result.append(from->name());
|
||||
return tr::link(result);
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities RepostNameValue(
|
||||
not_null<Data::Session*> owner,
|
||||
PeerData *peer,
|
||||
QString name) {
|
||||
auto result = Ui::Text::IconEmoji(&st::storiesRepostIcon);
|
||||
if (peer) {
|
||||
result.append(Ui::Text::SingleCustomEmoji(
|
||||
owner->customEmojiManager().peerUserpicEmojiData(
|
||||
peer,
|
||||
st::storiesRepostUserpicPadding)));
|
||||
}
|
||||
result.append(name);
|
||||
return tr::link(result);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Header::Header(not_null<Controller*> controller)
|
||||
: _controller(controller)
|
||||
, _dateUpdateTimer([=] { updateDateText(); }) {
|
||||
}
|
||||
|
||||
Header::~Header() = default;
|
||||
|
||||
void Header::show(HeaderData data, rpl::producer<int> videoStreamViewers) {
|
||||
if (_data == data) {
|
||||
setVideoStreamViewers(std::move(videoStreamViewers));
|
||||
return;
|
||||
}
|
||||
const auto peerChanged = !_data || (_data->peer != data.peer);
|
||||
_data = data;
|
||||
const auto updateInfoGeometry = [=] {
|
||||
if (_name && _date) {
|
||||
const auto namex = st::storiesHeaderNamePosition.x();
|
||||
const auto namer = namex + _name->width();
|
||||
const auto datex = st::storiesHeaderDatePosition.x();
|
||||
const auto dater = datex
|
||||
+ (_repost ? _repost->width() : 0)
|
||||
+ _date->width();
|
||||
const auto r = std::max(namer, dater);
|
||||
_info->setGeometry({ 0, 0, r, _widget->height() });
|
||||
}
|
||||
};
|
||||
_tooltip = nullptr;
|
||||
_tooltipShown = false;
|
||||
if (peerChanged) {
|
||||
_volume = nullptr;
|
||||
_date = nullptr;
|
||||
_repost = nullptr;
|
||||
_name = nullptr;
|
||||
_counter = nullptr;
|
||||
_userpic = nullptr;
|
||||
_info = nullptr;
|
||||
_privacy = nullptr;
|
||||
_playPause = nullptr;
|
||||
_volumeToggle = nullptr;
|
||||
const auto parent = _controller->wrap();
|
||||
auto widget = std::make_unique<Ui::RpWidget>(parent);
|
||||
const auto raw = widget.get();
|
||||
|
||||
_info = std::make_unique<Ui::AbstractButton>(raw);
|
||||
_info->setClickedCallback([=] {
|
||||
_controller->uiShow()->show(PrepareShortInfoBox(_data->peer));
|
||||
});
|
||||
|
||||
_userpic = std::make_unique<Ui::UserpicButton>(
|
||||
raw,
|
||||
data.peer,
|
||||
st::storiesHeaderPhoto);
|
||||
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_userpic->show();
|
||||
_userpic->move(
|
||||
st::storiesHeaderMargin.left(),
|
||||
st::storiesHeaderMargin.top());
|
||||
|
||||
_name = std::make_unique<Ui::FlatLabel>(
|
||||
raw,
|
||||
rpl::single(data.peer->isSelf()
|
||||
? tr::lng_stories_my_name(tr::now)
|
||||
: data.peer->name()),
|
||||
st::storiesHeaderName);
|
||||
_name->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_name->setOpacity(kNameOpacity);
|
||||
_name->show();
|
||||
_name->move(st::storiesHeaderNamePosition);
|
||||
|
||||
rpl::combine(
|
||||
_name->widthValue(),
|
||||
raw->heightValue()
|
||||
) | rpl::on_next(updateInfoGeometry, _name->lifetime());
|
||||
|
||||
raw->show();
|
||||
_widget = std::move(widget);
|
||||
|
||||
_controller->layoutValue(
|
||||
) | rpl::on_next([=](const Layout &layout) {
|
||||
raw->setGeometry(layout.header);
|
||||
_contentGeometry = layout.content;
|
||||
updateTooltipGeometry();
|
||||
}, raw->lifetime());
|
||||
}
|
||||
auto timestamp = ComposeDetails(data, base::unixtime::now());
|
||||
_date = std::make_unique<Ui::FlatLabel>(
|
||||
_widget.get(),
|
||||
std::move(timestamp.text),
|
||||
st::storiesHeaderDate);
|
||||
_date->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_date->show();
|
||||
_date->move(st::storiesHeaderDatePosition);
|
||||
setVideoStreamViewers(std::move(videoStreamViewers));
|
||||
|
||||
_date->widthValue(
|
||||
) | rpl::on_next(updateInfoGeometry, _date->lifetime());
|
||||
|
||||
if (!data.fromPeer && data.repostFrom.isEmpty()) {
|
||||
_repost = nullptr;
|
||||
} else {
|
||||
_repost = std::make_unique<Ui::FlatLabel>(
|
||||
_widget.get(),
|
||||
st::storiesHeaderDate);
|
||||
const auto prefixName = data.fromPeer
|
||||
? FromNameValue(data.fromPeer)
|
||||
: RepostNameValue(
|
||||
&data.peer->owner(),
|
||||
data.repostPeer,
|
||||
data.repostFrom);
|
||||
const auto prefix = data.fromPeer ? data.fromPeer : data.repostPeer;
|
||||
_repost->setMarkedText(
|
||||
(prefix ? tr::link(prefixName) : prefixName),
|
||||
Core::TextContext({ .session = &data.peer->session() }));
|
||||
if (prefix) {
|
||||
_repost->setClickHandlerFilter([=](const auto &...) {
|
||||
_controller->uiShow()->show(PrepareShortInfoBox(prefix));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
_repost->show();
|
||||
_repost->widthValue(
|
||||
) | rpl::on_next(updateInfoGeometry, _repost->lifetime());
|
||||
}
|
||||
|
||||
auto counter = ComposeCounter(data);
|
||||
if (!counter.isEmpty()) {
|
||||
_counter = std::make_unique<Ui::FlatLabel>(
|
||||
_widget.get(),
|
||||
std::move(counter),
|
||||
st::storiesHeaderDate);
|
||||
_counter->resizeToWidth(_counter->textMaxWidth());
|
||||
_counter->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_counter->setOpacity(kNameOpacity);
|
||||
_counter->show();
|
||||
} else {
|
||||
_counter = nullptr;
|
||||
}
|
||||
|
||||
auto made = MakePrivacyBadge(_userpic.get(), data.privacy);
|
||||
_privacy = std::move(made.widget);
|
||||
_privacyBadgeOver = false;
|
||||
_privacyBadgeGeometry = _privacy
|
||||
? Ui::MapFrom(_info.get(), _privacy.get(), made.geometry)
|
||||
: QRect();
|
||||
if (_privacy) {
|
||||
_info->setMouseTracking(true);
|
||||
_info->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type != QEvent::Leave && type != QEvent::MouseMove) {
|
||||
return false;
|
||||
}
|
||||
const auto over = (type == QEvent::MouseMove)
|
||||
&& _privacyBadgeGeometry.contains(
|
||||
static_cast<QMouseEvent*>(e.get())->pos());
|
||||
return (_privacyBadgeOver != over);
|
||||
}) | rpl::on_next([=] {
|
||||
_privacyBadgeOver = !_privacyBadgeOver;
|
||||
toggleTooltip(Tooltip::Privacy, _privacyBadgeOver);
|
||||
}, _privacy->lifetime());
|
||||
}
|
||||
|
||||
if (data.video || data.videoStream) {
|
||||
if (data.video) {
|
||||
createPlayPause();
|
||||
}
|
||||
createVolumeToggle();
|
||||
|
||||
_widget->widthValue() | rpl::on_next([=](int width) {
|
||||
if (_playPause) {
|
||||
const auto playPause = st::storiesPlayButtonPosition;
|
||||
_playPause->moveToRight(playPause.x(), playPause.y(), width);
|
||||
}
|
||||
const auto volume = st::storiesVolumeButtonPosition;
|
||||
_volumeToggle->moveToRight(volume.x(), volume.y(), width);
|
||||
updateTooltipGeometry();
|
||||
}, _volumeToggle->lifetime());
|
||||
|
||||
if (data.video) {
|
||||
_pauseState = _controller->pauseState();
|
||||
applyPauseState();
|
||||
}
|
||||
} else {
|
||||
_playPause = nullptr;
|
||||
_volumeToggle = nullptr;
|
||||
_volume = nullptr;
|
||||
}
|
||||
|
||||
rpl::combine(
|
||||
_widget->widthValue(),
|
||||
_counter ? _counter->widthValue() : rpl::single(0),
|
||||
_dateUpdated.events_starting_with_copy(rpl::empty)
|
||||
) | rpl::on_next([=](int outer, int counter, auto) {
|
||||
const auto right = _playPause
|
||||
? _playPause->x()
|
||||
: (outer - st::storiesHeaderMargin.right());
|
||||
const auto nameLeft = st::storiesHeaderNamePosition.x();
|
||||
if (counter) {
|
||||
counter += st::normalFont->spacew;
|
||||
}
|
||||
const auto nameAvailable = right - nameLeft - counter;
|
||||
auto counterLeft = nameLeft;
|
||||
if (nameAvailable <= 0) {
|
||||
_name->hide();
|
||||
} else {
|
||||
_name->show();
|
||||
_name->resizeToNaturalWidth(nameAvailable);
|
||||
counterLeft += _name->width() + st::normalFont->spacew;
|
||||
}
|
||||
if (_counter) {
|
||||
_counter->move(counterLeft, _name->y());
|
||||
}
|
||||
const auto dateLeft = st::storiesHeaderDatePosition.x();
|
||||
const auto dateTop = st::storiesHeaderDatePosition.y();
|
||||
const auto dateSkip = _repost ? st::storiesHeaderRepostWidthMin : 0;
|
||||
const auto dateAvailable = right - dateLeft - dateSkip;
|
||||
if (dateAvailable <= 0) {
|
||||
_date->hide();
|
||||
} else {
|
||||
_date->show();
|
||||
_date->resizeToNaturalWidth(dateAvailable);
|
||||
}
|
||||
if (_repost) {
|
||||
const auto repostAvailable = dateAvailable
|
||||
+ dateSkip
|
||||
- _date->width();
|
||||
if (repostAvailable <= 0) {
|
||||
_repost->hide();
|
||||
} else {
|
||||
_repost->show();
|
||||
_repost->resizeToNaturalWidth(repostAvailable);
|
||||
}
|
||||
_repost->move(dateLeft, dateTop);
|
||||
const auto space = st::normalFont->spacew;
|
||||
_date->move(dateLeft + _repost->width() + space, dateTop);
|
||||
} else {
|
||||
_date->move(dateLeft, dateTop);
|
||||
}
|
||||
}, _date->lifetime());
|
||||
|
||||
if (timestamp.changes > 0) {
|
||||
_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
|
||||
}
|
||||
}
|
||||
|
||||
void Header::setVideoStreamViewers(rpl::producer<int> viewers) {
|
||||
_videoStreamViewersLifetime.destroy();
|
||||
if (!_date) {
|
||||
return;
|
||||
} else if (!viewers) {
|
||||
_date->setOpacity(kDateOpacity);
|
||||
return;
|
||||
}
|
||||
_date->setOpacity(1.);
|
||||
auto helper = Ui::Text::CustomEmojiHelper();
|
||||
const auto badge = helper.paletteDependent(
|
||||
Ui::Text::CustomEmojiTextBadge(
|
||||
tr::lng_video_stream_live(tr::now),
|
||||
st::groupCallMessageBadge,
|
||||
st::groupCallMessageBadgeMargin));
|
||||
const auto context = helper.context();
|
||||
_videoStreamViewersLifetime = std::move(
|
||||
viewers
|
||||
) | rpl::on_next([=](int count) {
|
||||
auto text = badge;
|
||||
if (count) {
|
||||
text.append(' ').append(tr::lng_group_call_rtmp_viewers(
|
||||
tr::now,
|
||||
lt_count_decimal,
|
||||
count));
|
||||
}
|
||||
_date->setMarkedText(text, context);
|
||||
_dateUpdated.fire({});
|
||||
});
|
||||
}
|
||||
|
||||
void Header::createPlayPause() {
|
||||
struct PlayPauseState {
|
||||
Ui::Animations::Simple overAnimation;
|
||||
bool over = false;
|
||||
bool down = false;
|
||||
};
|
||||
_playPause = std::make_unique<Ui::RpWidget>(_widget.get());
|
||||
auto &lifetime = _playPause->lifetime();
|
||||
const auto state = lifetime.make_state<PlayPauseState>();
|
||||
|
||||
_playPause->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter || type == QEvent::Leave) {
|
||||
const auto over = (e->type() == QEvent::Enter);
|
||||
if (state->over != over) {
|
||||
state->over = over;
|
||||
state->overAnimation.start(
|
||||
[=] { _playPause->update(); },
|
||||
over ? 0. : 1.,
|
||||
over ? 1. : 0.,
|
||||
st::mediaviewFadeDuration);
|
||||
}
|
||||
} else if (type == QEvent::MouseButtonPress && state->over) {
|
||||
state->down = true;
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
const auto down = base::take(state->down);
|
||||
if (down && state->over) {
|
||||
const auto paused = (_pauseState == PauseState::Paused)
|
||||
|| (_pauseState == PauseState::InactivePaused);
|
||||
_controller->togglePaused(!paused);
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
_playPause->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(_playPause.get());
|
||||
const auto paused = (_pauseState == PauseState::Paused)
|
||||
|| (_pauseState == PauseState::InactivePaused);
|
||||
const auto icon = paused
|
||||
? &st::storiesPlayIcon
|
||||
: &st::storiesPauseIcon;
|
||||
const auto over = state->overAnimation.value(
|
||||
state->over ? 1. : 0.);
|
||||
p.setOpacity(over * kControlOpacityOver
|
||||
+ (1. - over) * kControlOpacity);
|
||||
icon->paint(
|
||||
p,
|
||||
st::storiesPlayButton.iconPosition,
|
||||
_playPause->width());
|
||||
}, lifetime);
|
||||
|
||||
_playPause->resize(
|
||||
st::storiesPlayButton.width,
|
||||
st::storiesPlayButton.height);
|
||||
_playPause->show();
|
||||
_playPause->setCursor(style::cur_pointer);
|
||||
}
|
||||
|
||||
void Header::createVolumeToggle() {
|
||||
Expects(_data.has_value());
|
||||
|
||||
struct VolumeState {
|
||||
base::Timer hideTimer;
|
||||
bool over = false;
|
||||
bool silent = false;
|
||||
bool dropdownOver = false;
|
||||
};
|
||||
_volumeToggle = std::make_unique<Ui::RpWidget>(_widget.get());
|
||||
_volume = std::make_unique<Ui::FadeWrap<Ui::RpWidget>>(
|
||||
_widget->parentWidget(),
|
||||
object_ptr<Ui::RpWidget>(_widget->parentWidget()));
|
||||
|
||||
auto &lifetime = _volume->lifetime();
|
||||
const auto state = lifetime.make_state<VolumeState>();
|
||||
state->silent = _data->silent;
|
||||
state->hideTimer.setCallback([=] {
|
||||
_volume->toggle(false, anim::type::normal);
|
||||
});
|
||||
|
||||
_volumeToggle->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter || type == QEvent::Leave) {
|
||||
const auto over = (e->type() == QEvent::Enter);
|
||||
if (state->over != over) {
|
||||
state->over = over;
|
||||
if (state->silent) {
|
||||
toggleTooltip(Tooltip::SilentVideo, over);
|
||||
} else if (over) {
|
||||
state->hideTimer.cancel();
|
||||
_volume->toggle(true, anim::type::normal);
|
||||
} else if (!state->dropdownOver) {
|
||||
state->hideTimer.callOnce(kVolumeHideTimeoutShort);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
_volumeToggle->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(_volumeToggle.get());
|
||||
p.setOpacity(state->silent
|
||||
? kControlOpacityDisabled
|
||||
: kControlOpacity);
|
||||
_volumeIcon.current()->paint(
|
||||
p,
|
||||
st::storiesVolumeButton.iconPosition,
|
||||
_volumeToggle->width());
|
||||
}, lifetime);
|
||||
updateVolumeIcon();
|
||||
|
||||
_volume->toggle(false, anim::type::instant);
|
||||
_volume->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter || type == QEvent::Leave) {
|
||||
const auto over = (e->type() == QEvent::Enter);
|
||||
if (state->dropdownOver != over) {
|
||||
state->dropdownOver = over;
|
||||
if (over) {
|
||||
state->hideTimer.cancel();
|
||||
_volume->toggle(true, anim::type::normal);
|
||||
} else if (!state->over) {
|
||||
state->hideTimer.callOnce(kVolumeHideTimeoutLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
rebuildVolumeControls(_volume->entity(), false);
|
||||
|
||||
rpl::combine(
|
||||
_widget->positionValue(),
|
||||
_volumeToggle->positionValue(),
|
||||
rpl::mappers::_1 + rpl::mappers::_2
|
||||
) | rpl::on_next([=](QPoint position) {
|
||||
_volume->move(position);
|
||||
}, _volume->lifetime());
|
||||
|
||||
_volumeToggle->resize(
|
||||
st::storiesVolumeButton.width,
|
||||
st::storiesVolumeButton.height);
|
||||
_volumeToggle->show();
|
||||
if (!state->silent) {
|
||||
_volumeToggle->setCursor(style::cur_pointer);
|
||||
}
|
||||
}
|
||||
|
||||
void Header::toggleTooltip(Tooltip type, bool show) {
|
||||
const auto guard = gsl::finally([&] {
|
||||
_tooltipShown = (_tooltip != nullptr);
|
||||
});
|
||||
if (const auto was = _tooltip.release()) {
|
||||
was->toggleAnimated(false);
|
||||
}
|
||||
if (!show) {
|
||||
return;
|
||||
}
|
||||
const auto text = [&]() -> TextWithEntities {
|
||||
using Privacy = Data::StoryPrivacy;
|
||||
const auto boldName = tr::bold(_data->peer->shortName());
|
||||
const auto self = _data->peer->isSelf();
|
||||
switch (type) {
|
||||
case Tooltip::SilentVideo:
|
||||
return { tr::lng_stories_about_silent(tr::now) };
|
||||
case Tooltip::Privacy: switch (_data->privacy) {
|
||||
case Privacy::CloseFriends:
|
||||
return self
|
||||
? tr::lng_stories_about_close_friends_my(
|
||||
tr::now,
|
||||
tr::rich)
|
||||
: tr::lng_stories_about_close_friends(
|
||||
tr::now,
|
||||
lt_user,
|
||||
boldName,
|
||||
tr::rich);
|
||||
case Privacy::Contacts:
|
||||
return self
|
||||
? tr::lng_stories_about_contacts_my(
|
||||
tr::now,
|
||||
tr::rich)
|
||||
: tr::lng_stories_about_contacts(
|
||||
tr::now,
|
||||
lt_user,
|
||||
boldName,
|
||||
tr::rich);
|
||||
case Privacy::SelectedContacts:
|
||||
return self
|
||||
? tr::lng_stories_about_selected_contacts_my(
|
||||
tr::now,
|
||||
tr::rich)
|
||||
: tr::lng_stories_about_selected_contacts(
|
||||
tr::now,
|
||||
lt_user,
|
||||
boldName,
|
||||
tr::rich);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}();
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
_tooltipType = type;
|
||||
_tooltip = std::make_unique<Ui::ImportantTooltip>(
|
||||
_widget->parentWidget(),
|
||||
Ui::MakeNiceTooltipLabel(
|
||||
_widget.get(),
|
||||
rpl::single(text),
|
||||
st::storiesInfoTooltipMaxWidth,
|
||||
st::storiesInfoTooltipLabel),
|
||||
st::storiesInfoTooltip);
|
||||
const auto tooltip = _tooltip.get();
|
||||
const auto weak = base::make_weak(tooltip);
|
||||
const auto destroy = [=] {
|
||||
delete weak.get();
|
||||
};
|
||||
tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
tooltip->setHiddenCallback(destroy);
|
||||
updateTooltipGeometry();
|
||||
tooltip->toggleAnimated(true);
|
||||
}
|
||||
|
||||
void Header::updateTooltipGeometry() {
|
||||
if (!_tooltip) {
|
||||
return;
|
||||
}
|
||||
const auto geometry = [&] {
|
||||
switch (_tooltipType) {
|
||||
case Tooltip::SilentVideo:
|
||||
return Ui::MapFrom(
|
||||
_widget->parentWidget(),
|
||||
_volumeToggle.get(),
|
||||
_volumeToggle->rect());
|
||||
case Tooltip::Privacy:
|
||||
return Ui::MapFrom(
|
||||
_widget->parentWidget(),
|
||||
_info.get(),
|
||||
_privacyBadgeGeometry.marginsAdded(
|
||||
st::storiesInfoTooltip.padding));
|
||||
}
|
||||
return QRect();
|
||||
}();
|
||||
if (geometry.isEmpty()) {
|
||||
toggleTooltip(Tooltip::None, false);
|
||||
return;
|
||||
}
|
||||
const auto weak = QPointer<QWidget>(_tooltip.get());
|
||||
const auto countPosition = [=](QSize size) {
|
||||
const auto result = geometry.bottomLeft()
|
||||
- QPoint(size.width() / 2, 0);
|
||||
const auto inner = _contentGeometry.marginsRemoved(
|
||||
st::storiesInfoTooltip.padding);
|
||||
if (size.width() > inner.width()) {
|
||||
return QPoint(
|
||||
inner.x() + (inner.width() - size.width()) / 2,
|
||||
result.y());
|
||||
} else if (result.x() < inner.x()) {
|
||||
return QPoint(inner.x(), result.y());
|
||||
}
|
||||
return result;
|
||||
};
|
||||
_tooltip->pointAt(geometry, RectPart::Bottom, countPosition);
|
||||
}
|
||||
|
||||
void Header::rebuildVolumeControls(
|
||||
not_null<Ui::RpWidget*> dropdown,
|
||||
bool horizontal) {
|
||||
auto removed = false;
|
||||
do {
|
||||
removed = false;
|
||||
for (const auto &child : dropdown->children()) {
|
||||
if (child->isWidgetType()) {
|
||||
removed = true;
|
||||
delete child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (removed);
|
||||
|
||||
const auto button = Ui::CreateChild<Ui::IconButton>(
|
||||
dropdown.get(),
|
||||
st::storiesVolumeButton);
|
||||
_volumeIcon.value(
|
||||
) | rpl::on_next([=](const style::icon *icon) {
|
||||
button->setIconOverride(icon, icon);
|
||||
}, button->lifetime());
|
||||
|
||||
const auto slider = Ui::CreateChild<Ui::MediaSlider>(
|
||||
dropdown.get(),
|
||||
st::storiesVolumeSlider);
|
||||
slider->setMoveByWheel(true);
|
||||
slider->setAlwaysDisplayMarker(true);
|
||||
using Direction = Ui::MediaSlider::Direction;
|
||||
slider->setDirection(horizontal
|
||||
? Direction::Horizontal
|
||||
: Direction::Vertical);
|
||||
|
||||
slider->setChangeProgressCallback([=](float64 value) {
|
||||
_ignoreWindowMove = true;
|
||||
_controller->changeVolume(value);
|
||||
updateVolumeIcon();
|
||||
});
|
||||
slider->setChangeFinishedCallback([=](float64 value) {
|
||||
_ignoreWindowMove = false;
|
||||
_controller->volumeChangeFinished();
|
||||
});
|
||||
button->setClickedCallback([=] {
|
||||
_controller->toggleVolume();
|
||||
slider->setValue(_controller->currentVolume());
|
||||
updateVolumeIcon();
|
||||
});
|
||||
slider->setValue(_controller->currentVolume());
|
||||
|
||||
const auto size = button->width()
|
||||
+ st::storiesVolumeSize
|
||||
+ st::storiesVolumeBottom;
|
||||
const auto seekSize = st::storiesVolumeSlider.seekSize;
|
||||
|
||||
button->move(0, 0);
|
||||
if (horizontal) {
|
||||
dropdown->resize(size, button->height());
|
||||
slider->resize(st::storiesVolumeSize, seekSize.height());
|
||||
slider->move(
|
||||
button->width(),
|
||||
(button->height() - slider->height()) / 2);
|
||||
} else {
|
||||
dropdown->resize(button->width(), size);
|
||||
slider->resize(seekSize.width(), st::storiesVolumeSize);
|
||||
slider->move(
|
||||
(button->width() - slider->width()) / 2,
|
||||
button->height());
|
||||
}
|
||||
|
||||
dropdown->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(dropdown);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = button->width() / 2.;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::mediaviewSaveMsgBg);
|
||||
p.drawRoundedRect(dropdown->rect(), radius, radius);
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
void Header::updatePauseState() {
|
||||
if (!_playPause) {
|
||||
return;
|
||||
} else if (const auto s = _controller->pauseState(); _pauseState != s) {
|
||||
_pauseState = s;
|
||||
applyPauseState();
|
||||
}
|
||||
}
|
||||
|
||||
void Header::updateVolumeIcon() {
|
||||
const auto volume = _controller->currentVolume();
|
||||
_volumeIcon = (volume <= 0. || (_data && _data->silent))
|
||||
? &st::mediaviewVolumeIcon0Over
|
||||
: (volume < 1 / 2.)
|
||||
? &st::mediaviewVolumeIcon1Over
|
||||
: &st::mediaviewVolumeIcon2Over;
|
||||
}
|
||||
|
||||
void Header::applyPauseState() {
|
||||
Expects(_playPause != nullptr);
|
||||
|
||||
const auto inactive = (_pauseState == PauseState::Inactive)
|
||||
|| (_pauseState == PauseState::InactivePaused);
|
||||
_playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive);
|
||||
if (inactive) {
|
||||
QEvent e(QEvent::Leave);
|
||||
QGuiApplication::sendEvent(_playPause.get(), &e);
|
||||
}
|
||||
_playPause->update();
|
||||
}
|
||||
|
||||
void Header::raise() {
|
||||
if (_widget) {
|
||||
_widget->raise();
|
||||
}
|
||||
}
|
||||
|
||||
bool Header::ignoreWindowMove(QPoint position) const {
|
||||
return _ignoreWindowMove;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Header::tooltipShownValue() const {
|
||||
return _tooltipShown.value();
|
||||
}
|
||||
|
||||
void Header::updateDateText() {
|
||||
if (!_date || !_data || !_data->date || _videoStreamViewersLifetime) {
|
||||
return;
|
||||
}
|
||||
auto timestamp = ComposeDetails(*_data, base::unixtime::now());
|
||||
_date->setText(timestamp.text);
|
||||
_dateUpdated.fire({});
|
||||
if (timestamp.changes > 0) {
|
||||
_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
114
Telegram/SourceFiles/media/stories/media_stories_header.h
Normal file
114
Telegram/SourceFiles/media/stories/media_stories_header.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "ui/userpic_view.h"
|
||||
|
||||
namespace Data {
|
||||
enum class StoryPrivacy : uchar;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class FlatLabel;
|
||||
class IconButton;
|
||||
class AbstractButton;
|
||||
class UserpicButton;
|
||||
class ImportantTooltip;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
enum class PauseState;
|
||||
|
||||
struct HeaderData {
|
||||
not_null<PeerData*> peer;
|
||||
PeerData *fromPeer = nullptr;
|
||||
PeerData *repostPeer = nullptr;
|
||||
QString repostFrom;
|
||||
TimeId date = 0;
|
||||
int fullIndex = 0;
|
||||
int fullCount = 0;
|
||||
Data::StoryPrivacy privacy = {};
|
||||
bool edited = false;
|
||||
bool video = false;
|
||||
bool videoStream = false;
|
||||
bool silent = false;
|
||||
|
||||
friend inline auto operator<=>(HeaderData, HeaderData) = default;
|
||||
friend inline bool operator==(HeaderData, HeaderData) = default;
|
||||
};
|
||||
|
||||
class Header final {
|
||||
public:
|
||||
explicit Header(not_null<Controller*> controller);
|
||||
~Header();
|
||||
|
||||
void updatePauseState();
|
||||
void updateVolumeIcon();
|
||||
|
||||
void show(HeaderData data, rpl::producer<int> videoStreamViewers);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
|
||||
[[nodiscard]] rpl::producer<bool> tooltipShownValue() const;
|
||||
|
||||
private:
|
||||
enum class Tooltip {
|
||||
None,
|
||||
SilentVideo,
|
||||
Privacy,
|
||||
};
|
||||
|
||||
void updateDateText();
|
||||
void applyPauseState();
|
||||
void createPlayPause();
|
||||
void createVolumeToggle();
|
||||
void rebuildVolumeControls(
|
||||
not_null<Ui::RpWidget*> dropdown,
|
||||
bool horizontal);
|
||||
void toggleTooltip(Tooltip type, bool show);
|
||||
void updateTooltipGeometry();
|
||||
void setVideoStreamViewers(rpl::producer<int> viewers);
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
|
||||
PauseState _pauseState = {};
|
||||
|
||||
std::unique_ptr<Ui::RpWidget> _widget;
|
||||
std::unique_ptr<Ui::AbstractButton> _info;
|
||||
std::unique_ptr<Ui::UserpicButton> _userpic;
|
||||
std::unique_ptr<Ui::FlatLabel> _name;
|
||||
std::unique_ptr<Ui::FlatLabel> _counter;
|
||||
std::unique_ptr<Ui::FlatLabel> _repost;
|
||||
std::unique_ptr<Ui::FlatLabel> _date;
|
||||
rpl::event_stream<> _dateUpdated;
|
||||
std::unique_ptr<Ui::RpWidget> _playPause;
|
||||
std::unique_ptr<Ui::RpWidget> _volumeToggle;
|
||||
std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;
|
||||
rpl::variable<const style::icon*> _volumeIcon;
|
||||
std::unique_ptr<Ui::RpWidget> _privacy;
|
||||
QRect _privacyBadgeGeometry;
|
||||
std::optional<HeaderData> _data;
|
||||
std::unique_ptr<Ui::ImportantTooltip> _tooltip;
|
||||
rpl::variable<bool> _tooltipShown = false;
|
||||
QRect _contentGeometry;
|
||||
Tooltip _tooltipType = {};
|
||||
base::Timer _dateUpdateTimer;
|
||||
bool _ignoreWindowMove = false;
|
||||
bool _privacyBadgeOver = false;
|
||||
|
||||
rpl::lifetime _videoStreamViewersLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
1445
Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
Normal file
1445
Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
149
Telegram/SourceFiles/media/stories/media_stories_reactions.h
Normal file
149
Telegram/SourceFiles/media/stories/media_stories_reactions.h
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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_message_reaction_id.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
struct ReactionId;
|
||||
class Session;
|
||||
class Story;
|
||||
struct SuggestedReaction;
|
||||
struct WeatherArea;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView::Reactions {
|
||||
class Selector;
|
||||
struct ChosenReaction;
|
||||
enum class AttachSelectorResult;
|
||||
} // namespace HistoryView::Reactions
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
struct ReactionFlyAnimationArgs;
|
||||
struct ReactionFlyCenter;
|
||||
class EmojiFlyAnimation;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
|
||||
enum class ReactionsMode {
|
||||
Message,
|
||||
Reaction,
|
||||
};
|
||||
|
||||
class StoryAreaView {
|
||||
public:
|
||||
virtual ~StoryAreaView() = default;
|
||||
|
||||
virtual void setAreaGeometry(QRect geometry, float64 radius) = 0;
|
||||
virtual void updateReactionsCount(int count) = 0;
|
||||
virtual void playEffect() = 0;
|
||||
virtual bool contains(QPoint point) = 0;
|
||||
};
|
||||
|
||||
class Reactions final {
|
||||
public:
|
||||
explicit Reactions(not_null<Controller*> controller);
|
||||
~Reactions();
|
||||
|
||||
using Mode = ReactionsMode;
|
||||
|
||||
template <typename Reaction>
|
||||
struct ChosenWrap {
|
||||
Reaction reaction;
|
||||
Mode mode;
|
||||
};
|
||||
using Chosen = ChosenWrap<HistoryView::Reactions::ChosenReaction>;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> activeValue() const;
|
||||
[[nodiscard]] rpl::producer<Chosen> chosen() const;
|
||||
|
||||
[[nodiscard]] Data::ReactionId liked() const;
|
||||
[[nodiscard]] rpl::producer<Data::ReactionId> likedValue() const;
|
||||
void showLikeFrom(Data::Story *story);
|
||||
|
||||
void hide();
|
||||
void outsidePressed();
|
||||
void toggleLiked();
|
||||
void applyLike(Data::ReactionId id);
|
||||
void ready();
|
||||
|
||||
[[nodiscard]] auto makeSuggestedReactionWidget(
|
||||
const Data::SuggestedReaction &reaction)
|
||||
-> std::unique_ptr<StoryAreaView>;
|
||||
[[nodiscard]] auto makeWeatherAreaWidget(
|
||||
const Data::WeatherArea &data,
|
||||
rpl::producer<bool> weatherInCelsius)
|
||||
-> std::unique_ptr<StoryAreaView>;
|
||||
|
||||
void setReplyFieldState(
|
||||
rpl::producer<bool> focused,
|
||||
rpl::producer<bool> hasSendText);
|
||||
void attachToReactionButton(not_null<Ui::RpWidget*> button);
|
||||
void setReactionIconWidget(Ui::RpWidget *widget);
|
||||
|
||||
void animateAndProcess(Chosen &&chosen);
|
||||
|
||||
using AttachStripResult = HistoryView::Reactions::AttachSelectorResult;
|
||||
[[nodiscard]] AttachStripResult attachToMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
QPoint desiredPosition);
|
||||
|
||||
private:
|
||||
class Panel;
|
||||
|
||||
void assignLikedId(Data::ReactionId id);
|
||||
[[nodiscard]] Fn<void(Ui::ReactionFlyCenter)> setLikedIdIconInit(
|
||||
not_null<Data::Session*> owner,
|
||||
Data::ReactionId id,
|
||||
bool force = false);
|
||||
void setLikedIdFrom(Data::Story *story);
|
||||
void setLikedId(
|
||||
not_null<Data::Session*> owner,
|
||||
Data::ReactionId id,
|
||||
bool force = false);
|
||||
void startReactionAnimation(
|
||||
Ui::ReactionFlyAnimationArgs from,
|
||||
not_null<QWidget*> target,
|
||||
Fn<void(Ui::ReactionFlyCenter)> done = nullptr);
|
||||
void waitForLikeIcon(
|
||||
not_null<Data::Session*> owner,
|
||||
Data::ReactionId id);
|
||||
void initLikeIcon(
|
||||
not_null<Data::Session*> owner,
|
||||
Data::ReactionId id,
|
||||
Ui::ReactionFlyCenter center);
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
const std::unique_ptr<Panel> _panel;
|
||||
|
||||
rpl::event_stream<Chosen> _chosen;
|
||||
bool _replyFocused = false;
|
||||
bool _hasSendText = false;
|
||||
|
||||
Ui::RpWidget *_likeIconWidget = nullptr;
|
||||
rpl::variable<Data::ReactionId> _liked;
|
||||
base::has_weak_ptr _likeIconGuard;
|
||||
std::unique_ptr<Ui::RpWidget> _likeIcon;
|
||||
std::shared_ptr<Data::DocumentMedia> _likeIconMedia;
|
||||
|
||||
std::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;
|
||||
|
||||
rpl::lifetime _likeIconWaitLifetime;
|
||||
rpl::lifetime _likeFromLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
@@ -0,0 +1,672 @@
|
||||
/*
|
||||
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_recent_views.h"
|
||||
|
||||
#include "api/api_who_reacted.h" // FormatReadDate.
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "history/history_item.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/controls/who_reacted_context_action.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kAddPerPage = 50;
|
||||
constexpr auto kLoadViewsPages = 2;
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<Ui::GroupCallUser>> ContentByUsers(
|
||||
const std::vector<not_null<PeerData*>> &list) {
|
||||
struct Userpic {
|
||||
not_null<PeerData*> peer;
|
||||
mutable Ui::PeerUserpicView view;
|
||||
mutable InMemoryKey uniqueKey;
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::vector<Userpic> userpics;
|
||||
std::vector<Ui::GroupCallUser> current;
|
||||
base::has_weak_ptr guard;
|
||||
bool someUserpicsNotLoaded = false;
|
||||
bool scheduled = false;
|
||||
};
|
||||
|
||||
static const auto size = st::storiesWhoViewed.userpics.size;
|
||||
|
||||
static const auto GenerateUserpic = [](Userpic &userpic) {
|
||||
auto result = PeerData::GenerateUserpicImage(
|
||||
userpic.peer,
|
||||
userpic.view,
|
||||
size * style::DevicePixelRatio());
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
return result;
|
||||
};
|
||||
|
||||
static const auto RegenerateUserpics = [](not_null<State*> state) {
|
||||
Expects(state->userpics.size() == state->current.size());
|
||||
|
||||
state->someUserpicsNotLoaded = false;
|
||||
const auto count = int(state->userpics.size());
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
auto &userpic = state->userpics[i];
|
||||
auto &participant = state->current[i];
|
||||
const auto peer = userpic.peer;
|
||||
const auto key = peer->userpicUniqueKey(userpic.view);
|
||||
if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) {
|
||||
state->someUserpicsNotLoaded = true;
|
||||
}
|
||||
if (userpic.uniqueKey == key) {
|
||||
continue;
|
||||
}
|
||||
participant.userpicKey = userpic.uniqueKey = key;
|
||||
participant.userpic = GenerateUserpic(userpic);
|
||||
}
|
||||
};
|
||||
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
const auto state = lifetime.make_state<State>();
|
||||
const auto pushNext = [=] {
|
||||
RegenerateUserpics(state);
|
||||
consumer.put_next_copy(state->current);
|
||||
};
|
||||
|
||||
for (const auto &peer : list) {
|
||||
state->userpics.push_back(Userpic{
|
||||
.peer = peer,
|
||||
});
|
||||
state->current.push_back(Ui::GroupCallUser{
|
||||
.id = uint64(peer->id.value),
|
||||
});
|
||||
peer->loadUserpic();
|
||||
}
|
||||
pushNext();
|
||||
|
||||
if (!list.empty()) {
|
||||
list.front()->session().downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
return state->someUserpicsNotLoaded && !state->scheduled;
|
||||
}) | rpl::on_next([=] {
|
||||
for (const auto &userpic : state->userpics) {
|
||||
if (userpic.peer->userpicUniqueKey(userpic.view)
|
||||
!= userpic.uniqueKey) {
|
||||
state->scheduled = true;
|
||||
crl::on_main(&state->guard, [=] {
|
||||
state->scheduled = false;
|
||||
pushNext();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
return lifetime;
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ComposeRepostStatus(
|
||||
const QString &date,
|
||||
not_null<Data::Story*> repost) {
|
||||
return date + (repost->repostModified()
|
||||
? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now))
|
||||
: !repost->caption().empty()
|
||||
? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_commented(tr::now))
|
||||
: QString());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RecentViewsType RecentViewsTypeFor(
|
||||
not_null<PeerData*> peer,
|
||||
bool videoStream) {
|
||||
return videoStream
|
||||
? RecentViewsType::Other
|
||||
: peer->isSelf()
|
||||
? RecentViewsType::Self
|
||||
: peer->isBroadcast()
|
||||
? RecentViewsType::Channel
|
||||
: peer->isServiceUser()
|
||||
? RecentViewsType::Changelog
|
||||
: RecentViewsType::Other;
|
||||
}
|
||||
|
||||
bool CanViewReactionsFor(not_null<PeerData*> peer) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
return channel->amCreator() || channel->hasAdminRights();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RecentViews::RecentViews(not_null<Controller*> controller)
|
||||
: _controller(controller) {
|
||||
}
|
||||
|
||||
RecentViews::~RecentViews() = default;
|
||||
|
||||
void RecentViews::show(
|
||||
RecentViewsData data,
|
||||
rpl::producer<Data::ReactionId> likedValue) {
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (_likeIcon && likedValue) {
|
||||
std::move(
|
||||
likedValue
|
||||
) | rpl::map([](const Data::ReactionId &id) {
|
||||
return !id.empty();
|
||||
}) | rpl::on_next([=](bool liked) {
|
||||
const auto icon = liked
|
||||
? &st::storiesComposeControls.liked
|
||||
: &st::storiesLikesIcon;
|
||||
_likeIcon->setIconOverride(icon, icon);
|
||||
}, _likeIcon->lifetime());
|
||||
}
|
||||
});
|
||||
|
||||
if (_data == data) {
|
||||
return;
|
||||
}
|
||||
const auto countersChanged = _text.isEmpty()
|
||||
|| (_data.total != data.total)
|
||||
|| (_data.views != data.views)
|
||||
|| (_data.forwards != data.forwards)
|
||||
|| (_data.reactions != data.reactions);
|
||||
const auto usersChanged = !_userpics || (_data.list != data.list);
|
||||
const auto canViewReactions = data.canViewReactions
|
||||
&& (data.reactions > 0 || data.forwards > 0);
|
||||
_data = data;
|
||||
if (_data.type != RecentViewsType::Self && !canViewReactions) {
|
||||
_text = {};
|
||||
_clickHandlerLifetime.destroy();
|
||||
_userpicsLifetime.destroy();
|
||||
_userpics = nullptr;
|
||||
_widget = nullptr;
|
||||
} else {
|
||||
if (!_widget) {
|
||||
setupWidget();
|
||||
}
|
||||
if (!_userpics) {
|
||||
setupUserpics();
|
||||
}
|
||||
if (countersChanged) {
|
||||
updateText();
|
||||
}
|
||||
if (usersChanged) {
|
||||
updateUserpics();
|
||||
}
|
||||
refreshClickHandler();
|
||||
}
|
||||
|
||||
if (_data.type != RecentViewsType::Channel
|
||||
&& _data.type != RecentViewsType::Changelog) {
|
||||
_likeIcon = nullptr;
|
||||
_likeWrap = nullptr;
|
||||
_viewsWrap = nullptr;
|
||||
} else {
|
||||
_viewsCounter = (_data.type == RecentViewsType::Channel)
|
||||
? Lang::FormatCountDecimal(std::max(_data.views, 1))
|
||||
: tr::lng_stories_cant_reply(tr::now);
|
||||
_likesCounter = ((_data.type == RecentViewsType::Channel)
|
||||
&& _data.reactions)
|
||||
? Lang::FormatCountDecimal(_data.reactions)
|
||||
: QString();
|
||||
if (!_likeWrap || !_likeIcon || !_viewsWrap) {
|
||||
setupViewsReactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ui::RpWidget *RecentViews::likeButton() const {
|
||||
return _likeWrap.get();
|
||||
}
|
||||
|
||||
Ui::RpWidget *RecentViews::likeIconWidget() const {
|
||||
return _likeIcon.get();
|
||||
}
|
||||
|
||||
void RecentViews::refreshClickHandler() {
|
||||
const auto nowEmpty = (_data.type != RecentViewsType::Channel)
|
||||
&& _data.list.empty();
|
||||
const auto wasEmpty = !_clickHandlerLifetime;
|
||||
const auto raw = _widget.get();
|
||||
if (wasEmpty == nowEmpty) {
|
||||
return;
|
||||
} else if (nowEmpty) {
|
||||
_clickHandlerLifetime.destroy();
|
||||
} else {
|
||||
_clickHandlerLifetime = raw->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (_data.total > 0)
|
||||
&& (e->type() == QEvent::MouseButtonPress)
|
||||
&& (static_cast<QMouseEvent*>(e.get())->button()
|
||||
== Qt::LeftButton);
|
||||
}) | rpl::on_next([=] {
|
||||
showMenu();
|
||||
});
|
||||
}
|
||||
raw->setCursor(_clickHandlerLifetime
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
}
|
||||
|
||||
void RecentViews::updateUserpics() {
|
||||
_userpicsLifetime = ContentByUsers(
|
||||
_data.list
|
||||
) | rpl::on_next([=](
|
||||
const std::vector<Ui::GroupCallUser> &list) {
|
||||
_userpics->update(list, true);
|
||||
});
|
||||
_userpics->finishAnimating();
|
||||
}
|
||||
|
||||
void RecentViews::setupUserpics() {
|
||||
_userpics = std::make_unique<Ui::GroupCallUserpics>(
|
||||
st::storiesWhoViewed.userpics,
|
||||
rpl::single(true),
|
||||
[=] { _widget->update(); });
|
||||
|
||||
_userpics->widthValue() | rpl::on_next([=](int width) {
|
||||
if (_userpicsWidth != width) {
|
||||
_userpicsWidth = width;
|
||||
updatePartsGeometry();
|
||||
}
|
||||
}, _widget->lifetime());
|
||||
}
|
||||
|
||||
void RecentViews::setupWidget() {
|
||||
_widget = std::make_unique<Ui::RpWidget>(_controller->wrap());
|
||||
const auto raw = _widget.get();
|
||||
raw->show();
|
||||
|
||||
_controller->layoutValue(
|
||||
) | rpl::on_next([=](const Layout &layout) {
|
||||
_outer = layout.views;
|
||||
updatePartsGeometry();
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = Painter(raw);
|
||||
_userpics->paint(
|
||||
p,
|
||||
_userpicsPosition.x(),
|
||||
_userpicsPosition.y(),
|
||||
st::storiesWhoViewed.userpics.size);
|
||||
p.setPen(st::storiesComposeWhiteText);
|
||||
_text.drawElided(
|
||||
p,
|
||||
_textPosition.x(),
|
||||
_textPosition.y(),
|
||||
raw->width() - _userpicsWidth - st::storiesRecentViewsSkip);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
|
||||
void RecentViews::setupViewsReactions() {
|
||||
_viewsWrap = std::make_unique<Ui::RpWidget>(_controller->wrap());
|
||||
_likeWrap = std::make_unique<Ui::AbstractButton>(_controller->wrap());
|
||||
_likeIcon = std::make_unique<Ui::IconButton>(
|
||||
_likeWrap.get(),
|
||||
st::storiesComposeControls.like);
|
||||
_likeIcon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_controller->layoutValue(
|
||||
) | rpl::on_next([=](const Layout &layout) {
|
||||
_outer = QRect(
|
||||
layout.content.x(),
|
||||
layout.views.y(),
|
||||
layout.content.width(),
|
||||
layout.views.height());
|
||||
updateViewsReactionsGeometry();
|
||||
}, _likeWrap->lifetime());
|
||||
|
||||
const auto views = Ui::CreateChild<Ui::FlatLabel>(
|
||||
_viewsWrap.get(),
|
||||
_viewsCounter.value(),
|
||||
st::storiesViewsText);
|
||||
views->show();
|
||||
views->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
views->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto left = (_data.type == RecentViewsType::Changelog)
|
||||
? st::mediaviewCaptionPadding.left()
|
||||
: st::storiesViewsTextPosition.x();
|
||||
views->move(left, st::storiesViewsTextPosition.y());
|
||||
_viewsWrap->resize(left + width, _likeIcon->height());
|
||||
updateViewsReactionsGeometry();
|
||||
}, _viewsWrap->lifetime());
|
||||
_viewsWrap->paintRequest() | rpl::filter([=] {
|
||||
return (_data.type != RecentViewsType::Changelog);
|
||||
}) | rpl::on_next([=] {
|
||||
auto p = QPainter(_viewsWrap.get());
|
||||
const auto &icon = st::storiesViewsIcon;
|
||||
const auto top = (_viewsWrap->height() - icon.height()) / 2;
|
||||
icon.paint(p, 0, top, _viewsWrap->width());
|
||||
}, _viewsWrap->lifetime());
|
||||
|
||||
_likeIcon->move(0, 0);
|
||||
const auto likes = Ui::CreateChild<Ui::FlatLabel>(
|
||||
_likeWrap.get(),
|
||||
_likesCounter.value(),
|
||||
st::storiesLikesText);
|
||||
likes->show();
|
||||
likes->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
likes->move(st::storiesLikesTextPosition);
|
||||
|
||||
likes->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
width += width
|
||||
? st::storiesLikesTextRightSkip
|
||||
: st::storiesLikesEmptyRightSkip;
|
||||
_likeWrap->resize(likes->x() + width, _likeIcon->height());
|
||||
updateViewsReactionsGeometry();
|
||||
}, _likeWrap->lifetime());
|
||||
|
||||
_viewsWrap->show();
|
||||
_likeIcon->show();
|
||||
_likeWrap->show();
|
||||
|
||||
_likeWrap->setClickedCallback([=] {
|
||||
_controller->toggleLiked();
|
||||
});
|
||||
}
|
||||
|
||||
void RecentViews::updateViewsReactionsGeometry() {
|
||||
const auto outerWidth = (_data.type == RecentViewsType::Changelog)
|
||||
? std::max(_outer.width(), st::storiesChangelogFooterWidthMin)
|
||||
: _outer.width();
|
||||
const auto outerOrigin = _outer.topLeft()
|
||||
+ QPoint((_outer.width() - outerWidth) / 2, 0);
|
||||
_viewsWrap->move(outerOrigin + st::storiesViewsPosition);
|
||||
_likeWrap->move(outerOrigin
|
||||
+ QPoint(outerWidth - _likeWrap->width(), 0)
|
||||
+ st::storiesLikesPosition);
|
||||
}
|
||||
|
||||
void RecentViews::updatePartsGeometry() {
|
||||
const auto skip = st::storiesRecentViewsSkip;
|
||||
const auto full = _userpicsWidth + skip + _text.maxWidth();
|
||||
const auto add = (_data.type == RecentViewsType::Channel)
|
||||
? st::storiesViewsTextPosition.y()
|
||||
: 0;
|
||||
const auto use = std::min(full, _outer.width());
|
||||
const auto ux = _outer.x() + (_outer.width() - use) / 2;
|
||||
const auto uheight = st::storiesWhoViewed.userpics.size;
|
||||
const auto uy = _outer.y() + (_outer.height() - uheight) / 2 + add;
|
||||
const auto tx = ux + _userpicsWidth + skip;
|
||||
const auto theight = st::normalFont->height;
|
||||
const auto ty = _outer.y() + (_outer.height() - theight) / 2 + add;
|
||||
const auto my = std::min(uy, ty);
|
||||
const auto mheight = std::max(uheight, theight);
|
||||
const auto padding = skip;
|
||||
_userpicsPosition = QPoint(padding, uy - my);
|
||||
_textPosition = QPoint(tx - ux + padding, ty - my);
|
||||
_widget->setGeometry(ux - padding, my, use + 2 * padding, mheight);
|
||||
_widget->update();
|
||||
}
|
||||
|
||||
void RecentViews::updateText() {
|
||||
const auto text = (_data.type == RecentViewsType::Channel)
|
||||
? tr::lng_stories_view_reactions(tr::now)
|
||||
: _data.views
|
||||
? (tr::lng_stories_views(tr::now, lt_count, _data.views)
|
||||
+ (_data.reactions
|
||||
? (u" "_q + QChar(10084) + QString::number(_data.reactions))
|
||||
: QString()))
|
||||
: tr::lng_stories_no_views(tr::now);
|
||||
_text.setText(st::defaultTextStyle, text);
|
||||
updatePartsGeometry();
|
||||
}
|
||||
|
||||
void RecentViews::showMenu() {
|
||||
if (_menu
|
||||
|| (_data.type != RecentViewsType::Channel && _data.list.empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto views = _controller->views(kAddPerPage * 2, true);
|
||||
if (views.list.empty() && !views.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
using namespace Ui;
|
||||
_menuShortLifetime.destroy();
|
||||
_menu = base::make_unique_q<PopupMenu>(
|
||||
_widget.get(),
|
||||
st::storiesViewsMenu);
|
||||
auto count = 0;
|
||||
const auto session = &_controller->story()->session();
|
||||
const auto added = std::min(int(views.list.size()), kAddPerPage);
|
||||
const auto add = std::min(views.total, kAddPerPage);
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
for (const auto &entry : views.list) {
|
||||
addMenuRow(entry, now);
|
||||
if (++count >= add) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (count++ < add) {
|
||||
addMenuRowPlaceholder(session);
|
||||
}
|
||||
rpl::merge(
|
||||
_controller->moreViewsLoaded(),
|
||||
rpl::combine(
|
||||
_menu->scrollTopValue(),
|
||||
_menuEntriesCount.value()
|
||||
) | rpl::filter([=](int scrollTop, int count) {
|
||||
const auto fullHeight = count
|
||||
* (st::defaultWhoRead.photoSkip * 2
|
||||
+ st::defaultWhoRead.photoSize);
|
||||
return fullHeight
|
||||
< (scrollTop
|
||||
+ st::storiesViewsMenu.maxHeight * kLoadViewsPages);
|
||||
}) | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
rebuildMenuTail();
|
||||
}, _menuShortLifetime);
|
||||
|
||||
_controller->setMenuShown(true);
|
||||
_menu->setDestroyedCallback(crl::guard(_widget.get(), [=] {
|
||||
_controller->setMenuShown(false);
|
||||
_waitingForUserpicsLifetime.destroy();
|
||||
_waitingForUserpics.clear();
|
||||
_menuShortLifetime.destroy();
|
||||
_menuEntries.clear();
|
||||
_menuEntriesCount = 0;
|
||||
_menuPlaceholderCount = 0;
|
||||
}));
|
||||
|
||||
const auto size = _menu->size();
|
||||
const auto geometry = _widget->mapToGlobal(_widget->rect());
|
||||
_menu->setForcedVerticalOrigin(PopupMenu::VerticalOrigin::Bottom);
|
||||
_menu->popup(QPoint(
|
||||
geometry.x() + (_widget->width() - size.width()) / 2,
|
||||
geometry.y() + _widget->height()));
|
||||
|
||||
_menuEntriesCount = _menuEntriesCount.current() + added;
|
||||
}
|
||||
|
||||
void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
|
||||
Expects(_menu != nullptr);
|
||||
|
||||
const auto peer = entry.peer;
|
||||
const auto repost = entry.repostId
|
||||
? peer->owner().stories().lookup({ peer->id, entry.repostId })
|
||||
: base::make_unexpected(Data::NoStory::Deleted);
|
||||
const auto forward = entry.forwardId
|
||||
? peer->owner().message({ peer->id, entry.forwardId })
|
||||
: nullptr;
|
||||
const auto date = Api::FormatReadDate(
|
||||
repost ? (*repost)->date() : forward ? forward->date() : entry.date,
|
||||
now);
|
||||
const auto type = forward
|
||||
? Ui::WhoReactedType::Forwarded
|
||||
: repost
|
||||
? Ui::WhoReactedType::Reposted
|
||||
: Ui::WhoReactedType::Viewed;
|
||||
const auto status = repost ? ComposeRepostStatus(date, *repost) : date;
|
||||
const auto show = _controller->uiShow();
|
||||
const auto prepare = [&](Ui::PeerUserpicView &view) {
|
||||
const auto size = st::storiesWhoViewed.photoSize;
|
||||
auto userpic = PeerData::GenerateUserpicImage(
|
||||
peer,
|
||||
view,
|
||||
size * style::DevicePixelRatio());
|
||||
userpic.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
return Ui::WhoReactedEntryData{
|
||||
.text = peer->name(),
|
||||
.date = status,
|
||||
.type = type,
|
||||
.customEntityData = Data::ReactionEntityData(entry.reaction),
|
||||
.userpic = std::move(userpic),
|
||||
.callback = [=] { show->show(PrepareShortInfoBox(peer)); },
|
||||
};
|
||||
};
|
||||
if (_menuPlaceholderCount > 0) {
|
||||
const auto i = _menuEntries.end() - (_menuPlaceholderCount--);
|
||||
auto data = prepare(i->view);
|
||||
i->peer = peer;
|
||||
i->type = type;
|
||||
i->status = status;
|
||||
i->customEntityData = data.customEntityData;
|
||||
i->callback = data.callback;
|
||||
i->action->setData(std::move(data));
|
||||
} else {
|
||||
auto view = Ui::PeerUserpicView();
|
||||
auto data = prepare(view);
|
||||
auto callback = data.callback;
|
||||
auto customEntityData = data.customEntityData;
|
||||
auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
|
||||
_menu->menu(),
|
||||
Data::ReactedMenuFactory(&entry.peer->session()),
|
||||
_menu->menu()->st(),
|
||||
prepare(view));
|
||||
const auto raw = action.get();
|
||||
_menu->addAction(std::move(action));
|
||||
_menuEntries.push_back({
|
||||
.action = raw,
|
||||
.peer = peer,
|
||||
.type = type,
|
||||
.status = status,
|
||||
.customEntityData = std::move(customEntityData),
|
||||
.callback = std::move(callback),
|
||||
.view = std::move(view),
|
||||
});
|
||||
}
|
||||
const auto i = end(_menuEntries) - _menuPlaceholderCount - 1;
|
||||
i->key = peer->userpicUniqueKey(i->view);
|
||||
if (peer->hasUserpic() && peer->useEmptyUserpic(i->view)) {
|
||||
if (_waitingForUserpics.emplace(i - begin(_menuEntries)).second
|
||||
&& _waitingForUserpics.size() == 1) {
|
||||
subscribeToMenuUserpicsLoading(&peer->session());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RecentViews::addMenuRowPlaceholder(not_null<Main::Session*> session) {
|
||||
auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
|
||||
_menu->menu(),
|
||||
Data::ReactedMenuFactory(session),
|
||||
_menu->menu()->st(),
|
||||
Ui::WhoReactedEntryData{ .type = Ui::WhoReactedType::Preloader });
|
||||
const auto raw = action.get();
|
||||
_menu->addAction(std::move(action));
|
||||
_menuEntries.push_back({ .action = raw });
|
||||
++_menuPlaceholderCount;
|
||||
}
|
||||
|
||||
void RecentViews::rebuildMenuTail() {
|
||||
const auto elements = _menuEntries.size() - _menuPlaceholderCount;
|
||||
const auto views = _controller->views(elements + kAddPerPage, false);
|
||||
if (views.list.size() <= elements) {
|
||||
return;
|
||||
}
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto added = std::min(
|
||||
_menuPlaceholderCount + kAddPerPage,
|
||||
int(views.list.size() - elements));
|
||||
const auto height = _menu->height();
|
||||
for (auto i = elements, till = i + added; i != till; ++i) {
|
||||
const auto &entry = views.list[i];
|
||||
addMenuRow(entry, now);
|
||||
}
|
||||
_menuEntriesCount = _menuEntriesCount.current() + added;
|
||||
if (const auto delta = _menu->height() - height) {
|
||||
_menu->move(_menu->x(), _menu->y() - delta);
|
||||
}
|
||||
}
|
||||
|
||||
void RecentViews::subscribeToMenuUserpicsLoading(
|
||||
not_null<Main::Session*> session) {
|
||||
_shortAnimationPlaying = style::ShortAnimationPlaying();
|
||||
_waitingForUserpicsLifetime = rpl::merge(
|
||||
_shortAnimationPlaying.changes() | rpl::filter([=](bool playing) {
|
||||
return !playing && _waitingUserpicsCheck;
|
||||
}) | rpl::to_empty,
|
||||
session->downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
if (_shortAnimationPlaying.current()) {
|
||||
_waitingUserpicsCheck = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
) | rpl::on_next([=] {
|
||||
_waitingUserpicsCheck = false;
|
||||
for (auto i = begin(_waitingForUserpics)
|
||||
; i != end(_waitingForUserpics)
|
||||
;) {
|
||||
auto &entry = _menuEntries[*i];
|
||||
auto &view = entry.view;
|
||||
const auto peer = entry.peer;
|
||||
const auto key = peer->userpicUniqueKey(view);
|
||||
const auto update = (entry.key != key);
|
||||
if (update) {
|
||||
const auto size = st::storiesWhoViewed.photoSize;
|
||||
auto userpic = PeerData::GenerateUserpicImage(
|
||||
peer,
|
||||
view,
|
||||
size * style::DevicePixelRatio());
|
||||
userpic.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
entry.action->setData({
|
||||
.text = peer->name(),
|
||||
.date = entry.status,
|
||||
.type = entry.type,
|
||||
.customEntityData = entry.customEntityData,
|
||||
.userpic = std::move(userpic),
|
||||
.callback = entry.callback,
|
||||
});
|
||||
entry.key = key;
|
||||
if (!peer->hasUserpic() || !peer->useEmptyUserpic(view)) {
|
||||
i = _waitingForUserpics.erase(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (_waitingForUserpics.empty()) {
|
||||
_waitingForUserpicsLifetime.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
138
Telegram/SourceFiles/media/stories/media_stories_recent_views.h
Normal file
138
Telegram/SourceFiles/media/stories/media_stories_recent_views.h
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/userpic_view.h"
|
||||
|
||||
namespace Data {
|
||||
struct StoryView;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class IconButton;
|
||||
class RpWidget;
|
||||
class GroupCallUserpics;
|
||||
class PopupMenu;
|
||||
class WhoReactedEntryAction;
|
||||
enum class WhoReactedType : uchar;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
|
||||
enum class RecentViewsType {
|
||||
Other,
|
||||
Self,
|
||||
Channel,
|
||||
Changelog,
|
||||
};
|
||||
|
||||
struct RecentViewsData {
|
||||
std::vector<not_null<PeerData*>> list;
|
||||
int reactions = 0;
|
||||
int forwards = 0;
|
||||
int views = 0;
|
||||
int total = 0;
|
||||
RecentViewsType type = RecentViewsType::Other;
|
||||
bool canViewReactions = false;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const RecentViewsData &,
|
||||
const RecentViewsData &) = default;
|
||||
friend inline bool operator==(
|
||||
const RecentViewsData &,
|
||||
const RecentViewsData &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] RecentViewsType RecentViewsTypeFor(
|
||||
not_null<PeerData*> peer,
|
||||
bool videoStream);
|
||||
[[nodiscard]] bool CanViewReactionsFor(not_null<PeerData*> peer);
|
||||
|
||||
class RecentViews final {
|
||||
public:
|
||||
explicit RecentViews(not_null<Controller*> controller);
|
||||
~RecentViews();
|
||||
|
||||
void show(
|
||||
RecentViewsData data,
|
||||
rpl::producer<Data::ReactionId> likedValue = nullptr);
|
||||
|
||||
[[nodiscard]] Ui::RpWidget *likeButton() const;
|
||||
[[nodiscard]] Ui::RpWidget *likeIconWidget() const;
|
||||
|
||||
private:
|
||||
struct MenuEntry {
|
||||
not_null<Ui::WhoReactedEntryAction*> action;
|
||||
PeerData *peer = nullptr;
|
||||
Ui::WhoReactedType type = {};
|
||||
QString status;
|
||||
QString customEntityData;
|
||||
Fn<void()> callback;
|
||||
Ui::PeerUserpicView view;
|
||||
InMemoryKey key;
|
||||
};
|
||||
|
||||
void setupWidget();
|
||||
void setupUserpics();
|
||||
void updateUserpics();
|
||||
void updateText();
|
||||
void updatePartsGeometry();
|
||||
void showMenu();
|
||||
|
||||
void setupViewsReactions();
|
||||
void updateViewsReactionsGeometry();
|
||||
|
||||
void addMenuRow(Data::StoryView entry, const QDateTime &now);
|
||||
void addMenuRowPlaceholder(not_null<Main::Session*> session);
|
||||
void rebuildMenuTail();
|
||||
void subscribeToMenuUserpicsLoading(not_null<Main::Session*> session);
|
||||
void refreshClickHandler();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
|
||||
std::unique_ptr<Ui::RpWidget> _widget;
|
||||
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
|
||||
Ui::Text::String _text;
|
||||
RecentViewsData _data;
|
||||
rpl::lifetime _userpicsLifetime;
|
||||
|
||||
rpl::variable<QString> _viewsCounter;
|
||||
rpl::variable<QString> _likesCounter;
|
||||
std::unique_ptr<Ui::RpWidget> _viewsWrap;
|
||||
std::unique_ptr<Ui::AbstractButton> _likeWrap;
|
||||
std::unique_ptr<Ui::IconButton> _likeIcon;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
rpl::lifetime _menuShortLifetime;
|
||||
std::vector<MenuEntry> _menuEntries;
|
||||
rpl::variable<int> _menuEntriesCount = 0;
|
||||
int _menuPlaceholderCount = 0;
|
||||
base::flat_set<int> _waitingForUserpics;
|
||||
rpl::variable<bool> _shortAnimationPlaying;
|
||||
bool _waitingUserpicsCheck = false;
|
||||
rpl::lifetime _waitingForUserpicsLifetime;
|
||||
rpl::lifetime _clickHandlerLifetime;
|
||||
|
||||
QRect _outer;
|
||||
QPoint _userpicsPosition;
|
||||
QPoint _textPosition;
|
||||
int _userpicsWidth = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
1094
Telegram/SourceFiles/media/stories/media_stories_reply.cpp
Normal file
1094
Telegram/SourceFiles/media/stories/media_stories_reply.cpp
Normal file
File diff suppressed because it is too large
Load Diff
195
Telegram/SourceFiles/media/stories/media_stories_reply.h
Normal file
195
Telegram/SourceFiles/media/stories/media_stories_reply.h
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
|
||||
class History;
|
||||
enum class SendMediaType;
|
||||
|
||||
namespace Api {
|
||||
struct MessageToSend;
|
||||
struct SendAction;
|
||||
struct SendOptions;
|
||||
} // namespace Api
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
class ComposeControls;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
struct VoiceToSend;
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
namespace InlineBots {
|
||||
class Result;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace SendMenu {
|
||||
struct Details;
|
||||
} // namespace SendMenu
|
||||
|
||||
namespace Ui {
|
||||
struct PreparedList;
|
||||
struct PreparedBundle;
|
||||
class SendFilesWay;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
|
||||
struct ReplyAreaData {
|
||||
PeerData *peer = nullptr;
|
||||
StoryId id = 0;
|
||||
std::shared_ptr<Data::GroupCall> videoStream;
|
||||
|
||||
friend inline auto operator<=>(ReplyAreaData, ReplyAreaData) = default;
|
||||
friend inline bool operator==(ReplyAreaData, ReplyAreaData) = default;
|
||||
};
|
||||
|
||||
enum class ReplyAreaType {
|
||||
Reply,
|
||||
Comment,
|
||||
VideoStreamComment,
|
||||
};
|
||||
|
||||
class ReplyArea final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit ReplyArea(not_null<Controller*> controller);
|
||||
~ReplyArea();
|
||||
|
||||
void show(
|
||||
ReplyAreaData data,
|
||||
rpl::producer<Data::ReactionId> likedValue);
|
||||
bool sendReaction(const Data::ReactionId &id);
|
||||
|
||||
[[nodiscard]] bool focused() const;
|
||||
[[nodiscard]] rpl::producer<bool> focusedValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> activeValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
|
||||
|
||||
[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
|
||||
void tryProcessKeyInput(not_null<QKeyEvent*> e);
|
||||
|
||||
[[nodiscard]] Ui::RpWidget *likeAnimationTarget() const;
|
||||
|
||||
void updateVideoStream(not_null<Calls::GroupCall*> videoStream);
|
||||
|
||||
private:
|
||||
class Cant;
|
||||
|
||||
using VoiceToSend = HistoryView::Controls::VoiceToSend;
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<History*> history() const;
|
||||
|
||||
bool send(
|
||||
Api::MessageToSend message,
|
||||
bool skipToast = false);
|
||||
|
||||
[[nodiscard]] bool checkSendPayment(
|
||||
int messagesCount,
|
||||
Api::SendOptions options,
|
||||
Fn<void(int)> withPaymentApproved);
|
||||
|
||||
void uploadFile(const QByteArray &fileContent, SendMediaType type);
|
||||
bool confirmSendingFiles(
|
||||
QImage &&image,
|
||||
QByteArray &&content,
|
||||
std::optional<bool> overrideSendImagesAsPhotos = std::nullopt,
|
||||
const QString &insertTextOnCancel = QString());
|
||||
bool confirmSendingFiles(
|
||||
Ui::PreparedList &&list,
|
||||
const QString &insertTextOnCancel = QString());
|
||||
bool confirmSendingFiles(
|
||||
not_null<const QMimeData*> data,
|
||||
std::optional<bool> overrideSendImagesAsPhotos,
|
||||
const QString &insertTextOnCancel = QString());
|
||||
bool showSendingFilesError(const Ui::PreparedList &list) const;
|
||||
bool showSendingFilesError(
|
||||
const Ui::PreparedList &list,
|
||||
std::optional<bool> compress) const;
|
||||
void sendingFilesConfirmed(
|
||||
Ui::PreparedList &&list,
|
||||
Ui::SendFilesWay way,
|
||||
TextWithTags &&caption,
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter);
|
||||
void sendingFilesConfirmed(
|
||||
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||
Api::SendOptions options);
|
||||
void finishSending(bool skipToast = false);
|
||||
|
||||
bool sendExistingDocument(
|
||||
not_null<DocumentData*> document,
|
||||
Api::MessageToSend messageToSend,
|
||||
std::optional<MsgId> localId);
|
||||
void sendExistingPhoto(not_null<PhotoData*> photo);
|
||||
bool sendExistingPhoto(
|
||||
not_null<PhotoData*> photo,
|
||||
Api::SendOptions options);
|
||||
void sendInlineResult(
|
||||
std::shared_ptr<InlineBots::Result> result,
|
||||
not_null<UserData*> bot);
|
||||
void sendInlineResult(
|
||||
std::shared_ptr<InlineBots::Result> result,
|
||||
not_null<UserData*> bot,
|
||||
Api::SendOptions options,
|
||||
std::optional<MsgId> localMessageId);
|
||||
|
||||
void initGeometry();
|
||||
void initActions();
|
||||
|
||||
[[nodiscard]] Api::SendAction prepareSendAction(
|
||||
Api::SendOptions options) const;
|
||||
void send(Api::SendOptions options);
|
||||
void sendVoice(const VoiceToSend &data);
|
||||
void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);
|
||||
|
||||
[[nodiscard]] Fn<SendMenu::Details()> sendMenuDetails() const;
|
||||
[[nodiscard]] rpl::producer<int> starsPerMessageValue() const;
|
||||
|
||||
void showPremiumToast(not_null<DocumentData*> emoji);
|
||||
[[nodiscard]] bool showSlowmodeError();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
rpl::variable<ReplyAreaType> _type;
|
||||
rpl::variable<int> _starsForMessage;
|
||||
base::weak_ptr<Calls::GroupCall> _videoStream;
|
||||
|
||||
const std::unique_ptr<HistoryView::ComposeControls> _controls;
|
||||
std::unique_ptr<Cant> _cant;
|
||||
|
||||
ReplyAreaData _data;
|
||||
base::has_weak_ptr _shownPeerGuard;
|
||||
bool _chooseAttachRequest = false;
|
||||
rpl::variable<bool> _choosingAttach;
|
||||
|
||||
SendPaymentHelper _sendPayment;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
298
Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp
Normal file
298
Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
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_repost_view.h"
|
||||
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "history/view/history_view_reply.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "media/stories/media_stories_view.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
RepostView::RepostView(
|
||||
not_null<Controller*> controller,
|
||||
not_null<Data::Story*> story)
|
||||
: _controller(controller)
|
||||
, _story(story)
|
||||
, _sourcePeer(_story->repost()
|
||||
? _story->repostSourcePeer()
|
||||
: _story->owner().peer(
|
||||
_story->channelPosts().front().itemId.peer).get()) {
|
||||
Expects(_story->repost() || !_story->channelPosts().empty());
|
||||
|
||||
if (!_story->repost()) {
|
||||
_link = MakeChannelPostHandler(
|
||||
&_story->session(),
|
||||
_story->channelPosts().front().itemId);
|
||||
}
|
||||
|
||||
_story->session().colorIndicesValue(
|
||||
) | rpl::on_next([=](Ui::ColorIndicesCompressed &&indices) {
|
||||
_colorIndices = std::move(indices);
|
||||
if (_maxWidth) {
|
||||
_controller->repaint();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
RepostView::~RepostView() = default;
|
||||
|
||||
int RepostView::height() const {
|
||||
return st::historyReplyPadding.top()
|
||||
+ st::semiboldFont->height
|
||||
+ st::normalFont->height
|
||||
+ st::historyReplyPadding.bottom();
|
||||
}
|
||||
|
||||
void RepostView::draw(Painter &p, int x, int y, int availableWidth) {
|
||||
if (!_maxWidth) {
|
||||
recountDimensions();
|
||||
}
|
||||
if (_loading) {
|
||||
return;
|
||||
}
|
||||
const auto simple = _text.isEmpty();
|
||||
if (simple) {
|
||||
y += st::normalFont->height;
|
||||
}
|
||||
const auto w = _lastWidth = std::min(int(_maxWidth), availableWidth);
|
||||
const auto h = height() - (simple ? st::normalFont->height : 0);
|
||||
const auto rect = QRect(x, y, w, h);
|
||||
const auto backgroundEmojiId = (!simple && _sourcePeer)
|
||||
? _sourcePeer->backgroundEmojiId()
|
||||
: DocumentId();
|
||||
const auto cache = &_quoteCache;
|
||||
const auto "eSt = simple
|
||||
? st::storiesRepostSimpleStyle
|
||||
: st::messageQuoteStyle;
|
||||
const auto backgroundEmoji = backgroundEmojiId
|
||||
? &_backgroundEmojiData
|
||||
: nullptr;
|
||||
const auto backgroundEmojiCache = backgroundEmoji
|
||||
? &backgroundEmoji->caches[0]
|
||||
: nullptr;
|
||||
|
||||
auto rippleColor = cache->bg;
|
||||
cache->bg = QColor(0, 0, 0, 64);
|
||||
Ui::Text::ValidateQuotePaintCache(*cache, quoteSt);
|
||||
Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt);
|
||||
if (backgroundEmoji) {
|
||||
using namespace HistoryView;
|
||||
if (backgroundEmoji->firstFrameMask.isNull()
|
||||
&& !backgroundEmoji->emoji) {
|
||||
backgroundEmoji->emoji = CreateBackgroundEmojiInstance(
|
||||
&_story->owner(),
|
||||
backgroundEmojiId,
|
||||
crl::guard(this, [=] { _controller->repaint(); }));
|
||||
}
|
||||
ValidateBackgroundEmoji(
|
||||
backgroundEmoji,
|
||||
backgroundEmojiCache,
|
||||
cache);
|
||||
if (!backgroundEmojiCache->frames[0].isNull()) {
|
||||
const auto hasQuoteIcon = false;
|
||||
FillBackgroundEmoji(
|
||||
p,
|
||||
rect,
|
||||
hasQuoteIcon,
|
||||
*backgroundEmojiCache,
|
||||
backgroundEmoji->firstGiftFrame);
|
||||
}
|
||||
}
|
||||
cache->bg = rippleColor;
|
||||
|
||||
if (_ripple) {
|
||||
_ripple->paint(p, x, y, w, &rippleColor);
|
||||
if (_ripple->empty()) {
|
||||
_ripple.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if (w > st::historyReplyPadding.left()) {
|
||||
const auto textw = w
|
||||
- st::historyReplyPadding.left()
|
||||
- st::historyReplyPadding.right();
|
||||
const auto namew = textw;
|
||||
if (namew > 0) {
|
||||
p.setPen(cache->icon);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
x + st::historyReplyPadding.left(),
|
||||
y + st::historyReplyPadding.top(),
|
||||
namew,
|
||||
w + 2 * x);
|
||||
if (!simple) {
|
||||
const auto textLeft = x + st::historyReplyPadding.left();
|
||||
const auto textTop = y
|
||||
+ st::historyReplyPadding.top()
|
||||
+ st::semiboldFont->height;
|
||||
_text.draw(p, {
|
||||
.position = { textLeft, textTop },
|
||||
.availableWidth = textw,
|
||||
.palette = &st::mediaviewTextPalette,
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.pausedEmoji = On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = On(PowerSaving::kChatSpoiler),
|
||||
.elisionLines = 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RepostClickHandler RepostView::lookupHandler(QPoint position) {
|
||||
if (_loading) {
|
||||
return {};
|
||||
}
|
||||
const auto simple = _text.isEmpty();
|
||||
const auto w = _lastWidth;
|
||||
const auto skip = simple ? st::normalFont->height : 0;
|
||||
const auto h = height() - skip;
|
||||
const auto rect = QRect(0, skip, w, h);
|
||||
if (!rect.contains(position)) {
|
||||
return {};
|
||||
} else if (!_link) {
|
||||
_link = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
|
||||
const auto peer = _story->repostSourcePeer();
|
||||
const auto owner = &_story->owner();
|
||||
if (const auto id = peer ? _story->repostSourceId() : 0) {
|
||||
const auto of = owner->stories().lookup({ peer->id, id });
|
||||
if (of) {
|
||||
using namespace Data;
|
||||
_controller->jumpTo(*of, { StoriesContextSingle() });
|
||||
} else {
|
||||
_controller->uiShow()->show(PrepareShortInfoBox(peer));
|
||||
}
|
||||
} else {
|
||||
_controller->uiShow()->showToast(
|
||||
tr::lng_forwarded_story_expired(tr::now));
|
||||
}
|
||||
}));
|
||||
}
|
||||
_lastPosition = position;
|
||||
return { _link, this };
|
||||
}
|
||||
|
||||
PeerData *RepostView::fromPeer() const {
|
||||
return _sourcePeer;
|
||||
}
|
||||
|
||||
QString RepostView::fromName() const {
|
||||
return _sourcePeer ? _sourcePeer->name() : _story->repostSourceName();
|
||||
}
|
||||
|
||||
void RepostView::recountDimensions() {
|
||||
const auto name = _sourcePeer
|
||||
? _sourcePeer->name()
|
||||
: _story->repostSourceName();
|
||||
const auto owner = &_story->owner();
|
||||
const auto repostId = _story->repost() ? _story->repostSourceId() : 0;
|
||||
|
||||
const auto colorIndexPlusOne = _sourcePeer
|
||||
? (_sourcePeer->colorIndex() + 1)
|
||||
: 1;
|
||||
const auto dark = true;
|
||||
const auto colorPattern = colorIndexPlusOne
|
||||
? Ui::ColorPatternIndex(_colorIndices, colorIndexPlusOne - 1, dark)
|
||||
: 0;
|
||||
Assert(colorPattern < Ui::Text::kMaxQuoteOutlines);
|
||||
const auto values = Ui::SimpleColorIndexValues(
|
||||
QColor(255, 255, 255),
|
||||
colorPattern);
|
||||
_quoteCache.bg = values.bg;
|
||||
_quoteCache.outlines = values.outlines;
|
||||
_quoteCache.icon = values.name;
|
||||
|
||||
auto text = TextWithEntities();
|
||||
auto unavailable = false;
|
||||
if (_sourcePeer && repostId) {
|
||||
const auto senderId = _sourcePeer->id;
|
||||
const auto of = owner->stories().lookup({ senderId, repostId });
|
||||
unavailable = !of && (of.error() == Data::NoStory::Deleted);
|
||||
if (of) {
|
||||
text = (*of)->caption();
|
||||
} else if (!unavailable) {
|
||||
const auto done = crl::guard(this, [=] {
|
||||
_maxWidth = 0;
|
||||
_controller->repaint();
|
||||
});
|
||||
owner->stories().resolve({ _sourcePeer->id, repostId }, done);
|
||||
}
|
||||
}
|
||||
|
||||
auto nameFull = TextWithEntities();
|
||||
nameFull.append(HistoryView::Reply::PeerEmoji(_sourcePeer));
|
||||
nameFull.append(name);
|
||||
auto context = Core::TextContext({
|
||||
.session = &_story->session(),
|
||||
.customEmojiLoopLimit = 1,
|
||||
});
|
||||
_name.setMarkedText(
|
||||
st::semiboldTextStyle,
|
||||
nameFull,
|
||||
Ui::NameTextOptions(),
|
||||
context);
|
||||
context.repaint = crl::guard(this, [=] {
|
||||
_controller->repaint();
|
||||
});
|
||||
_text.setMarkedText(
|
||||
st::defaultTextStyle,
|
||||
text,
|
||||
Ui::DialogTextOptions(),
|
||||
context);
|
||||
|
||||
const auto nameMaxWidth = _name.maxWidth();
|
||||
const auto optimalTextWidth = _text.isEmpty()
|
||||
? 0
|
||||
: std::min(_text.maxWidth(), st::maxSignatureSize);
|
||||
_maxWidth = std::max(nameMaxWidth, optimalTextWidth);
|
||||
_maxWidth = st::historyReplyPadding.left()
|
||||
+ _maxWidth
|
||||
+ st::historyReplyPadding.right();
|
||||
}
|
||||
|
||||
void RepostView::clickHandlerPressedChanged(
|
||||
const ClickHandlerPtr &action,
|
||||
bool pressed) {
|
||||
if (action == _link) {
|
||||
if (pressed) {
|
||||
const auto simple = _text.isEmpty();
|
||||
const auto skip = simple ? st::normalFont->height : 0;
|
||||
if (!_ripple) {
|
||||
const auto h = height() - skip;
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RoundRectMask(
|
||||
QSize(_lastWidth, h),
|
||||
(simple
|
||||
? st::storiesRepostSimpleStyle
|
||||
: st::messageQuoteStyle).radius),
|
||||
[=] { _controller->repaint(); });
|
||||
}
|
||||
_ripple->add(_lastPosition - QPoint(0, skip));
|
||||
} else if (_ripple) {
|
||||
_ripple->lastStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
class Story;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
struct RepostClickHandler;
|
||||
|
||||
class RepostView final
|
||||
: public base::has_weak_ptr
|
||||
, public ClickHandlerHost {
|
||||
public:
|
||||
RepostView(
|
||||
not_null<Controller*> controller,
|
||||
not_null<Data::Story*> story);
|
||||
~RepostView();
|
||||
|
||||
[[nodiscard]] int height() const;
|
||||
void draw(Painter &p, int x, int y, int availableWidth);
|
||||
[[nodiscard]] RepostClickHandler lookupHandler(QPoint position);
|
||||
|
||||
[[nodiscard]] PeerData *fromPeer() const;
|
||||
[[nodiscard]] QString fromName() const;
|
||||
|
||||
private:
|
||||
void recountDimensions();
|
||||
|
||||
void clickHandlerPressedChanged(
|
||||
const ClickHandlerPtr &action,
|
||||
bool pressed);
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
const not_null<Data::Story*> _story;
|
||||
PeerData *_sourcePeer = nullptr;
|
||||
ClickHandlerPtr _link;
|
||||
std::unique_ptr<Ui::RippleAnimation> _ripple;
|
||||
|
||||
Ui::Text::String _name;
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::QuotePaintCache _quoteCache;
|
||||
Ui::BackgroundEmojiData _backgroundEmojiData;
|
||||
Ui::ColorIndicesCompressed _colorIndices;
|
||||
QPoint _lastPosition;
|
||||
mutable int _lastWidth = 0;
|
||||
uint32 _maxWidth : 31 = 0;
|
||||
uint32 _loading : 1 = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
320
Telegram/SourceFiles/media/stories/media_stories_share.cpp
Normal file
320
Telegram/SourceFiles/media/stories/media_stories_share.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
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_share.h"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/random.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/business/data_shortcut_messages.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/data_thread.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item_helpers.h" // GetErrorForSending.
|
||||
#include "history/view/history_view_context_menu.h" // CopyStoryLink.
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
FullStoryId id,
|
||||
bool viewerStyle) {
|
||||
const auto session = &show->session();
|
||||
const auto resolve = [=] {
|
||||
const auto maybeStory = session->data().stories().lookup(id);
|
||||
return maybeStory ? maybeStory->get() : nullptr;
|
||||
};
|
||||
const auto story = resolve();
|
||||
if (!story) {
|
||||
return { nullptr };
|
||||
}
|
||||
const auto canCopyLink = story->hasDirectLink();
|
||||
const auto shareJustLink = (story->call() != nullptr);
|
||||
if (!canCopyLink && shareJustLink) {
|
||||
return { nullptr };
|
||||
}
|
||||
|
||||
auto copyCallback = [=] {
|
||||
const auto story = resolve();
|
||||
if (!story) {
|
||||
return;
|
||||
}
|
||||
if (story->hasDirectLink()) {
|
||||
using namespace HistoryView;
|
||||
CopyStoryLink(show, story->fullId());
|
||||
}
|
||||
};
|
||||
|
||||
struct State {
|
||||
int requests = 0;
|
||||
};
|
||||
const auto state = std::make_shared<State>();
|
||||
auto filterCallback = [=](not_null<Data::Thread*> thread) {
|
||||
if (const auto user = thread->peer()->asUser()) {
|
||||
if (user->canSendIgnoreMoneyRestrictions()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (shareJustLink) {
|
||||
return ::Data::CanSend(thread, ChatRestriction::SendOther);
|
||||
}
|
||||
return Data::CanSend(thread, ChatRestriction::SendPhotos)
|
||||
&& Data::CanSend(thread, ChatRestriction::SendVideos);
|
||||
};
|
||||
auto copyLinkCallback = canCopyLink
|
||||
? Fn<void()>(std::move(copyCallback))
|
||||
: Fn<void()>();
|
||||
auto countMessagesCallback = [=](const TextWithTags &comment) {
|
||||
return (shareJustLink || comment.text.isEmpty()) ? 1 : 2;
|
||||
};
|
||||
auto submitCallback = [=](
|
||||
std::vector<not_null<Data::Thread*>> &&result,
|
||||
Fn<bool()> checkPaid,
|
||||
TextWithTags &&comment,
|
||||
Api::SendOptions options,
|
||||
Data::ForwardOptions forwardOptions) {
|
||||
if (state->requests) {
|
||||
return; // Share clicked already.
|
||||
}
|
||||
const auto story = resolve();
|
||||
if (!story) {
|
||||
return;
|
||||
}
|
||||
const auto peer = story->peer();
|
||||
const auto error = GetErrorForSending(
|
||||
result,
|
||||
{ .story = shareJustLink ? nullptr : story, .text = &comment });
|
||||
if (error.error) {
|
||||
show->showBox(MakeSendErrorBox(error, result.size() > 1));
|
||||
return;
|
||||
} else if (!checkPaid()) {
|
||||
return;
|
||||
} else if (shareJustLink) {
|
||||
const auto url = session->api().exportDirectStoryLink(story);
|
||||
if (!comment.text.isEmpty()) {
|
||||
comment.text = url + "\n" + comment.text;
|
||||
const auto add = url.size() + 1;
|
||||
for (auto &tag : comment.tags) {
|
||||
tag.offset += add;
|
||||
}
|
||||
} else {
|
||||
comment.text = url;
|
||||
}
|
||||
auto &api = show->session().api();
|
||||
for (const auto thread : result) {
|
||||
auto message = Api::MessageToSend(
|
||||
Api::SendAction(thread, options));
|
||||
message.textWithTags = comment;
|
||||
message.action.clearDraft = false;
|
||||
api.sendMessage(std::move(message));
|
||||
}
|
||||
show->showToast(tr::lng_share_done(tr::now));
|
||||
show->hideLayer();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto api = &story->session().api();
|
||||
auto &histories = story->owner().histories();
|
||||
for (const auto thread : result) {
|
||||
const auto action = Api::SendAction(thread, options);
|
||||
if (!comment.text.isEmpty()) {
|
||||
auto message = Api::MessageToSend(action);
|
||||
message.textWithTags = comment;
|
||||
message.action.clearDraft = false;
|
||||
api->sendMessage(std::move(message));
|
||||
}
|
||||
const auto session = &thread->session();
|
||||
const auto threadPeer = thread->peer();
|
||||
const auto threadHistory = thread->owningHistory();
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
using SendFlag = MTPmessages_SendMedia::Flag;
|
||||
auto sendFlags = SendFlag(0) | SendFlag(0);
|
||||
if (action.replyTo) {
|
||||
sendFlags |= SendFlag::f_reply_to;
|
||||
}
|
||||
const auto silentPost = ShouldSendSilent(threadPeer, options);
|
||||
if (silentPost) {
|
||||
sendFlags |= SendFlag::f_silent;
|
||||
}
|
||||
if (options.scheduled) {
|
||||
sendFlags |= SendFlag::f_schedule_date;
|
||||
if (options.scheduleRepeatPeriod) {
|
||||
sendFlags |= SendFlag::f_schedule_repeat_period;
|
||||
}
|
||||
}
|
||||
if (options.shortcutId) {
|
||||
sendFlags |= SendFlag::f_quick_reply_shortcut;
|
||||
}
|
||||
if (options.effectId) {
|
||||
sendFlags |= SendFlag::f_effect;
|
||||
}
|
||||
if (options.suggest) {
|
||||
sendFlags |= SendFlag::f_suggested_post;
|
||||
}
|
||||
if (options.invertCaption) {
|
||||
sendFlags |= SendFlag::f_invert_media;
|
||||
}
|
||||
const auto starsPaid = std::min(
|
||||
threadHistory->peer->starsPerMessageChecked(),
|
||||
options.starsApproved);
|
||||
if (starsPaid) {
|
||||
options.starsApproved -= starsPaid;
|
||||
sendFlags |= SendFlag::f_allow_paid_stars;
|
||||
}
|
||||
const auto done = [=] {
|
||||
if (!--state->requests) {
|
||||
if (show->valid()) {
|
||||
show->showToast(tr::lng_share_done(tr::now));
|
||||
show->hideLayer();
|
||||
}
|
||||
}
|
||||
};
|
||||
histories.sendPreparedMessage(
|
||||
threadHistory,
|
||||
action.replyTo,
|
||||
randomId,
|
||||
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
|
||||
MTP_flags(sendFlags),
|
||||
threadPeer->input(),
|
||||
Data::Histories::ReplyToPlaceholder(),
|
||||
MTP_inputMediaStory(peer->input(), MTP_int(id.story)),
|
||||
MTPstring(),
|
||||
MTP_long(randomId),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>(),
|
||||
MTP_int(options.scheduled),
|
||||
MTP_int(options.scheduleRepeatPeriod),
|
||||
MTP_inputPeerEmpty(),
|
||||
Data::ShortcutIdToMTP(session, options.shortcutId),
|
||||
MTP_long(options.effectId),
|
||||
MTP_long(starsPaid),
|
||||
Api::SuggestToMTP(options.suggest)
|
||||
), [=](
|
||||
const MTPUpdates &result,
|
||||
const MTP::Response &response) {
|
||||
done();
|
||||
}, [=](
|
||||
const MTP::Error &error,
|
||||
const MTP::Response &response) {
|
||||
api->sendMessageFail(error, threadPeer, randomId);
|
||||
done();
|
||||
});
|
||||
++state->requests;
|
||||
}
|
||||
};
|
||||
const auto st = viewerStyle
|
||||
? ::Settings::DarkCreditsEntryBoxStyle()
|
||||
: ::Settings::CreditsEntryBoxStyleOverrides();
|
||||
return Box<ShareBox>(ShareBox::Descriptor{
|
||||
.session = session,
|
||||
.copyCallback = std::move(copyLinkCallback),
|
||||
.countMessagesCallback = std::move(countMessagesCallback),
|
||||
.submitCallback = std::move(submitCallback),
|
||||
.filterCallback = std::move(filterCallback),
|
||||
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
|
||||
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
|
||||
});
|
||||
}
|
||||
|
||||
QString FormatShareAtTime(TimeId seconds) {
|
||||
const auto minutes = seconds / 60;
|
||||
const auto h = minutes / 60;
|
||||
const auto m = minutes % 60;
|
||||
const auto s = seconds % 60;
|
||||
const auto zero = QChar('0');
|
||||
return h
|
||||
? u"%1:%2:%3"_q.arg(h).arg(m, 2, 10, zero).arg(s, 2, 10, zero)
|
||||
: u"%1:%2"_q.arg(m).arg(s, 2, 10, zero);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PrepareShareAtTimeBox(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<HistoryItem*> item,
|
||||
TimeId videoTimestamp) {
|
||||
const auto id = item->fullId();
|
||||
const auto history = item->history();
|
||||
const auto owner = &history->owner();
|
||||
const auto session = &history->session();
|
||||
const auto canCopyLink = item->hasDirectLink()
|
||||
&& history->peer->isBroadcast()
|
||||
&& history->peer->asBroadcast()->hasUsername();
|
||||
const auto hasCaptions = item->media()
|
||||
&& !item->originalText().text.isEmpty()
|
||||
&& item->media()->allowsEditCaption();
|
||||
const auto hasOnlyForcedForwardedInfo = !hasCaptions
|
||||
&& item->media()
|
||||
&& item->media()->forceForwardedInfo();
|
||||
|
||||
auto copyCallback = [=] {
|
||||
const auto item = owner->message(id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
CopyPostLink(
|
||||
show,
|
||||
item->fullId(),
|
||||
HistoryView::Context::History,
|
||||
videoTimestamp);
|
||||
};
|
||||
|
||||
const auto requiredRight = item->requiredSendRight();
|
||||
const auto requiresInline = item->requiresSendInlineRight();
|
||||
auto filterCallback = [=](not_null<Data::Thread*> thread) {
|
||||
if (const auto user = thread->peer()->asUser()) {
|
||||
if (user->canSendIgnoreMoneyRestrictions()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Data::CanSend(thread, requiredRight)
|
||||
&& (!requiresInline
|
||||
|| Data::CanSend(thread, ChatRestriction::SendInline));
|
||||
};
|
||||
auto copyLinkCallback = canCopyLink
|
||||
? Fn<void()>(std::move(copyCallback))
|
||||
: Fn<void()>();
|
||||
const auto st = ::Settings::DarkCreditsEntryBoxStyle();
|
||||
return Box<ShareBox>(ShareBox::Descriptor{
|
||||
.session = session,
|
||||
.copyCallback = std::move(copyLinkCallback),
|
||||
.countMessagesCallback = ShareBox::DefaultForwardCountMessages(
|
||||
history,
|
||||
{ id }),
|
||||
.submitCallback = ShareBox::DefaultForwardCallback(
|
||||
show,
|
||||
history,
|
||||
{ id },
|
||||
videoTimestamp),
|
||||
.filterCallback = std::move(filterCallback),
|
||||
.titleOverride = tr::lng_share_at_time_title(
|
||||
lt_time,
|
||||
rpl::single(FormatShareAtTime(videoTimestamp))),
|
||||
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
|
||||
.forwardOptions = {
|
||||
.sendersCount = ItemsForwardSendersCount({ item }),
|
||||
.captionsCount = ItemsForwardCaptionsCount({ item }),
|
||||
.show = !hasOnlyForcedForwardedInfo,
|
||||
},
|
||||
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
34
Telegram/SourceFiles/media/stories/media_stories_share.h
Normal file
34
Telegram/SourceFiles/media/stories/media_stories_share.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
FullStoryId id,
|
||||
bool viewerStyle = false);
|
||||
|
||||
[[nodiscard]] QString FormatShareAtTime(TimeId seconds);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareAtTimeBox(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<HistoryItem*> item,
|
||||
TimeId videoTimestamp);
|
||||
|
||||
} // namespace Media::Stories
|
||||
431
Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
Normal file
431
Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
Normal file
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
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_peer.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "media/stories/media_stories_view.h"
|
||||
#include "media/streaming/media_streaming_instance.h"
|
||||
#include "media/streaming/media_streaming_player.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kGoodFadeDuration = crl::time(200);
|
||||
constexpr auto kSiblingFade = 0.5;
|
||||
constexpr auto kSiblingFadeOver = 0.4;
|
||||
constexpr auto kSiblingNameOpacity = 0.8;
|
||||
constexpr auto kSiblingNameOpacityOver = 1.;
|
||||
constexpr auto kSiblingScaleOver = 0.05;
|
||||
|
||||
[[nodiscard]] StoryId LookupShownId(
|
||||
const Data::StoriesSource &source,
|
||||
StoryId suggestedId) {
|
||||
const auto i = suggestedId
|
||||
? source.ids.lower_bound(Data::StoryIdDates{ suggestedId })
|
||||
: end(source.ids);
|
||||
return (i != end(source.ids) && i->id == suggestedId)
|
||||
? suggestedId
|
||||
: source.toOpen().id;
|
||||
}
|
||||
|
||||
} // 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 createStreamedPlayer();
|
||||
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::on_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()
|
||||
&& !_video->goodThumbnailNoData()) {
|
||||
if (!_checkingGoodInCache) {
|
||||
waitForGoodThumbnail();
|
||||
}
|
||||
} else if (_failed) {
|
||||
return QImage();
|
||||
} else if (!_streamed) {
|
||||
createStreamedPlayer();
|
||||
} else if (_streamed->ready()) {
|
||||
return _streamed->info().video.cover;
|
||||
}
|
||||
return QImage();
|
||||
}
|
||||
|
||||
void Sibling::LoaderVideo::createStreamedPlayer() {
|
||||
_streamed = std::make_unique<Streaming::Instance>(
|
||||
_video,
|
||||
_origin,
|
||||
[] {}); // waitingCallback
|
||||
_streamed->lockPlayer();
|
||||
_streamed->player().updates(
|
||||
) | rpl::on_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->player().active()
|
||||
&& !_streamed->player().finished()) {
|
||||
_streamed->play({
|
||||
.mode = Streaming::Mode::Video,
|
||||
});
|
||||
_streamed->pause();
|
||||
}
|
||||
}
|
||||
|
||||
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::on_next([=] {
|
||||
strong->updateAfterGoodCheck();
|
||||
}, strong->_waitingGoodGeneration);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool Sibling::LoaderVideo::updateAfterGoodCheck() {
|
||||
if (!_video->goodThumbnailChecked()
|
||||
&& !_video->goodThumbnailNoData()) {
|
||||
return false;
|
||||
}
|
||||
_checkingGoodInCache = false;
|
||||
_waitingGoodGeneration.destroy();
|
||||
_update();
|
||||
return true;
|
||||
}
|
||||
|
||||
Sibling::Sibling(
|
||||
not_null<Controller*> controller,
|
||||
const Data::StoriesSource &source,
|
||||
StoryId suggestedId)
|
||||
: _controller(controller)
|
||||
, _id{ source.peer->id, LookupShownId(source, suggestedId) }
|
||||
, _peer(source.peer) {
|
||||
checkStory();
|
||||
_goodShown.stop();
|
||||
}
|
||||
|
||||
Sibling::~Sibling() = default;
|
||||
|
||||
void Sibling::checkStory() {
|
||||
const auto maybeStory = _peer->owner().stories().lookup(_id);
|
||||
if (!maybeStory) {
|
||||
if (_blurred.isNull()) {
|
||||
setBlackThumbnail();
|
||||
if (maybeStory.error() == Data::NoStory::Unknown) {
|
||||
_peer->owner().stories().resolve(_id, crl::guard(this, [=] {
|
||||
checkStory();
|
||||
}));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto story = *maybeStory;
|
||||
const auto origin = Data::FileOrigin();
|
||||
v::match(story->media().data, [&](not_null<PhotoData*> photo) {
|
||||
_loader = std::make_unique<LoaderPhoto>(photo, origin, [=] {
|
||||
check();
|
||||
});
|
||||
}, [&](not_null<DocumentData*> document) {
|
||||
_loader = std::make_unique<LoaderVideo>(document, origin, [=] {
|
||||
check();
|
||||
});
|
||||
}, [&](const std::shared_ptr<Data::GroupCall> &call) {
|
||||
_loader = nullptr;
|
||||
}, [&](v::null_t) {
|
||||
_loader = nullptr;
|
||||
});
|
||||
if (!_loader) {
|
||||
setBlackThumbnail();
|
||||
return;
|
||||
}
|
||||
_blurred = _loader->blurred();
|
||||
check();
|
||||
}
|
||||
|
||||
void Sibling::setBlackThumbnail() {
|
||||
_blurred = QImage(
|
||||
st::storiesMaxSize,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_blurred.fill(Qt::black);
|
||||
}
|
||||
|
||||
FullStoryId Sibling::shownId() const {
|
||||
return _id;
|
||||
}
|
||||
|
||||
not_null<PeerData*> Sibling::peer() const {
|
||||
return _peer;
|
||||
}
|
||||
|
||||
bool Sibling::shows(
|
||||
const Data::StoriesSource &source,
|
||||
StoryId suggestedId) const {
|
||||
const auto fullId = FullStoryId{
|
||||
source.peer->id,
|
||||
LookupShownId(source, suggestedId),
|
||||
};
|
||||
return (_id == fullId);
|
||||
}
|
||||
|
||||
SiblingView Sibling::view(const SiblingLayout &layout, float64 over) {
|
||||
const auto name = nameImage(layout);
|
||||
return {
|
||||
.image = _good.isNull() ? _blurred : _good,
|
||||
.layout = {
|
||||
.geometry = layout.geometry,
|
||||
.fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over,
|
||||
.radius = st::storiesRadius,
|
||||
},
|
||||
.userpic = userpicImage(layout),
|
||||
.userpicPosition = layout.userpic.topLeft(),
|
||||
.name = name,
|
||||
.namePosition = namePosition(layout, name),
|
||||
.nameOpacity = (kSiblingNameOpacity * (1 - over)
|
||||
+ kSiblingNameOpacityOver * over),
|
||||
.scale = 1. + (over * kSiblingScaleOver),
|
||||
};
|
||||
}
|
||||
|
||||
QImage Sibling::userpicImage(const SiblingLayout &layout) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = layout.userpic.width() * ratio;
|
||||
const auto key = _peer->userpicUniqueKey(_userpicView);
|
||||
if (_userpicImage.width() != size || _userpicKey != key) {
|
||||
_userpicKey = key;
|
||||
_userpicImage = PeerData::GenerateUserpicImage(
|
||||
_peer,
|
||||
_userpicView,
|
||||
size);
|
||||
_userpicImage.setDevicePixelRatio(ratio);
|
||||
}
|
||||
return _userpicImage;
|
||||
}
|
||||
|
||||
QImage Sibling::nameImage(const SiblingLayout &layout) {
|
||||
if (_nameFontSize != layout.nameFontSize) {
|
||||
_nameFontSize = layout.nameFontSize;
|
||||
|
||||
const auto family = 0; // Default font family.
|
||||
const auto font = style::font(
|
||||
_nameFontSize,
|
||||
style::FontFlag::Semibold,
|
||||
family);
|
||||
_name.reset();
|
||||
_nameStyle = std::make_unique<style::TextStyle>(style::TextStyle{
|
||||
.font = font,
|
||||
});
|
||||
};
|
||||
const auto text = _peer->isSelf()
|
||||
? tr::lng_stories_my_name(tr::now)
|
||||
: _peer->shortName();
|
||||
if (_nameText != text) {
|
||||
_name.reset();
|
||||
_nameText = text;
|
||||
}
|
||||
if (!_name) {
|
||||
_nameAvailableWidth = 0;
|
||||
_name.emplace(*_nameStyle, _nameText);
|
||||
}
|
||||
const auto available = layout.nameBoundingRect.width();
|
||||
const auto wasCut = (_nameAvailableWidth < _name->maxWidth());
|
||||
const auto nowCut = (available < _name->maxWidth());
|
||||
if (_nameImage.isNull()
|
||||
|| _nameAvailableWidth != layout.nameBoundingRect.width()) {
|
||||
_nameAvailableWidth = layout.nameBoundingRect.width();
|
||||
if (_nameImage.isNull() || nowCut || wasCut) {
|
||||
const auto w = std::min(_nameAvailableWidth, _name->maxWidth());
|
||||
const auto h = _nameStyle->font->height;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
_nameImage = QImage(
|
||||
QSize(w, h) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_nameImage.setDevicePixelRatio(ratio);
|
||||
_nameImage.fill(Qt::transparent);
|
||||
auto p = Painter(&_nameImage);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setFont(_nameStyle->font);
|
||||
p.setPen(Qt::white);
|
||||
_name->drawLeftElided(p, 0, 0, w, w);
|
||||
}
|
||||
}
|
||||
return _nameImage;
|
||||
}
|
||||
|
||||
QPoint Sibling::namePosition(
|
||||
const SiblingLayout &layout,
|
||||
const QImage &image) const {
|
||||
const auto size = image.size() / image.devicePixelRatio();
|
||||
const auto width = size.width();
|
||||
const auto bounding = layout.nameBoundingRect;
|
||||
const auto left = layout.geometry.x()
|
||||
+ (layout.geometry.width() - width) / 2;
|
||||
const auto top = bounding.y() + bounding.height() - size.height();
|
||||
if (left < bounding.x()) {
|
||||
return { bounding.x(), top };
|
||||
} else if (left + width > bounding.x() + bounding.width()) {
|
||||
return { bounding.x() + bounding.width() - width, top };
|
||||
}
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
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
|
||||
81
Telegram/SourceFiles/media/stories/media_stories_sibling.h
Normal file
81
Telegram/SourceFiles/media/stories/media_stories_sibling.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/userpic_view.h"
|
||||
|
||||
namespace style {
|
||||
struct TextStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
struct SiblingView;
|
||||
struct SiblingLayout;
|
||||
|
||||
class Sibling final : public base::has_weak_ptr {
|
||||
public:
|
||||
Sibling(
|
||||
not_null<Controller*> controller,
|
||||
const Data::StoriesSource &source,
|
||||
StoryId suggestedId);
|
||||
~Sibling();
|
||||
|
||||
[[nodiscard]] FullStoryId shownId() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] bool shows(
|
||||
const Data::StoriesSource &source,
|
||||
StoryId suggestedId) const;
|
||||
|
||||
[[nodiscard]] SiblingView view(
|
||||
const SiblingLayout &layout,
|
||||
float64 over);
|
||||
|
||||
private:
|
||||
class Loader;
|
||||
class LoaderPhoto;
|
||||
class LoaderVideo;
|
||||
|
||||
void checkStory();
|
||||
void check();
|
||||
|
||||
void setBlackThumbnail();
|
||||
[[nodiscard]] QImage userpicImage(const SiblingLayout &layout);
|
||||
[[nodiscard]] QImage nameImage(const SiblingLayout &layout);
|
||||
[[nodiscard]] QPoint namePosition(
|
||||
const SiblingLayout &layout,
|
||||
const QImage &image) const;
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
|
||||
FullStoryId _id;
|
||||
not_null<PeerData*> _peer;
|
||||
QImage _blurred;
|
||||
QImage _good;
|
||||
Ui::Animations::Simple _goodShown;
|
||||
|
||||
QImage _userpicImage;
|
||||
InMemoryKey _userpicKey = {};
|
||||
Ui::PeerUserpicView _userpicView;
|
||||
|
||||
QImage _nameImage;
|
||||
std::unique_ptr<style::TextStyle> _nameStyle;
|
||||
std::optional<Ui::Text::String> _name;
|
||||
QString _nameText;
|
||||
int _nameAvailableWidth = 0;
|
||||
int _nameFontSize = 0;
|
||||
|
||||
std::unique_ptr<Loader> _loader;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
157
Telegram/SourceFiles/media/stories/media_stories_slider.cpp
Normal file
157
Telegram/SourceFiles/media/stories/media_stories_slider.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
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_slider.h"
|
||||
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "media/view/media_view_playback_progress.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kOpacityInactive = 0.4;
|
||||
constexpr auto kOpacityActive = 1.;
|
||||
|
||||
} // namespace
|
||||
|
||||
Slider::Slider(not_null<Controller*> controller)
|
||||
: _controller(controller)
|
||||
, _progress(std::make_unique<View::PlaybackProgress>()) {
|
||||
}
|
||||
|
||||
Slider::~Slider() {
|
||||
}
|
||||
|
||||
void Slider::show(SliderData data) {
|
||||
resetProgress();
|
||||
data.total = std::max(data.total, 1);
|
||||
data.index = std::clamp(data.index, 0, data.total - 1);
|
||||
|
||||
if (_data == data) {
|
||||
return;
|
||||
}
|
||||
_data = data;
|
||||
|
||||
const auto parent = _controller->wrap();
|
||||
auto widget = std::make_unique<Ui::RpWidget>(parent);
|
||||
const auto raw = widget.get();
|
||||
|
||||
_rects.resize(_data.total);
|
||||
|
||||
raw->widthValue() | rpl::filter([=](int width) {
|
||||
return (width >= st::storiesSliderWidth);
|
||||
}) | rpl::on_next([=](int width) {
|
||||
layout(width);
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::filter([=] {
|
||||
return !_data.videoStream
|
||||
&& (raw->width() >= st::storiesSliderWidth);
|
||||
}) | rpl::on_next([=](QRect clip) {
|
||||
paint(QRectF(clip));
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->show();
|
||||
_widget = std::move(widget);
|
||||
|
||||
_progress->setValueChangedCallback([=](float64, float64) {
|
||||
_widget->update(_activeBoundingRect);
|
||||
});
|
||||
|
||||
_controller->layoutValue(
|
||||
) | rpl::on_next([=](const Layout &layout) {
|
||||
raw->setGeometry(layout.slider - st::storiesSliderMargin);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
|
||||
void Slider::raise() {
|
||||
if (_widget) {
|
||||
_widget->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::updatePlayback(const Player::TrackState &state) {
|
||||
_progress->updateState(state);
|
||||
}
|
||||
|
||||
void Slider::resetProgress() {
|
||||
_progress->updateState({});
|
||||
}
|
||||
|
||||
void Slider::layout(int width) {
|
||||
const auto single = st::storiesSliderWidth;
|
||||
const auto skip = st::storiesSliderSkip;
|
||||
// width == single * max + skip * (max - 1);
|
||||
// max == (width + skip) / (single + skip);
|
||||
const auto max = (width + skip) / (single + skip);
|
||||
Assert(max > 0);
|
||||
const auto count = std::clamp(_data.total, 1, max);
|
||||
const auto one = (width - (count - 1) * skip) / float64(count);
|
||||
auto left = 0.;
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
_rects[i] = QRectF(left, 0, one, single);
|
||||
if (i == _data.index) {
|
||||
const auto from = int(std::floor(left));
|
||||
const auto size = int(std::ceil(left + one)) - from;
|
||||
_activeBoundingRect = QRect(from, 0, size, single);
|
||||
}
|
||||
left += one + skip;
|
||||
}
|
||||
for (auto i = count; i != _rects.size(); ++i) {
|
||||
_rects[i] = QRectF();
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::paint(QRectF clip) {
|
||||
auto p = QPainter(_widget.get());
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setBrush(st::mediaviewControlFg);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto radius = st::storiesSliderWidth / 2.;
|
||||
for (auto i = 0; i != int(_rects.size()); ++i) {
|
||||
if (_rects[i].isEmpty()) {
|
||||
break;
|
||||
} else if (!_rects[i].intersects(clip)) {
|
||||
continue;
|
||||
} else if (i == _data.index) {
|
||||
const auto progress = _progress->value();
|
||||
const auto full = _rects[i].width();
|
||||
const auto height = _rects[i].height();
|
||||
const auto min = height;
|
||||
const auto activeWidth = std::max(full * progress, min);
|
||||
const auto inactiveWidth = full - activeWidth + min;
|
||||
const auto activeLeft = _rects[i].left();
|
||||
const auto inactiveLeft = activeLeft + activeWidth - min;
|
||||
p.setOpacity(kOpacityInactive);
|
||||
p.drawRoundedRect(
|
||||
QRectF(inactiveLeft, 0, inactiveWidth, height),
|
||||
radius,
|
||||
radius);
|
||||
if (activeWidth > 0.) {
|
||||
p.setOpacity(kOpacityActive);
|
||||
p.drawRoundedRect(
|
||||
QRectF(activeLeft, 0, activeWidth, height),
|
||||
radius,
|
||||
radius);
|
||||
}
|
||||
} else {
|
||||
p.setOpacity((i < _data.index)
|
||||
? kOpacityActive
|
||||
: kOpacityInactive);
|
||||
p.drawRoundedRect(_rects[i], radius, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
63
Telegram/SourceFiles/media/stories/media_stories_slider.h
Normal file
63
Telegram/SourceFiles/media/stories/media_stories_slider.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::View {
|
||||
class PlaybackProgress;
|
||||
} // namespace Media::View
|
||||
|
||||
namespace Media::Player {
|
||||
struct TrackState;
|
||||
} // namespace Media::Player
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Controller;
|
||||
|
||||
struct SliderData {
|
||||
int index = 0;
|
||||
int total = 0;
|
||||
bool videoStream = false;
|
||||
|
||||
friend inline auto operator<=>(SliderData, SliderData) = default;
|
||||
friend inline bool operator==(SliderData, SliderData) = default;
|
||||
};
|
||||
|
||||
class Slider final {
|
||||
public:
|
||||
explicit Slider(not_null<Controller*> controller);
|
||||
~Slider();
|
||||
|
||||
void show(SliderData data);
|
||||
void raise();
|
||||
|
||||
void updatePlayback(const Player::TrackState &state);
|
||||
|
||||
private:
|
||||
void resetProgress();
|
||||
|
||||
void layout(int width);
|
||||
void paint(QRectF clip);
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
const std::unique_ptr<Media::View::PlaybackProgress> _progress;
|
||||
|
||||
std::unique_ptr<Ui::RpWidget> _widget;
|
||||
std::vector<QRectF> _rects;
|
||||
QRect _activeBoundingRect;
|
||||
|
||||
SliderData _data;
|
||||
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
423
Telegram/SourceFiles/media/stories/media_stories_stealth.cpp
Normal file
423
Telegram/SourceFiles/media/stories/media_stories_stealth.cpp
Normal file
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
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_stealth.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "ui/controls/feature_list.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_media_stories.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kAlreadyToastDuration = 4 * crl::time(1000);
|
||||
constexpr auto kCooldownButtonLabelOpacity = 0.5;
|
||||
|
||||
struct State {
|
||||
Data::StealthMode mode;
|
||||
TimeId now = 0;
|
||||
bool premium = false;
|
||||
bool hasCallback = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] Ui::Toast::Config ToastAlready(TimeId left) {
|
||||
return {
|
||||
.title = tr::lng_stealth_mode_already_title(tr::now),
|
||||
.text = tr::lng_stealth_mode_already_about(
|
||||
tr::now,
|
||||
lt_left,
|
||||
TextWithEntities{ TimeLeftText(left) },
|
||||
tr::rich),
|
||||
.st = &st::storiesStealthToast,
|
||||
.adaptive = true,
|
||||
.duration = kAlreadyToastDuration,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::Toast::Config ToastActivated() {
|
||||
return {
|
||||
.title = tr::lng_stealth_mode_enabled_tip_title(tr::now),
|
||||
.text = tr::lng_stealth_mode_enabled_tip(
|
||||
tr::now,
|
||||
tr::rich),
|
||||
.st = &st::storiesStealthToast,
|
||||
.adaptive = true,
|
||||
.duration = kAlreadyToastDuration,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::Toast::Config ToastCooldown() {
|
||||
return {
|
||||
.text = tr::lng_stealth_mode_cooldown_tip(
|
||||
tr::now,
|
||||
tr::rich),
|
||||
.st = &st::storiesStealthToast,
|
||||
.adaptive = true,
|
||||
.duration = kAlreadyToastDuration,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<State> StateValue(
|
||||
not_null<Main::Session*> session,
|
||||
bool hasCallback = false) {
|
||||
return rpl::combine(
|
||||
session->data().stories().stealthModeValue(),
|
||||
Data::AmPremiumValue(session)
|
||||
) | rpl::map([=](Data::StealthMode mode, bool premium) {
|
||||
return rpl::make_producer<State>([=](auto consumer) {
|
||||
struct Info {
|
||||
base::Timer timer;
|
||||
bool firstSent = false;
|
||||
bool enabledSent = false;
|
||||
bool cooldownSent = false;
|
||||
};
|
||||
auto lifetime = rpl::lifetime();
|
||||
const auto info = lifetime.make_state<Info>();
|
||||
const auto check = [=] {
|
||||
auto send = !info->firstSent;
|
||||
const auto now = base::unixtime::now();
|
||||
const auto left1 = (mode.enabledTill - now);
|
||||
const auto left2 = (mode.cooldownTill - now);
|
||||
info->firstSent = true;
|
||||
if (!info->enabledSent && left1 <= 0) {
|
||||
send = true;
|
||||
info->enabledSent = true;
|
||||
}
|
||||
if (!info->cooldownSent && left2 <= 0) {
|
||||
send = true;
|
||||
info->cooldownSent = true;
|
||||
}
|
||||
const auto left = (left1 <= 0)
|
||||
? left2
|
||||
: (left2 <= 0)
|
||||
? left1
|
||||
: std::min(left1, left2);
|
||||
if (left > 0) {
|
||||
info->timer.callOnce(left * crl::time(1000));
|
||||
}
|
||||
if (send) {
|
||||
consumer.put_next(
|
||||
State{ mode, now, premium, hasCallback });
|
||||
}
|
||||
if (left <= 0) {
|
||||
consumer.put_done();
|
||||
}
|
||||
};
|
||||
info->timer.setCallback(check);
|
||||
check();
|
||||
return lifetime;
|
||||
});
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::FeatureListEntry FeaturePast(
|
||||
const style::StealthBoxStyle &st) {
|
||||
return {
|
||||
.icon = st.featurePastIcon,
|
||||
.title = tr::lng_stealth_mode_past_title(tr::now),
|
||||
.about = { tr::lng_stealth_mode_past_about(tr::now) },
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::FeatureListEntry FeatureNext(
|
||||
const style::StealthBoxStyle &st) {
|
||||
return {
|
||||
.icon = st.featureNextIcon,
|
||||
.title = tr::lng_stealth_mode_next_title(tr::now),
|
||||
.about = { tr::lng_stealth_mode_next_about(tr::now) },
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeLogo(
|
||||
QWidget *parent,
|
||||
const style::StealthBoxStyle &st) {
|
||||
const auto add = st::storiesStealthLogoAdd;
|
||||
const auto icon = &st.logoIcon;
|
||||
const auto size = QSize(2 * add, 2 * add) + icon->size();
|
||||
auto result = object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(
|
||||
parent,
|
||||
object_ptr<Ui::RpWidget>(parent),
|
||||
st::storiesStealthLogoMargin);
|
||||
const auto inner = result->entity();
|
||||
inner->resize(size);
|
||||
inner->paintRequest(
|
||||
) | rpl::on_next([=, &st] {
|
||||
auto p = QPainter(inner);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(st.logoBg);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto left = (inner->width() - size.width()) / 2;
|
||||
const auto top = (inner->height() - size.height()) / 2;
|
||||
const auto rect = QRect(QPoint(left, top), size);
|
||||
p.drawEllipse(rect);
|
||||
icon->paintInCenter(p, rect);
|
||||
}, inner->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
|
||||
QWidget *parent,
|
||||
const style::StealthBoxStyle &st) {
|
||||
return object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
parent,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
tr::lng_stealth_mode_title(tr::now),
|
||||
st.box.title),
|
||||
st::storiesStealthTitleMargin);
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeAbout(
|
||||
QWidget *parent,
|
||||
rpl::producer<State> state,
|
||||
const style::StealthBoxStyle &st) {
|
||||
auto text = std::move(state) | rpl::map([](const State &state) {
|
||||
return state.premium
|
||||
? tr::lng_stealth_mode_about(tr::now)
|
||||
: tr::lng_stealth_mode_unlock_about(tr::now);
|
||||
});
|
||||
return object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
parent,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st.about),
|
||||
st::storiesStealthAboutMargin);
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RoundButton> MakeButton(
|
||||
QWidget *parent,
|
||||
rpl::producer<State> state,
|
||||
const style::StealthBoxStyle &st) {
|
||||
auto text = rpl::duplicate(state) | rpl::map([](const State &state) {
|
||||
if (!state.premium) {
|
||||
return tr::lng_stealth_mode_unlock();
|
||||
} else if (state.mode.cooldownTill <= state.now) {
|
||||
return state.hasCallback
|
||||
? tr::lng_stealth_mode_enable_and_open()
|
||||
: tr::lng_stealth_mode_enable();
|
||||
}
|
||||
return rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
base::timer_each(250)
|
||||
) | rpl::map([=] {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto left = std::max(state.mode.cooldownTill - now, 1);
|
||||
return tr::lng_stealth_mode_cooldown_in(
|
||||
tr::now,
|
||||
lt_left,
|
||||
TimeLeftText(left));
|
||||
}) | rpl::type_erased;
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
auto result = object_ptr<Ui::RoundButton>(
|
||||
parent,
|
||||
rpl::single(QString()),
|
||||
st.box.button);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
std::move(text),
|
||||
st.buttonLabel);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->show();
|
||||
|
||||
const auto lock = Ui::CreateChild<Ui::RpWidget>(raw);
|
||||
lock->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
lock->resize(st.lockIcon.size());
|
||||
lock->paintRequest(
|
||||
) | rpl::on_next([=, &st] {
|
||||
auto p = QPainter(lock);
|
||||
st.lockIcon.paintInCenter(p, lock->rect());
|
||||
}, lock->lifetime());
|
||||
|
||||
const auto lockLeft = -st.buttonLabel.style.font->height;
|
||||
const auto updateLabelLockGeometry = [=, &st] {
|
||||
const auto outer = raw->width();
|
||||
const auto added = -st.box.button.width;
|
||||
const auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width());
|
||||
const auto width = outer - added - skip;
|
||||
const auto top = st.box.button.textTop;
|
||||
label->resizeToWidth(width);
|
||||
label->move(added / 2, top);
|
||||
const auto inner = std::min(label->textMaxWidth(), width);
|
||||
const auto right = (added / 2) + (outer - inner) / 2 + inner;
|
||||
const auto lockTop = (label->height() - lock->height()) / 2;
|
||||
lock->move(right + lockLeft, top + lockTop);
|
||||
};
|
||||
|
||||
std::move(state) | rpl::on_next([=](const State &state) {
|
||||
const auto cooldown = state.premium
|
||||
&& (state.mode.cooldownTill > state.now);
|
||||
label->setOpacity(cooldown ? kCooldownButtonLabelOpacity : 1.);
|
||||
lock->setVisible(!state.premium);
|
||||
updateLabelLockGeometry();
|
||||
}, label->lifetime());
|
||||
|
||||
raw->widthValue(
|
||||
) | rpl::on_next(updateLabelLockGeometry, label->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> StealthModeBox(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
Fn<void()> onActivated,
|
||||
const style::StealthBoxStyle &st) {
|
||||
return Box([=](not_null<Ui::GenericBox*> box) {
|
||||
struct Data {
|
||||
rpl::variable<State> state;
|
||||
bool requested = false;
|
||||
};
|
||||
const auto data = box->lifetime().make_state<Data>();
|
||||
data->state = StateValue(&show->session(), onActivated != nullptr);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st.box);
|
||||
box->addRow(MakeLogo(box, st));
|
||||
box->addRow(MakeTitle(box, st), style::al_top);
|
||||
box->addRow(MakeAbout(box, data->state.value(), st), style::al_top);
|
||||
const auto make = [&](const Ui::FeatureListEntry &entry) {
|
||||
return Ui::MakeFeatureListEntry(
|
||||
box,
|
||||
entry,
|
||||
{},
|
||||
st.featureTitle,
|
||||
st.featureAbout);
|
||||
};
|
||||
box->addRow(make(FeaturePast(st)));
|
||||
box->addRow(
|
||||
make(FeatureNext(st)),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, 0, 0, st::storiesStealthBoxBottom)));
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st.boxClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
const auto button = box->addButton(
|
||||
MakeButton(box, data->state.value(), st));
|
||||
button->resizeToWidth(st::boxWideWidth
|
||||
- st.box.buttonPadding.left()
|
||||
- st.box.buttonPadding.right());
|
||||
button->setClickedCallback([=] {
|
||||
const auto now = data->state.current();
|
||||
if (now.mode.enabledTill > now.now) {
|
||||
show->showToast(ToastActivated());
|
||||
box->closeBox();
|
||||
} else if (!now.premium) {
|
||||
data->requested = false;
|
||||
if (const auto window = show->resolveWindow()) {
|
||||
ShowPremiumPreviewBox(window, PremiumFeature::Stories);
|
||||
window->window().activate();
|
||||
}
|
||||
} else if (now.mode.cooldownTill > now.now) {
|
||||
show->showToast(ToastCooldown());
|
||||
box->closeBox();
|
||||
} else if (!data->requested) {
|
||||
data->requested = true;
|
||||
show->session().data().stories().activateStealthMode(
|
||||
crl::guard(box, [=] { data->requested = false; }));
|
||||
}
|
||||
});
|
||||
data->state.value() | rpl::filter([](const State &state) {
|
||||
return state.mode.enabledTill > state.now;
|
||||
}) | rpl::on_next([=] {
|
||||
box->closeBox();
|
||||
show->showToast(ToastActivated());
|
||||
if (onActivated) {
|
||||
onActivated();
|
||||
}
|
||||
}, box->lifetime());
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SetupStealthMode(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
StealthModeDescriptor descriptor) {
|
||||
const auto onActivated = descriptor.onActivated;
|
||||
const auto st = descriptor.st;
|
||||
const auto now = base::unixtime::now();
|
||||
const auto mode = show->session().data().stories().stealthMode();
|
||||
if (const auto left = mode.enabledTill - now; left > 0) {
|
||||
show->showToast(ToastAlready(left));
|
||||
if (onActivated) {
|
||||
onActivated();
|
||||
}
|
||||
} else {
|
||||
const auto &style = st ? *st : st::storiesStealthStyle;
|
||||
show->show(StealthModeBox(show, onActivated, style));
|
||||
}
|
||||
}
|
||||
|
||||
void AddStealthModeMenu(
|
||||
const Ui::Menu::MenuCallback &add,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<Window::SessionController*> controller) {
|
||||
if (!peer->session().premiumPossible() || !peer->isUser()) {
|
||||
return;
|
||||
}
|
||||
const auto now = base::unixtime::now();
|
||||
const auto stealth = peer->owner().stories().stealthMode();
|
||||
add(
|
||||
tr::lng_stories_view_anonymously(tr::now),
|
||||
[=] {
|
||||
SetupStealthMode(
|
||||
controller->uiShow(),
|
||||
StealthModeDescriptor{
|
||||
[=] { controller->openPeerStories(peer->id); },
|
||||
&st::storiesStealthStyleDefault,
|
||||
});
|
||||
},
|
||||
((peer->session().premium() || (stealth.enabledTill > now))
|
||||
? &st::menuIconStealth
|
||||
: &st::menuIconStealthLocked));
|
||||
}
|
||||
|
||||
QString TimeLeftText(int left) {
|
||||
Expects(left >= 0);
|
||||
|
||||
const auto hours = left / 3600;
|
||||
const auto minutes = (left % 3600) / 60;
|
||||
const auto seconds = left % 60;
|
||||
const auto zero = QChar('0');
|
||||
if (hours) {
|
||||
return u"%1:%2:%3"_q
|
||||
.arg(hours)
|
||||
.arg(minutes, 2, 10, zero)
|
||||
.arg(seconds, 2, 10, zero);
|
||||
} else if (minutes) {
|
||||
return u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, zero);
|
||||
}
|
||||
return u"0:%1"_q.arg(left, 2, 10, zero);
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
44
Telegram/SourceFiles/media/stories/media_stories_stealth.h
Normal file
44
Telegram/SourceFiles/media/stories/media_stories_stealth.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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
|
||||
|
||||
namespace style {
|
||||
struct StealthBoxStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui::Menu {
|
||||
struct MenuCallback;
|
||||
} // namespace Ui::Menu
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
struct StealthModeDescriptor {
|
||||
Fn<void()> onActivated = nullptr;
|
||||
const style::StealthBoxStyle *st = nullptr;
|
||||
};
|
||||
|
||||
void SetupStealthMode(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
StealthModeDescriptor descriptor = {});
|
||||
|
||||
void AddStealthModeMenu(
|
||||
const Ui::Menu::MenuCallback &add,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
[[nodiscard]] QString TimeLeftText(int left);
|
||||
|
||||
} // namespace Media::Stories
|
||||
194
Telegram/SourceFiles/media/stories/media_stories_view.cpp
Normal file
194
Telegram/SourceFiles/media/stories/media_stories_view.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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_view.h"
|
||||
|
||||
#include "data/data_file_origin.h"
|
||||
#include "history/view/controls/compose_controls_common.h"
|
||||
#include "media/stories/media_stories_controller.h"
|
||||
#include "media/stories/media_stories_delegate.h"
|
||||
#include "media/stories/media_stories_header.h"
|
||||
#include "media/stories/media_stories_slider.h"
|
||||
#include "media/stories/media_stories_reply.h"
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
View::View(not_null<Delegate*> delegate)
|
||||
: _controller(std::make_unique<Controller>(delegate)) {
|
||||
}
|
||||
|
||||
View::~View() = default;
|
||||
|
||||
void View::show(
|
||||
not_null<Data::Story*> story,
|
||||
Data::StoriesContext context) {
|
||||
_controller->show(story, context);
|
||||
}
|
||||
|
||||
void View::ready() {
|
||||
_controller->ready();
|
||||
}
|
||||
|
||||
Data::Story *View::story() const {
|
||||
return _controller->story();
|
||||
}
|
||||
|
||||
QRect View::finalShownGeometry() const {
|
||||
return _controller->layout().content;
|
||||
}
|
||||
|
||||
rpl::producer<QRect> View::finalShownGeometryValue() const {
|
||||
return _controller->layoutValue(
|
||||
) | rpl::map([=](const Layout &layout) {
|
||||
return layout.content;
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
ContentLayout View::contentLayout() const {
|
||||
return _controller->contentLayout();
|
||||
}
|
||||
|
||||
bool View::closeByClickAt(QPoint position) const {
|
||||
return _controller->closeByClickAt(position);
|
||||
}
|
||||
|
||||
void View::updatePlayback(const Player::TrackState &state) {
|
||||
_controller->updateVideoPlayback(state);
|
||||
}
|
||||
|
||||
ClickHandlerPtr View::lookupAreaHandler(QPoint point) const {
|
||||
return _controller->lookupAreaHandler(point);
|
||||
}
|
||||
|
||||
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 {
|
||||
return _controller->jumpFor(delta);
|
||||
}
|
||||
|
||||
bool View::paused() const {
|
||||
return _controller->paused();
|
||||
}
|
||||
|
||||
void View::togglePaused(bool paused) {
|
||||
_controller->togglePaused(paused);
|
||||
}
|
||||
|
||||
void View::contentPressed(bool pressed) {
|
||||
_controller->contentPressed(pressed);
|
||||
}
|
||||
|
||||
void View::menuShown(bool shown) {
|
||||
_controller->setMenuShown(shown);
|
||||
}
|
||||
|
||||
void View::shareRequested() {
|
||||
_controller->shareRequested();
|
||||
}
|
||||
|
||||
void View::deleteRequested() {
|
||||
_controller->deleteRequested();
|
||||
}
|
||||
|
||||
void View::reportRequested() {
|
||||
_controller->reportRequested();
|
||||
}
|
||||
|
||||
void View::toggleInProfileRequested(bool inProfile) {
|
||||
_controller->toggleInProfileRequested(inProfile);
|
||||
}
|
||||
|
||||
bool View::ignoreWindowMove(QPoint position) const {
|
||||
return _controller->ignoreWindowMove(position);
|
||||
}
|
||||
|
||||
void View::tryProcessKeyInput(not_null<QKeyEvent*> e) {
|
||||
_controller->tryProcessKeyInput(e);
|
||||
}
|
||||
|
||||
bool View::allowStealthMode() const {
|
||||
return _controller->allowStealthMode();
|
||||
}
|
||||
|
||||
void View::setupStealthMode() {
|
||||
_controller->setupStealthMode();
|
||||
}
|
||||
|
||||
auto View::attachReactionsToMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
QPoint desiredPosition)
|
||||
-> AttachStripResult {
|
||||
return _controller->attachReactionsToMenu(menu, desiredPosition);
|
||||
}
|
||||
|
||||
SiblingView View::sibling(SiblingType type) const {
|
||||
return _controller->sibling(type);
|
||||
}
|
||||
|
||||
Data::FileOrigin View::fileOrigin() const {
|
||||
return _controller->fileOrigin();
|
||||
}
|
||||
|
||||
TextWithEntities View::captionText() const {
|
||||
return _controller->captionText();
|
||||
}
|
||||
|
||||
bool View::skipCaption() const {
|
||||
return _controller->skipCaption();
|
||||
}
|
||||
|
||||
bool View::repost() const {
|
||||
return _controller->repost();
|
||||
}
|
||||
|
||||
QMargins View::repostCaptionPadding() const {
|
||||
return _controller->repostCaptionPadding();
|
||||
}
|
||||
|
||||
void View::drawRepostInfo(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth) const {
|
||||
_controller->drawRepostInfo(p, x, y, availableWidth);
|
||||
}
|
||||
|
||||
RepostClickHandler View::lookupRepostHandler(QPoint position) const {
|
||||
return _controller->lookupRepostHandler(position);
|
||||
}
|
||||
|
||||
void View::showFullCaption() {
|
||||
_controller->showFullCaption();
|
||||
}
|
||||
|
||||
std::shared_ptr<ChatHelpers::Show> View::uiShow() const {
|
||||
return _controller->uiShow();
|
||||
}
|
||||
|
||||
|
||||
void View::updateVideoStream(not_null<Calls::GroupCall*> videoStream) {
|
||||
_controller->updateVideoStream(videoStream);
|
||||
}
|
||||
|
||||
rpl::producer<bool> View::commentsShownValue() const {
|
||||
return _controller->commentsStateValue(
|
||||
) | rpl::map([=](CommentsState state) {
|
||||
return (state == CommentsState::Shown);
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
rpl::lifetime &View::lifetime() {
|
||||
return _controller->lifetime();
|
||||
}
|
||||
|
||||
} // namespace Media::Stories
|
||||
148
Telegram/SourceFiles/media/stories/media_stories_view.h
Normal file
148
Telegram/SourceFiles/media/stories/media_stories_view.h
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class ClickHandlerHost;
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
class Story;
|
||||
struct StoriesContext;
|
||||
struct FileOrigin;
|
||||
} // namespace Data
|
||||
|
||||
namespace Media::Player {
|
||||
struct TrackState;
|
||||
} // namespace Media::Player
|
||||
|
||||
namespace HistoryView::Reactions {
|
||||
enum class AttachSelectorResult;
|
||||
} // namespace HistoryView::Reactions
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media::Stories {
|
||||
|
||||
class Delegate;
|
||||
class Controller;
|
||||
|
||||
struct ContentLayout {
|
||||
QRect geometry;
|
||||
float64 fade = 0.;
|
||||
float64 scale = 1.;
|
||||
int radius = 0;
|
||||
bool headerOutside = false;
|
||||
};
|
||||
|
||||
enum class SiblingType;
|
||||
|
||||
struct SiblingView {
|
||||
QImage image;
|
||||
ContentLayout layout;
|
||||
QImage userpic;
|
||||
QPoint userpicPosition;
|
||||
QImage name;
|
||||
QPoint namePosition;
|
||||
float64 nameOpacity = 0.;
|
||||
float64 scale = 1.;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
return !image.isNull();
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
};
|
||||
|
||||
struct RepostClickHandler {
|
||||
ClickHandlerPtr link;
|
||||
ClickHandlerHost *host = nullptr;
|
||||
|
||||
explicit operator bool() const {
|
||||
return link && host;
|
||||
}
|
||||
};
|
||||
|
||||
inline constexpr auto kCollapsedCaptionLines = 2;
|
||||
inline constexpr auto kMaxShownCaptionLines = 4;
|
||||
|
||||
class View final {
|
||||
public:
|
||||
explicit View(not_null<Delegate*> delegate);
|
||||
~View();
|
||||
|
||||
void show(not_null<Data::Story*> story, Data::StoriesContext context);
|
||||
void ready();
|
||||
|
||||
[[nodiscard]] Data::Story *story() const;
|
||||
[[nodiscard]] QRect finalShownGeometry() const;
|
||||
[[nodiscard]] rpl::producer<QRect> finalShownGeometryValue() const;
|
||||
[[nodiscard]] ContentLayout contentLayout() const;
|
||||
[[nodiscard]] bool closeByClickAt(QPoint position) const;
|
||||
[[nodiscard]] SiblingView sibling(SiblingType type) const;
|
||||
[[nodiscard]] Data::FileOrigin fileOrigin() const;
|
||||
[[nodiscard]] TextWithEntities captionText() const;
|
||||
[[nodiscard]] bool skipCaption() const;
|
||||
[[nodiscard]] bool repost() const;
|
||||
void showFullCaption();
|
||||
|
||||
[[nodiscard]] QMargins repostCaptionPadding() const;
|
||||
void drawRepostInfo(Painter &p, int x, int y, int availableWidth) const;
|
||||
[[nodiscard]] RepostClickHandler lookupRepostHandler(
|
||||
QPoint position) const;
|
||||
|
||||
void updatePlayback(const Player::TrackState &state);
|
||||
[[nodiscard]] ClickHandlerPtr lookupAreaHandler(QPoint point) 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);
|
||||
void contentPressed(bool pressed);
|
||||
void menuShown(bool shown);
|
||||
|
||||
void shareRequested();
|
||||
void deleteRequested();
|
||||
void reportRequested();
|
||||
void toggleInProfileRequested(bool inProfile);
|
||||
|
||||
[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
|
||||
void tryProcessKeyInput(not_null<QKeyEvent*> e);
|
||||
|
||||
[[nodiscard]] bool allowStealthMode() const;
|
||||
void setupStealthMode();
|
||||
|
||||
using AttachStripResult = HistoryView::Reactions::AttachSelectorResult;
|
||||
[[nodiscard]] AttachStripResult attachReactionsToMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
QPoint desiredPosition);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
|
||||
|
||||
void updateVideoStream(not_null<Calls::GroupCall*> videoStream);
|
||||
[[nodiscard]] rpl::producer<bool> commentsShownValue() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Controller> _controller;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Stories
|
||||
Reference in New Issue
Block a user