Implement simple UI for single-type likes in stories.

This commit is contained in:
John Preston 2023-08-02 21:41:58 +02:00
parent 3adb0c1856
commit 4bd925ac2c
27 changed files with 235 additions and 64 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -272,6 +272,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins."; "lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins.";
"lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins."; "lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins.";
"lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins."; "lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins.";
"lng_error_nocopy_story" = "Sorry, copying of this story is disabled by the author.";
"lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?"; "lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?";
"lng_sure_add_admin_invite_channel" = "This user is not a subscriber of this channel. Add them to the channel and promote them to admin?"; "lng_sure_add_admin_invite_channel" = "This user is not a subscriber of this channel. Add them to the channel and promote them to admin?";
"lng_sure_add_admin_unremove" = "This user is currently restricted or removed. Are you sure you want to promote them?"; "lng_sure_add_admin_unremove" = "This user is currently restricted or removed. Are you sure you want to promote them?";
@ -3876,6 +3877,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stories_archive_done" = "This story is hidden from your profile."; "lng_stories_archive_done" = "This story is hidden from your profile.";
"lng_stories_archive_done_many#one" = "{count} story is hidden from your profile."; "lng_stories_archive_done_many#one" = "{count} story is hidden from your profile.";
"lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile."; "lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile.";
"lng_stories_save_promo" = "Subscribe to {link} to download other people's unprotected stories to disk.";
"lng_stealth_mode_menu_item" = "Stealth Mode"; "lng_stealth_mode_menu_item" = "Stealth Mode";
"lng_stealth_mode_title" = "Stealth Mode"; "lng_stealth_mode_title" = "Stealth Mode";

View file

@ -196,6 +196,8 @@ ComposeControls {
send: SendButton; send: SendButton;
attach: IconButton; attach: IconButton;
emoji: EmojiButton; emoji: EmojiButton;
like: IconButton;
liked: icon;
suggestions: EmojiSuggestions; suggestions: EmojiSuggestions;
tabbed: EmojiPan; tabbed: EmojiPan;
tabbedHeightMin: pixels; tabbedHeightMin: pixels;

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers { namespace ChatHelpers {
struct ComposeFeatures { struct ComposeFeatures {
bool likes = false;
bool sendAs = true; bool sendAs = true;
bool ttlInfo = true; bool ttlInfo = true;
bool botCommandSend = true; bool botCommandSend = true;

View file

@ -276,8 +276,13 @@ bool Story::edited() const {
return _edited; return _edited;
} }
bool Story::canDownload() const { bool Story::canDownloadIfPremium() const {
return /*!forbidsForward() || */_peer->isSelf(); return !forbidsForward() || _peer->isSelf();
}
bool Story::canDownloadChecked() const {
return _peer->isSelf()
|| (canDownloadIfPremium() && _peer->session().premium());
} }
bool Story::canShare() const { bool Story::canShare() const {

View file

@ -100,7 +100,8 @@ public:
[[nodiscard]] bool forbidsForward() const; [[nodiscard]] bool forbidsForward() const;
[[nodiscard]] bool edited() const; [[nodiscard]] bool edited() const;
[[nodiscard]] bool canDownload() const; [[nodiscard]] bool canDownloadIfPremium() const;
[[nodiscard]] bool canDownloadChecked() const;
[[nodiscard]] bool canShare() const; [[nodiscard]] bool canShare() const;
[[nodiscard]] bool canDelete() const; [[nodiscard]] bool canDelete() const;
[[nodiscard]] bool canReport() const; [[nodiscard]] bool canReport() const;

View file

@ -41,6 +41,7 @@ struct SetHistoryArgs {
Fn<Api::SendAction()> sendActionFactory; Fn<Api::SendAction()> sendActionFactory;
rpl::producer<int> slowmodeSecondsLeft; rpl::producer<int> slowmodeSecondsLeft;
rpl::producer<bool> sendDisabledBySlowmode; rpl::producer<bool> sendDisabledBySlowmode;
rpl::producer<bool> liked;
rpl::producer<std::optional<QString>> writeRestriction; rpl::producer<std::optional<QString>> writeRestriction;
}; };

View file

@ -1069,9 +1069,10 @@ ComposeControls::ComposeControls(
, _wrap(std::make_unique<Ui::RpWidget>(parent)) , _wrap(std::make_unique<Ui::RpWidget>(parent))
, _writeRestricted(std::make_unique<Ui::RpWidget>(parent)) , _writeRestricted(std::make_unique<Ui::RpWidget>(parent))
, _send(std::make_shared<Ui::SendButton>(_wrap.get(), _st.send)) , _send(std::make_shared<Ui::SendButton>(_wrap.get(), _st.send))
, _attachToggle(Ui::CreateChild<Ui::IconButton>( , _like(_features.likes
_wrap.get(), ? Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.like)
_st.attach)) : nullptr)
, _attachToggle(Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.attach))
, _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>( , _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>(
_wrap.get(), _wrap.get(),
_st.emoji)) _st.emoji))
@ -1138,6 +1139,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) {
| rpl::then(std::move(args.slowmodeSecondsLeft)); | rpl::then(std::move(args.slowmodeSecondsLeft));
_sendDisabledBySlowmode = rpl::single(false) _sendDisabledBySlowmode = rpl::single(false)
| rpl::then(std::move(args.sendDisabledBySlowmode)); | rpl::then(std::move(args.sendDisabledBySlowmode));
_liked = args.liked ? std::move(args.liked) : rpl::single(false);
_writeRestriction = rpl::single(std::optional<QString>()) _writeRestriction = rpl::single(std::optional<QString>())
| rpl::then(std::move(args.writeRestriction)); | rpl::then(std::move(args.writeRestriction));
const auto history = *args.history; const auto history = *args.history;
@ -1153,6 +1155,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) {
initWebpageProcess(); initWebpageProcess();
initForwardProcess(); initForwardProcess();
updateBotCommandShown(); updateBotCommandShown();
updateLikeShown();
updateMessagesTTLShown(); updateMessagesTTLShown();
updateControlsGeometry(_wrap->size()); updateControlsGeometry(_wrap->size());
updateControlsVisibility(); updateControlsVisibility();
@ -1559,6 +1562,15 @@ void ComposeControls::init() {
_botCommandStart->setClickedCallback([=] { setText({ "/" }); }); _botCommandStart->setClickedCallback([=] { setText({ "/" }); });
} }
if (_like) {
_like->setClickedCallback([=] { _likeToggled.fire({}); });
_liked.value(
) | rpl::start_with_next([=](bool liked) {
const auto icon = liked ? &_st.liked : nullptr;
_like->setIconOverride(icon, icon);
}, _like->lifetime());
}
_wrap->sizeValue( _wrap->sizeValue(
) | rpl::start_with_next([=](QSize size) { ) | rpl::start_with_next([=](QSize size) {
updateControlsGeometry(size); updateControlsGeometry(size);
@ -1980,7 +1992,7 @@ void ComposeControls::fieldChanged() {
if (!_hasSendText.current() && _preview) { if (!_hasSendText.current() && _preview) {
_preview->setState(Data::PreviewState::Allowed); _preview->setState(Data::PreviewState::Allowed);
} }
if (updateBotCommandShown()) { if (updateBotCommandShown() || updateLikeShown()) {
updateControlsVisibility(); updateControlsVisibility();
updateControlsGeometry(_wrap->size()); updateControlsGeometry(_wrap->size());
} }
@ -2521,6 +2533,7 @@ void ComposeControls::updateControlsGeometry(QSize size) {
- st::historySendRight - st::historySendRight
- _send->width() - _send->width()
- _tabbedSelectorToggle->width() - _tabbedSelectorToggle->width()
- (_likeShown ? _like->width() : 0)
- (_botCommandShown ? _botCommandStart->width() : 0) - (_botCommandShown ? _botCommandStart->width() : 0)
- (_silent ? _silent->width() : 0) - (_silent ? _silent->width() : 0)
- (_ttlInfo ? _ttlInfo->width() : 0); - (_ttlInfo ? _ttlInfo->width() : 0);
@ -2560,6 +2573,12 @@ void ComposeControls::updateControlsGeometry(QSize size) {
right += _send->width(); right += _send->width();
_tabbedSelectorToggle->moveToRight(right, buttonsTop); _tabbedSelectorToggle->moveToRight(right, buttonsTop);
right += _tabbedSelectorToggle->width(); right += _tabbedSelectorToggle->width();
if (_like) {
_like->moveToRight(right, buttonsTop);
if (_likeShown) {
right += _like->width();
}
}
if (_botCommandStart) { if (_botCommandStart) {
_botCommandStart->moveToRight(right, buttonsTop); _botCommandStart->moveToRight(right, buttonsTop);
if (_botCommandShown) { if (_botCommandShown) {
@ -2584,6 +2603,9 @@ void ComposeControls::updateControlsVisibility() {
if (_botCommandStart) { if (_botCommandStart) {
_botCommandStart->setVisible(_botCommandShown); _botCommandStart->setVisible(_botCommandShown);
} }
if (_like) {
_like->setVisible(_likeShown);
}
if (_ttlInfo) { if (_ttlInfo) {
_ttlInfo->show(); _ttlInfo->show();
} }
@ -2598,6 +2620,15 @@ void ComposeControls::updateControlsVisibility() {
} }
} }
bool ComposeControls::updateLikeShown() {
auto shown = _like && !HasSendText(_field);
if (_likeShown != shown) {
_likeShown = shown;
return true;
}
return false;
}
bool ComposeControls::updateBotCommandShown() { bool ComposeControls::updateBotCommandShown() {
auto shown = false; auto shown = false;
const auto peer = _history ? _history->peer.get() : nullptr; const auto peer = _history ? _history->peer.get() : nullptr;
@ -3100,6 +3131,10 @@ rpl::producer<not_null<QEvent*>> ComposeControls::viewportEvents() const {
return _voiceRecordBar->lockViewportEvents(); return _voiceRecordBar->lockViewportEvents();
} }
rpl::producer<> ComposeControls::likeToggled() const {
return _likeToggled.events();
}
bool ComposeControls::isRecording() const { bool ComposeControls::isRecording() const {
return _voiceRecordBar->isRecording(); return _voiceRecordBar->isRecording();
} }
@ -3123,6 +3158,12 @@ rpl::producer<bool> ComposeControls::fieldMenuShownValue() const {
return _field->menuShownValue(); return _field->menuShownValue();
} }
not_null<QWidget*> ComposeControls::likeAnimationTarget() const {
Expects(_like != nullptr);
return _like;
}
bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const { bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const {
if (_voiceRecordBar->isActive()) { if (_voiceRecordBar->isActive()) {
_voiceRecordBar->showDiscardBox(std::move(continueCallback)); _voiceRecordBar->showDiscardBox(std::move(continueCallback));

View file

@ -161,6 +161,7 @@ public:
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const; [[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const; [[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const; [[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const;
[[nodiscard]] rpl::producer<> likeToggled() const;
[[nodiscard]] auto scrollKeyEvents() const [[nodiscard]] auto scrollKeyEvents() const
-> rpl::producer<not_null<QKeyEvent*>>; -> rpl::producer<not_null<QKeyEvent*>>;
[[nodiscard]] auto editLastMessageRequests() const [[nodiscard]] auto editLastMessageRequests() const
@ -221,6 +222,7 @@ public:
[[nodiscard]] rpl::producer<bool> recordingActiveValue() const; [[nodiscard]] rpl::producer<bool> recordingActiveValue() const;
[[nodiscard]] rpl::producer<bool> hasSendTextValue() const; [[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
[[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const; [[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const;
[[nodiscard]] not_null<QWidget*> likeAnimationTarget() const;
void applyCloudDraft(); void applyCloudDraft();
void applyDraft( void applyDraft(
@ -292,6 +294,7 @@ private:
bool showRecordButton() const; bool showRecordButton() const;
void drawRestrictedWrite(QPainter &p, const QString &error); void drawRestrictedWrite(QPainter &p, const QString &error);
bool updateBotCommandShown(); bool updateBotCommandShown();
bool updateLikeShown();
void cancelInlineBot(); void cancelInlineBot();
void clearInlineBot(); void clearInlineBot();
@ -344,6 +347,7 @@ private:
Fn<Api::SendAction()> _sendActionFactory; Fn<Api::SendAction()> _sendActionFactory;
rpl::variable<int> _slowmodeSecondsLeft; rpl::variable<int> _slowmodeSecondsLeft;
rpl::variable<bool> _sendDisabledBySlowmode; rpl::variable<bool> _sendDisabledBySlowmode;
rpl::variable<bool> _liked;
rpl::variable<std::optional<QString>> _writeRestriction; rpl::variable<std::optional<QString>> _writeRestriction;
rpl::variable<bool> _hidden; rpl::variable<bool> _hidden;
Mode _mode = Mode::Normal; Mode _mode = Mode::Normal;
@ -354,6 +358,7 @@ private:
std::optional<Ui::RoundRect> _backgroundRect; std::optional<Ui::RoundRect> _backgroundRect;
const std::shared_ptr<Ui::SendButton> _send; const std::shared_ptr<Ui::SendButton> _send;
Ui::IconButton * const _like = nullptr;
const not_null<Ui::IconButton*> _attachToggle; const not_null<Ui::IconButton*> _attachToggle;
std::unique_ptr<Ui::IconButton> _replaceMedia; std::unique_ptr<Ui::IconButton> _replaceMedia;
const not_null<Ui::EmojiButton*> _tabbedSelectorToggle; const not_null<Ui::EmojiButton*> _tabbedSelectorToggle;
@ -386,6 +391,7 @@ private:
rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents; rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;
rpl::event_stream<not_null<QKeyEvent*>> _editLastMessageRequests; rpl::event_stream<not_null<QKeyEvent*>> _editLastMessageRequests;
rpl::event_stream<std::optional<bool>> _attachRequests; rpl::event_stream<std::optional<bool>> _attachRequests;
rpl::event_stream<> _likeToggled;
rpl::event_stream<ReplyNextRequest> _replyNextRequests; rpl::event_stream<ReplyNextRequest> _replyNextRequests;
rpl::event_stream<> _focusRequests; rpl::event_stream<> _focusRequests;
rpl::variable<bool> _recording; rpl::variable<bool> _recording;
@ -407,6 +413,7 @@ private:
mtpRequestId _inlineBotResolveRequestId = 0; mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false; bool _isInlineBot = false;
bool _botCommandShown = false; bool _botCommandShown = false;
bool _likeShown = false;
FullMsgId _editingId; FullMsgId _editingId;
std::shared_ptr<Data::PhotoMedia> _photoEditMedia; std::shared_ptr<Data::PhotoMedia> _photoEditMedia;

View file

@ -313,7 +313,7 @@ Controller::Controller(not_null<Delegate*> delegate)
.type = Ui::MessageSendingAnimationFrom::Type::Emoji, .type = Ui::MessageSendingAnimationFrom::Type::Emoji,
.globalStartGeometry = id.globalGeometry, .globalStartGeometry = id.globalGeometry,
.frame = id.icon, .frame = id.icon,
}); }, _wrap.get());
_replyArea->sendReaction(id.id); _replyArea->sendReaction(id.id);
unfocusReply(); unfocusReply();
}, _lifetime); }, _lifetime);
@ -587,6 +587,18 @@ bool Controller::skipCaption() const {
return _captionFullView != nullptr; return _captionFullView != nullptr;
} }
bool Controller::liked() const {
return _liked.current();
}
rpl::producer<bool> Controller::likedValue() const {
return _liked.value();
}
void Controller::toggleLiked(bool liked) {
_liked = liked;
}
void Controller::showFullCaption() { void Controller::showFullCaption() {
if (_captionText.empty()) { if (_captionText.empty()) {
return; return;
@ -892,6 +904,7 @@ bool Controller::changeShown(Data::Story *story) {
story, story,
Data::Stories::Polling::Viewer); Data::Stories::Polling::Viewer);
} }
_liked = false;
return true; return true;
} }
@ -1551,7 +1564,8 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
void Controller::startReactionAnimation( void Controller::startReactionAnimation(
Data::ReactionId id, Data::ReactionId id,
Ui::MessageSendingAnimationFrom from) { Ui::MessageSendingAnimationFrom from,
not_null<QWidget*> target) {
Expects(shown()); Expects(shown());
auto args = Ui::ReactionFlyAnimationArgs{ auto args = Ui::ReactionFlyAnimationArgs{
@ -1568,7 +1582,7 @@ void Controller::startReactionAnimation(
Data::CustomEmojiSizeTag::Isolated); Data::CustomEmojiSizeTag::Isolated);
const auto layer = _reactionAnimation->layer(); const auto layer = _reactionAnimation->layer();
_wrap->paintRequest() | rpl::start_with_next([=] { _wrap->paintRequest() | rpl::start_with_next([=] {
if (!_reactionAnimation->paintBadgeFrame(_wrap.get())) { if (!_reactionAnimation->paintBadgeFrame(target)) {
InvokeQueued(layer, [=] { InvokeQueued(layer, [=] {
_reactionAnimation = nullptr; _reactionAnimation = nullptr;
_wrap->update(); _wrap->update();

View file

@ -123,6 +123,9 @@ public:
[[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] TextWithEntities captionText() const;
[[nodiscard]] bool skipCaption() const; [[nodiscard]] bool skipCaption() const;
[[nodiscard]] bool liked() const;
[[nodiscard]] rpl::producer<bool> likedValue() const;
void toggleLiked(bool liked);
void showFullCaption(); void showFullCaption();
void captionClosing(); void captionClosing();
void captionClosed(); void captionClosed();
@ -236,7 +239,8 @@ private:
void startReactionAnimation( void startReactionAnimation(
Data::ReactionId id, Data::ReactionId id,
Ui::MessageSendingAnimationFrom from); Ui::MessageSendingAnimationFrom from,
not_null<QWidget*> target);
const not_null<Delegate*> _delegate; const not_null<Delegate*> _delegate;
@ -269,6 +273,7 @@ private:
Data::StoriesContext _context; Data::StoriesContext _context;
std::optional<Data::StoriesSource> _source; std::optional<Data::StoriesSource> _source;
std::optional<StoriesList> _list; std::optional<StoriesList> _list;
rpl::variable<bool> _liked;
FullStoryId _waitingForId; FullStoryId _waitingForId;
int _waitingForDelta = 0; int _waitingForDelta = 0;
int _index = 0; int _index = 0;

View file

@ -121,6 +121,7 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),
.voiceLockFromBottom = true, .voiceLockFromBottom = true,
.features = { .features = {
.likes = true,
.sendAs = false, .sendAs = false,
.ttlInfo = false, .ttlInfo = false,
.botCommandSend = false, .botCommandSend = false,
@ -620,6 +621,11 @@ void ReplyArea::initActions() {
sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); sendInlineResult(chosen.result, chosen.bot, chosen.options, localId);
}, _lifetime); }, _lifetime);
_controls->likeToggled(
) | rpl::start_with_next([=] {
_controller->toggleLiked(!_controller->liked());
}, _lifetime);
_controls->setMimeDataHook([=]( _controls->setMimeDataHook([=](
not_null<const QMimeData*> data, not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) { Ui::InputField::MimeAction action) {
@ -660,6 +666,7 @@ void ReplyArea::show(ReplyAreaData data) {
const auto history = user ? user->owner().history(user).get() : nullptr; const auto history = user ? user->owner().history(user).get() : nullptr;
_controls->setHistory({ _controls->setHistory({
.history = history, .history = history,
.liked = _controller->likedValue(),
}); });
_controls->clear(); _controls->clear();
const auto hidden = user && user->isSelf(); const auto hidden = user && user->isSelf();
@ -718,6 +725,10 @@ void ReplyArea::tryProcessKeyInput(not_null<QKeyEvent*> e) {
_controls->tryProcessKeyInput(e); _controls->tryProcessKeyInput(e);
} }
not_null<QWidget*> ReplyArea::likeAnimationTarget() const {
return _controls->likeAnimationTarget();
}
void ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) { void ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) {
// #TODO stories // #TODO stories
} }

View file

@ -70,6 +70,8 @@ public:
[[nodiscard]] bool ignoreWindowMove(QPoint position) const; [[nodiscard]] bool ignoreWindowMove(QPoint position) const;
void tryProcessKeyInput(not_null<QKeyEvent*> e); void tryProcessKeyInput(not_null<QKeyEvent*> e);
[[nodiscard]] not_null<QWidget*> likeAnimationTarget() const;
private: private:
class Cant; class Cant;

View file

@ -353,7 +353,7 @@ struct Feature {
if (const auto window = show->resolveWindow(usage)) { if (const auto window = show->resolveWindow(usage)) {
Settings::ShowPremium( Settings::ShowPremium(
window, window,
u"stories_stealth_mode"_q); u"stories__stealth_mode"_q);
window->window().activate(); window->window().activate();
} }
} else if (now.mode.cooldownTill > now.now) { } else if (now.mode.cooldownTill > now.now) {

View file

@ -109,6 +109,7 @@ mediaviewRight: icon {
{ "mediaview/next", mediaviewControlFg } { "mediaview/next", mediaviewControlFg }
}; };
mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }}; mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }};
mediaviewSaveLocked: icon {{ "mediaview/download_locked", mediaviewControlFg }};
mediaviewShare: icon {{ "mediaview/viewer_share", mediaviewControlFg }}; mediaviewShare: icon {{ "mediaview/viewer_share", mediaviewControlFg }};
mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }}; mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }};
mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }}; mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }};
@ -466,6 +467,10 @@ storiesAttach: IconButton(historyAttach) {
iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }};
ripple: storiesComposeRippleLight; ripple: storiesComposeRippleLight;
} }
storiesLike: IconButton(storiesAttach) {
icon: icon {{ "chat/input_like", storiesComposeGrayIcon }};
iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }};
}
storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRemoveSet: IconButton(stickerPanRemoveSet) { storiesRemoveSet: IconButton(stickerPanRemoveSet) {
@ -676,6 +681,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
} }
attach: storiesAttach; attach: storiesAttach;
emoji: storiesAttachEmoji; emoji: storiesAttachEmoji;
like: storiesLike;
liked: icon{{ "chat/input_liked", settingsIconBg1 }};
suggestions: EmojiSuggestions(defaultEmojiSuggestions) { suggestions: EmojiSuggestions(defaultEmojiSuggestions) {
dropdown: InnerDropdown(emojiSuggestionsDropdown) { dropdown: InnerDropdown(emojiSuggestionsDropdown) {
animation: PanelAnimation(defaultPanelAnimation) { animation: PanelAnimation(defaultPanelAnimation) {

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "media/view/media_view_overlay_opengl.h" #include "media/view/media_view_overlay_opengl.h"
#include "data/data_peer_values.h" // AmPremiumValue.
#include "ui/gl/gl_shader.h" #include "ui/gl/gl_shader.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "media/stories/media_stories_view.h" #include "media/stories/media_stories_view.h"
@ -122,7 +123,16 @@ OverlayWidget::RendererGL::RendererGL(not_null<OverlayWidget*> owner)
crl::on_main(this, [=] { crl::on_main(this, [=] {
_owner->_storiesChanged.events( _owner->_storiesChanged.events(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
invalidateControls(); if (_owner->_storiesSession) {
Data::AmPremiumValue(
_owner->_storiesSession
) | rpl::start_with_next([=] {
invalidateControls();
}, _storiesLifetime);
} else {
_storiesLifetime.destroy();
invalidateControls();
}
}, _lifetime); }, _lifetime);
}); });
} }
@ -648,8 +658,7 @@ void OverlayWidget::RendererGL::paintControl(
QRect inner, QRect inner,
float64 innerOpacity, float64 innerOpacity,
const style::icon &icon) { const style::icon &icon) {
const auto stories = (_owner->_stories != nullptr); const auto meta = controlMeta(control);
const auto meta = ControlMeta(control, stories);
Assert(meta.icon == &icon); Assert(meta.icon == &icon);
const auto overAlpha = overOpacity * kOverBackgroundOpacity; const auto overAlpha = overOpacity * kOverBackgroundOpacity;
@ -707,18 +716,25 @@ void OverlayWidget::RendererGL::paintControl(
FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset);
} }
auto OverlayWidget::RendererGL::ControlMeta(Over control, bool stories) auto OverlayWidget::RendererGL::controlMeta(Over control) const -> Control {
-> Control { const auto stories = [&] {
return (_owner->_stories != nullptr);
};
switch (control) { switch (control) {
case Over::Left: return { case Over::Left: return {
0, 0,
stories ? &st::storiesLeft : &st::mediaviewLeft stories() ? &st::storiesLeft : &st::mediaviewLeft
}; };
case Over::Right: return { case Over::Right: return {
1, 1,
stories ? &st::storiesRight : &st::mediaviewRight stories() ? &st::storiesRight : &st::mediaviewRight
};
case Over::Save: return {
2,
(_owner->saveControlLocked()
? &st::mediaviewSaveLocked
: &st::mediaviewSave)
}; };
case Over::Save: return { 2, &st::mediaviewSave };
case Over::Share: return { 3, &st::mediaviewShare }; case Over::Share: return { 3, &st::mediaviewShare };
case Over::Rotate: return { 4, &st::mediaviewRotate }; case Over::Rotate: return { 4, &st::mediaviewRotate };
case Over::More: return { 5, &st::mediaviewMore }; case Over::More: return { 5, &st::mediaviewMore };
@ -730,14 +746,13 @@ void OverlayWidget::RendererGL::validateControls() {
if (!_controlsImage.image().isNull()) { if (!_controlsImage.image().isNull()) {
return; return;
} }
const auto stories = (_owner->_stories != nullptr);
const auto metas = { const auto metas = {
ControlMeta(Over::Left, stories), controlMeta(Over::Left),
ControlMeta(Over::Right, stories), controlMeta(Over::Right),
ControlMeta(Over::Save, stories), controlMeta(Over::Save),
ControlMeta(Over::Share, stories), controlMeta(Over::Share),
ControlMeta(Over::Rotate, stories), controlMeta(Over::Rotate),
ControlMeta(Over::More, stories), controlMeta(Over::More),
}; };
auto maxWidth = 0; auto maxWidth = 0;
auto fullHeight = 0; auto fullHeight = 0;

View file

@ -149,7 +149,7 @@ private:
Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount]; Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount];
static constexpr auto kControlsCount = 6; static constexpr auto kControlsCount = 6;
[[nodiscard]] static Control ControlMeta(Over control, bool stories); [[nodiscard]] Control controlMeta(Over control) const;
// Last one is for the over circle image. // Last one is for the over circle image.
std::array<QRect, kControlsCount + 1> _controlsTextures; std::array<QRect, kControlsCount + 1> _controlsTextures;
@ -158,6 +158,7 @@ private:
bool _shadowsForStories = false; bool _shadowsForStories = false;
bool _blendingEnabled = false; bool _blendingEnabled = false;
rpl::lifetime _storiesLifetime;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };

View file

@ -86,6 +86,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "layout/layout_document_generic_preview.h" #include "layout/layout_document_generic_preview.h"
#include "platform/platform_overlay_widget.h" #include "platform/platform_overlay_widget.h"
#include "settings/settings_premium.h"
#include "storage/file_download.h" #include "storage/file_download.h"
#include "storage/storage_account.h" #include "storage/storage_account.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
@ -130,6 +131,7 @@ constexpr auto kIdsPreloadAfter = 28;
constexpr auto kLeftSiblingTextureIndex = 1; constexpr auto kLeftSiblingTextureIndex = 1;
constexpr auto kRightSiblingTextureIndex = 2; constexpr auto kRightSiblingTextureIndex = 2;
constexpr auto kStoriesControlsOpacity = 1.; constexpr auto kStoriesControlsOpacity = 1.;
constexpr auto kStorySavePromoDuration = 3 * crl::time(1000);
class PipDelegate final : public Pip::Delegate { class PipDelegate final : public Pip::Delegate {
public: public:
@ -326,16 +328,18 @@ public:
return _widget->_body; return _widget->_body;
} }
bool valid() const override { bool valid() const override {
return _widget->_storiesSession != nullptr; return _widget->_session || _widget->_storiesSession;
} }
operator bool() const override { operator bool() const override {
return valid(); return valid();
} }
Main::Session &session() const override { Main::Session &session() const override {
Expects(_widget->_storiesSession != nullptr); Expects(_widget->_session || _widget->_storiesSession);
return *_widget->_storiesSession; return _widget->_session
? *_widget->_session
: *_widget->_storiesSession;
} }
bool paused(ChatHelpers::PauseReason reason) const override { bool paused(ChatHelpers::PauseReason reason) const override {
if (_widget->isHidden() if (_widget->isHidden()
@ -1024,22 +1028,26 @@ QSize OverlayWidget::flipSizeByRotation(QSize size) const {
return FlipSizeByRotation(size, _rotation); return FlipSizeByRotation(size, _rotation);
} }
bool OverlayWidget::hasCopyMediaRestriction() const { bool OverlayWidget::hasCopyMediaRestriction(bool skipPremiumCheck) const {
const auto story = _stories ? _stories->story() : nullptr; if (const auto story = _stories ? _stories->story() : nullptr) {
return (story && !story->canDownload()) return skipPremiumCheck
|| (_history && !_history->peer->allowsForwarding()) ? story->canDownloadIfPremium()
: story->canDownloadChecked();
}
return (_history && !_history->peer->allowsForwarding())
|| (_message && _message->forbidsSaving()); || (_message && _message->forbidsSaving());
} }
bool OverlayWidget::showCopyMediaRestriction() { bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) {
if (!hasCopyMediaRestriction()) { if (!hasCopyMediaRestriction(skipPRemiumCheck)) {
return false; return false;
} else if (!_history) { } else if (_stories) {
return true; uiShow()->showToast(tr::lng_error_nocopy_story(tr::now));
} else if (_history) {
uiShow()->showToast(_history->peer->isBroadcast()
? tr::lng_error_nocopy_channel(tr::now)
: tr::lng_error_nocopy_group(tr::now));
} }
Ui::Toast::Show(_widget, _history->peer->isBroadcast()
? tr::lng_error_nocopy_channel(tr::now)
: tr::lng_error_nocopy_group(tr::now));
return true; return true;
} }
@ -1210,8 +1218,8 @@ void OverlayWidget::refreshNavVisibility() {
} }
} }
bool OverlayWidget::contentCanBeSaved() const { bool OverlayWidget::computeSaveButtonVisible() const {
if (hasCopyMediaRestriction()) { if (hasCopyMediaRestriction(true)) {
return false; return false;
} else if (_photo) { } else if (_photo) {
return _photo->hasVideo() || _photoMedia->loaded(); return _photo->hasVideo() || _photoMedia->loaded();
@ -1240,6 +1248,26 @@ void OverlayWidget::checkForSaveLoaded() {
} }
} }
void OverlayWidget::showPremiumDownloadPromo() {
const auto filter = [=](const auto &...) {
const auto usage = ChatHelpers::WindowUsage::PremiumPromo;
if (const auto window = uiShow()->resolveWindow(usage)) {
const auto ref = u"stories__save_stories_to_gallery"_q;
Settings::ShowPremium(window, ref);
}
return false;
};
uiShow()->showToast({
.text = tr::lng_stories_save_promo(
tr::now,
lt_link,
Ui::Text::Bold(tr::lng_send_as_premium_required_link(tr::now)),
Ui::Text::WithEntities),
.duration = kStorySavePromoDuration,
.filter = filter,
});
}
void OverlayWidget::updateControls() { void OverlayWidget::updateControls() {
if (_document && documentBubbleShown()) { if (_document && documentBubbleShown()) {
_docRect = QRect( _docRect = QRect(
@ -1290,7 +1318,7 @@ void OverlayWidget::updateControls() {
const auto overRect = QRect( const auto overRect = QRect(
QPoint(), QPoint(),
QSize(st::mediaviewIconOver, st::mediaviewIconOver)); QSize(st::mediaviewIconOver, st::mediaviewIconOver));
_saveVisible = contentCanBeSaved(); _saveVisible = computeSaveButtonVisible();
_shareVisible = story && story->canShare(); _shareVisible = story && story->canShare();
_rotateVisible = !_themePreviewShown && !story; _rotateVisible = !_themePreviewShown && !story;
const auto navRect = [&](int i) { const auto navRect = [&](int i) {
@ -1320,6 +1348,7 @@ void OverlayWidget::updateControls() {
_saveNav = navRect(index); _saveNav = navRect(index);
_saveNavOver = style::centerrect(_saveNav, overRect); _saveNavOver = style::centerrect(_saveNav, overRect);
_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave); _saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
Assert(st::mediaviewSave.size() == st::mediaviewSaveLocked.size());
const auto dNow = QDateTime::currentDateTime(); const auto dNow = QDateTime::currentDateTime();
const auto d = [&] { const auto d = [&] {
@ -1471,7 +1500,7 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
? &st::mediaMenuIconArchiveStory ? &st::mediaMenuIconArchiveStory
: &st::mediaMenuIconSaveStory); : &st::mediaMenuIconSaveStory);
} }
if ((!story || story->canDownload()) if ((!story || story->canDownloadChecked())
&& _document && _document
&& !_document->filepath(true).isEmpty()) { && !_document->filepath(true).isEmpty()) {
const auto text = Platform::IsMac() const auto text = Platform::IsMac()
@ -1533,11 +1562,13 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
[=] { deleteMedia(); }, [=] { deleteMedia(); },
&st::mediaMenuIconDelete); &st::mediaMenuIconDelete);
} }
if (!hasCopyMediaRestriction()) { if (!hasCopyMediaRestriction(true)) {
addAction( addAction(
tr::lng_mediaview_save_as(tr::now), tr::lng_mediaview_save_as(tr::now),
[=] { saveAs(); }, [=] { saveAs(); },
&st::mediaMenuIconDownload); (saveControlLocked()
? &st::mediaMenuIconDownloadLocked
: &st::mediaMenuIconDownload));
} }
if (const auto overviewType = computeOverviewType()) { if (const auto overviewType = computeOverviewType()) {
@ -2285,7 +2316,11 @@ void OverlayWidget::notifyFileDialogShown(bool shown) {
} }
void OverlayWidget::saveAs() { void OverlayWidget::saveAs() {
if (showCopyMediaRestriction()) { if (showCopyMediaRestriction(true)) {
return;
} else if (hasCopyMediaRestriction()) {
Assert(_stories != nullptr);
showPremiumDownloadPromo();
return; return;
} }
QString file; QString file;
@ -2421,9 +2456,13 @@ void OverlayWidget::handleDocumentClick() {
void OverlayWidget::downloadMedia() { void OverlayWidget::downloadMedia() {
if (!_photo && !_document) { if (!_photo && !_document) {
return; return;
} } else if (Core::App().settings().askDownloadPath()) {
if (Core::App().settings().askDownloadPath()) {
return saveAs(); return saveAs();
} else if (hasCopyMediaRestriction()) {
if (_stories && !hasCopyMediaRestriction(true)) {
showPremiumDownloadPromo();
}
return;
} }
QString path; QString path;
@ -2478,7 +2517,7 @@ void OverlayWidget::downloadMedia() {
} }
} }
} else { } else {
_saveVisible = contentCanBeSaved(); _saveVisible = computeSaveButtonVisible();
update(_saveNavOver); update(_saveNavOver);
} }
updateOver(_lastMouseMovePos); updateOver(_lastMouseMovePos);
@ -2498,7 +2537,7 @@ void OverlayWidget::downloadMedia() {
} }
} else { } else {
if (!_photo || !_photoMedia->loaded()) { if (!_photo || !_photoMedia->loaded()) {
_saveVisible = contentCanBeSaved(); _saveVisible = computeSaveButtonVisible();
update(_saveNavOver); update(_saveNavOver);
} else { } else {
if (!QDir().exists(path)) { if (!QDir().exists(path)) {
@ -3899,8 +3938,7 @@ void OverlayWidget::initThemePreview() {
_themeShare->setClickedCallback([=] { _themeShare->setClickedCallback([=] {
QGuiApplication::clipboard()->setText( QGuiApplication::clipboard()->setText(
session->createInternalLinkFull("addtheme/" + slug)); session->createInternalLinkFull("addtheme/" + slug));
Ui::Toast::Show( uiShow()->showToast(
_body,
tr::lng_background_link_copied(tr::now)); tr::lng_background_link_copied(tr::now));
}); });
} else { } else {
@ -4198,6 +4236,10 @@ not_null<Ui::RpWidget*> OverlayWidget::storiesWrap() {
} }
std::shared_ptr<ChatHelpers::Show> OverlayWidget::storiesShow() { std::shared_ptr<ChatHelpers::Show> OverlayWidget::storiesShow() {
return uiShow();
}
std::shared_ptr<ChatHelpers::Show> OverlayWidget::uiShow() {
if (!_cachedShow) { if (!_cachedShow) {
_cachedShow = std::make_shared<Show>(this); _cachedShow = std::make_shared<Show>(this);
} }
@ -4778,6 +4820,13 @@ void OverlayWidget::paintSaveMsgContent(
p.setOpacity(1); p.setOpacity(1);
} }
bool OverlayWidget::saveControlLocked() const {
const auto story = _stories ? _stories->story() : nullptr;
return story
&& story->canDownloadIfPremium()
&& !story->canDownloadChecked();
}
void OverlayWidget::paintControls( void OverlayWidget::paintControls(
not_null<Renderer*> renderer, not_null<Renderer*> renderer,
float64 opacity) { float64 opacity) {
@ -4811,7 +4860,9 @@ void OverlayWidget::paintControls(
_saveVisible, _saveVisible,
_saveNavOver, _saveNavOver,
_saveNavIcon, _saveNavIcon,
st::mediaviewSave }, (saveControlLocked()
? st::mediaviewSaveLocked
: st::mediaviewSave) },
{ {
Over::Share, Over::Share,
_shareVisible, _shareVisible,

View file

@ -245,6 +245,7 @@ private:
void playbackPauseMusic(); void playbackPauseMusic();
void switchToPip(); void switchToPip();
[[nodiscard]] int topNotchSkip() const; [[nodiscard]] int topNotchSkip() const;
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
not_null<Ui::RpWidget*> storiesWrap() override; not_null<Ui::RpWidget*> storiesWrap() override;
std::shared_ptr<ChatHelpers::Show> storiesShow() override; std::shared_ptr<ChatHelpers::Show> storiesShow() override;
@ -310,8 +311,9 @@ private:
void handleScreenChanged(QScreen *screen); void handleScreenChanged(QScreen *screen);
bool contentCanBeSaved() const; [[nodiscard]] bool computeSaveButtonVisible() const;
void checkForSaveLoaded(); void checkForSaveLoaded();
void showPremiumDownloadPromo();
Entity entityForUserPhotos(int index) const; Entity entityForUserPhotos(int index) const;
Entity entityForSharedMedia(int index) const; Entity entityForSharedMedia(int index) const;
@ -495,8 +497,10 @@ private:
void validatePhotoImage(Image *image, bool blurred); void validatePhotoImage(Image *image, bool blurred);
void validatePhotoCurrentImage(); void validatePhotoCurrentImage();
[[nodiscard]] bool hasCopyMediaRestriction() const; [[nodiscard]] bool hasCopyMediaRestriction(
[[nodiscard]] bool showCopyMediaRestriction(); bool skipPremiumCheck = false) const;
[[nodiscard]] bool showCopyMediaRestriction(
bool skipPRemiumCheck = false);
[[nodiscard]] QSize flipSizeByRotation(QSize size) const; [[nodiscard]] QSize flipSizeByRotation(QSize size) const;
@ -518,7 +522,8 @@ private:
[[nodiscard]] bool contentShown() const; [[nodiscard]] bool contentShown() const;
[[nodiscard]] bool opaqueContentShown() const; [[nodiscard]] bool opaqueContentShown() const;
void clearStreaming(bool savePosition = true); void clearStreaming(bool savePosition = true);
bool canInitStreaming() const; [[nodiscard]] bool canInitStreaming() const;
[[nodiscard]] bool saveControlLocked() const;
[[nodiscard]] bool topShadowOnTheRight() const; [[nodiscard]] bool topShadowOnTheRight() const;
void applyHideWindowWorkaround(); void applyHideWindowWorkaround();

View file

@ -95,8 +95,7 @@ void EmojiFlyAnimation::repaint() {
} }
} }
bool EmojiFlyAnimation::paintBadgeFrame( bool EmojiFlyAnimation::paintBadgeFrame(not_null<QWidget*> widget) {
not_null<Ui::RpWidget*> widget) {
_target = widget; _target = widget;
return !_fly.finished(); return !_fly.finished();
} }

View file

@ -25,7 +25,7 @@ public:
[[nodiscard]] bool finished() const; [[nodiscard]] bool finished() const;
void repaint(); void repaint();
bool paintBadgeFrame(not_null<Ui::RpWidget*> widget); bool paintBadgeFrame(not_null<QWidget*> widget);
private: private:
const int _flySize = 0; const int _flySize = 0;
@ -33,7 +33,7 @@ private:
Ui::RpWidget _layer; Ui::RpWidget _layer;
QRect _area; QRect _area;
bool _areaUpdated = false; bool _areaUpdated = false;
QPointer<Ui::RpWidget> _target; QPointer<QWidget> _target;
}; };

View file

@ -126,6 +126,7 @@ mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }};
mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }}; mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }};
mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }}; mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }};
mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }}; mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }};
mediaMenuIconDownloadLocked: icon {{ "menu/download_locked", mediaviewMenuFg }};
mediaMenuIconCopy: icon {{ "menu/copy", mediaviewMenuFg }}; mediaMenuIconCopy: icon {{ "menu/copy", mediaviewMenuFg }};
mediaMenuIconForward: icon {{ "menu/forward", mediaviewMenuFg }}; mediaMenuIconForward: icon {{ "menu/forward", mediaviewMenuFg }};
mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }}; mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }};