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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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

View File

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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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 &quoteSt = 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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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