diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 59c701340..b1fc1570d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -406,6 +406,8 @@ PRIVATE chat_helpers/tabbed_section.h chat_helpers/tabbed_selector.cpp chat_helpers/tabbed_selector.h + chat_helpers/ttl_media_layer_widget.cpp + chat_helpers/ttl_media_layer_widget.h core/application.cpp core/application.h core/base_integration.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 347fdbdc7..cec8bf36e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1719,6 +1719,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ttl_voice_expired" = "Voice message expired"; "lng_ttl_round_sent" = "You sent a self-destructing video message."; "lng_ttl_round_expired" = "Round message expired"; +"lng_ttl_voice_tooltip_in" = "This voice message can only be played once."; +"lng_ttl_voice_tooltip_out" = "This message will disappear once **{user}** plays it once."; +"lng_ttl_voice_close_in" = "Delete and close"; "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group."; "lng_profile_camera_title" = "Capture yourself"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index c86e98b75..a7f94d301 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1266,3 +1266,17 @@ dragDropColor: windowActiveTextFg; dragMargin: margins(0px, 10px, 0px, 10px); dragPadding: margins(20px, 10px, 20px, 10px); dragHeight: 72px; + +ttlMediaImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { + style: TextStyle(defaultTextStyle) { + font: font(14px); + } +} +ttlMediaButton: RoundButton(defaultActiveButton) { + textBg: shadowFg; + textBgOver: shadowFg; + ripple: universalRippleAnimation; + height: 31px; + textTop: 6px; +} +ttlMediaButtonBottomSkip: 14px; diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp new file mode 100644 index 000000000..91403a2c2 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp @@ -0,0 +1,273 @@ +/* +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 "chat_helpers/ttl_media_layer_widget.h" + +#include "base/event_filter.h" +#include "data/data_session.h" +#include "editor/editor_layer_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/view/history_view_element.h" +#include "history/view/media/history_view_document.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "media/audio/media_audio.h" +#include "media/player/media_player_instance.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/painter.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "window/themes/window_theme.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_dialogs.h" + +namespace ChatHelpers { +namespace { + +class PreviewDelegate final : public HistoryView::DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + HistoryView::Context elementContext() override; + bool elementIsChatWide() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +not_null PreviewDelegate::elementPathShiftGradient() { + return _pathGradient.get(); +} + +HistoryView::Context PreviewDelegate::elementContext() { + return HistoryView::Context::TTLViewer; +} + +bool PreviewDelegate::elementIsChatWide() { + return true; +} + +class PreviewWrap final : public Ui::RpWidget { +public: + PreviewWrap(not_null parent, not_null item); + ~PreviewWrap(); + + [[nodiscard]] rpl::producer<> closeRequests() const; + +private: + void paintEvent(QPaintEvent *e) override; + [[nodiscard]] QRect elementRect() const; + + const not_null _item; + const std::unique_ptr _theme; + const std::unique_ptr _style; + const std::unique_ptr _delegate; + std::unique_ptr _element; + rpl::lifetime _elementLifetime; + + rpl::event_stream<> _closeRequests; + +}; + +PreviewWrap::PreviewWrap( + not_null parent, + not_null item) +: RpWidget(parent) +, _item(item) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _style(std::make_unique( + item->history()->session().colorIndicesValue())) +, _delegate(std::make_unique( + parent, + _style.get(), + [=] { update(elementRect()); })) { + _style->apply(_theme.get()); + + const auto session = &_item->history()->session(); + session->data().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == _element.get()) { + update(elementRect()); + } + }, lifetime()); + + const auto closeCallback = [=] { _closeRequests.fire({}); }; + + { + const auto close = Ui::CreateChild( + this, + item->out() + ? tr::lng_close() + : tr::lng_ttl_voice_close_in(), + st::ttlMediaButton); + close->setFullRadius(true); + close->setClickedCallback(closeCallback); + close->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + + sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + close->moveToLeft( + (s.width() - close->width()) / 2, + s.height() - close->height() - st::ttlMediaButtonBottomSkip); + }, close->lifetime()); + } + + QWidget::setAttribute(Qt::WA_OpaquePaintEvent, false); + _element = _item->createView(_delegate.get()); + + { + _element->initDimensions(); + widthValue( + ) | rpl::filter([=](int width) { + return width > st::msgMinWidth; + }) | rpl::start_with_next([=](int width) { + _element->resizeGetHeight(width); + }, _elementLifetime); + } + + { + auto text = item->out() + ? tr::lng_ttl_voice_tooltip_out( + lt_user, + rpl::single( + item->history()->peer->name() + ) | rpl::map(Ui::Text::RichLangValue), + Ui::Text::RichLangValue) + : tr::lng_ttl_voice_tooltip_in(Ui::Text::RichLangValue); + const auto tooltip = Ui::CreateChild( + this, + object_ptr>( + this, + Ui::MakeNiceTooltipLabel( + parent, + std::move(text), + st::dialogsStoriesTooltipMaxWidth, + st::ttlMediaImportantTooltipLabel), + st::defaultImportantTooltip.padding), + st::dialogsStoriesTooltip); + tooltip->toggleFast(true); + sizeValue( + ) | rpl::filter( + [](const QSize &s) { return !s.isNull(); } + ) | rpl::take(1) | rpl::start_with_next([=](const QSize &s) { + if (s.isEmpty()) { + return; + } + auto area = elementRect(); + area.setWidth(_element->media() + ? _element->media()->width() + : _element->width()); + tooltip->pointAt(area, RectPart::Top, [=](QSize size) { + return QPoint{ + (area.width() - size.width()) / 2, + (s.height() - size.height() * 2 - _element->height()) / 2 + - st::defaultImportantTooltip.padding.top(), + }; + }); + }, tooltip->lifetime()); + } + + HistoryView::TTLVoiceStops( + item->fullId() + ) | rpl::start_with_next(closeCallback, lifetime()); +} + +QRect PreviewWrap::elementRect() const { + return QRect( + (width() - _element->width()) / 2, + (height() - _element->height()) / 2, + _element->width(), + _element->height()); +} + +rpl::producer<> PreviewWrap::closeRequests() const { + return _closeRequests.events(); +} + +PreviewWrap::~PreviewWrap() { + _elementLifetime.destroy(); + _element = nullptr; +} + +void PreviewWrap::paintEvent(QPaintEvent *e) { + if (!_element) { + return; + } + + auto p = Painter(this); + const auto r = rect(); + + auto context = _theme->preparePaintContext( + _style.get(), + r, + e->rect(), + !window()->isActiveWindow()); + context.outbg = _element->hasOutLayout(); + + p.translate( + (r.width() - _element->width()) / 2, + (r.height() - _element->height()) / 2); + _element->draw(p, context); +} + +} // namespace + +void ShowTTLMediaLayerWidget( + not_null controller, + not_null item) { + const auto parent = controller->content(); + const auto show = controller->uiShow(); + auto preview = base::make_unique_q(parent, item); + preview->closeRequests( + ) | rpl::start_with_next([=] { + show->hideLayer(); + }, preview->lifetime()); + auto layer = std::make_unique( + parent, + std::move(preview)); + layer->lifetime().add([] { ::Media::Player::instance()->stop(); }); + base::install_event_filter(layer.get(), [=](not_null e) { + if (e->type() == QEvent::KeyPress) { + const auto k = static_cast(e.get()); + if (k->key() == Qt::Key_Escape) { + show->hideLayer(); + } + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }); + controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); +} + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h new file mode 100644 index 000000000..f31d04982 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h @@ -0,0 +1,22 @@ +/* +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 HistoryItem; + +namespace Window { +class SessionController; +} // namespace Window + +namespace ChatHelpers { + +void ShowTTLMediaLayerWidget( + not_null controller, + not_null item); + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 2c34719ff..8ab502564 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -9,7 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "base/platform/base_platform_info.h" -#include "ui/boxes/confirm_box.h" +#include "boxes/abstract_box.h" // Ui::show(). +#include "chat_helpers/ttl_media_layer_widget.h" #include "core/application.h" #include "core/core_settings.h" #include "core/mime_type.h" @@ -17,17 +18,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "data/data_file_click_handler.h" #include "data/data_session.h" -#include "history/view/media/history_view_gif.h" #include "history/history.h" #include "history/history_item.h" -#include "media/player/media_player_instance.h" +#include "history/view/media/history_view_gif.h" #include "lang/lang_keys.h" +#include "media/player/media_player_instance.h" #include "platform/platform_file_utilities.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_theme.h" #include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" #include "window/window_session_controller.h" -#include "boxes/abstract_box.h" // Ui::show(). #include "styles/style_layers.h" #include @@ -298,6 +299,12 @@ void ResolveDocument( || document->isVoiceMessage() || document->isVideoMessage()) { ::Media::Player::instance()->playPause({ document, msgId }); + if (controller + && item + && item->media() + && item->media()->ttlSeconds()) { + ChatHelpers::ShowTTLMediaLayerWidget(controller, item); + } } else { showDocument(); } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 3b524915f..134a7bf70 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -58,6 +58,7 @@ enum class Context : char { AdminLog, ContactPreview, SavedSublist, + TTLViewer, }; enum class OnlyEmojiAndSpaces : char { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index b53fb1454..d7c9e2f16 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2005,6 +2005,7 @@ bool Message::hasFromPhoto() const { return !item->out() && !item->history()->peer->isUser(); } break; case Context::ContactPreview: + case Context::TTLViewer: return false; } Unexpected("Context in Message::hasFromPhoto."); @@ -3200,9 +3201,10 @@ bool Message::hasFromName() const { return false; } break; case Context::ContactPreview: + case Context::TTLViewer: return false; } - Unexpected("Context in Message::hasFromPhoto."); + Unexpected("Context in Message::hasFromName."); } bool Message::displayFromName() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 7ca7bc747..2dc91de74 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -134,7 +134,11 @@ void DrawCornerBadgeTTL( }); animate(animate); }); + const auto weak = std::weak_ptr(lifetime); return [=](QPainter &p, QRect r, QColor c) { + if (weak.expired()) { + return; + } (state->idle ? state->idle : state->start)->paintInCenter(p, r, c); }; } @@ -326,41 +330,36 @@ Document::Document( } if ((_data->isVoiceMessage() || isRound) - && IsVoiceOncePlayable(_parent->data())) { - _parent->data()->removeFromSharedMediaIndex(); - setDocumentLinks(_data, realParent, [=] { - _openl = nullptr; - + && _parent->data()->media()->ttlSeconds()) { + const auto fullId = _realParent->fullId(); + if (_parent->delegate()->elementContext() == Context::TTLViewer) { auto lifetime = std::make_shared(); - rpl::merge( - ::Media::Player::instance()->updatedNotifier( - ) | rpl::filter([=](::Media::Player::TrackState state) { - using State = ::Media::Player::State; - const auto badState = state.state == State::Stopped - || state.state == State::StoppedAtEnd - || state.state == State::StoppedAtError - || state.state == State::StoppedAtStart; - return (state.id.contextId() != _realParent->fullId()) - && !badState; - }) | rpl::to_empty, - ::Media::Player::instance()->tracksFinished( - ) | rpl::filter([=](AudioMsgId::Type type) { - return (type == AudioMsgId::Type::Voice); - }) | rpl::to_empty, - ::Media::Player::instance()->stops(AudioMsgId::Type::Voice) - ) | rpl::start_with_next([=]() mutable { - _drawTtl = nullptr; - const auto item = _parent->data(); + TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable { if (lifetime) { base::take(lifetime)->destroy(); } - // Destroys this. - ClearMediaAsExpired(item); }, *lifetime); _drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); }); + } else if (!_parent->data()->out()) { + _parent->data()->removeFromSharedMediaIndex(); + setDocumentLinks(_data, realParent, [=] { + _openl = nullptr; - return false; - }); + auto lifetime = std::make_shared(); + TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable { + const auto item = _parent->data(); + if (lifetime) { + base::take(lifetime)->destroy(); + } + // Destroys this. + ClearMediaAsExpired(item); + }, *lifetime); + + return false; + }); + } else { + setDocumentLinks(_data, realParent); + } } else { setDocumentLinks(_data, realParent); } @@ -918,7 +917,8 @@ void Document::draw( .highlight = highlightRequest ? &*highlightRequest : nullptr, }); } - if (_parent->data()->media() && _parent->data()->media()->ttlSeconds()) { + if ((_parent->data()->media() && _parent->data()->media()->ttlSeconds()) + && _openl) { const auto &fg = context.outbg ? st::historyFileOutIconFg : st::historyFileInIconFg; @@ -1739,4 +1739,23 @@ bool DrawThumbnailAsSongCover( return true; } +rpl::producer<> TTLVoiceStops(FullMsgId fullId) { + return rpl::merge( + ::Media::Player::instance()->updatedNotifier( + ) | rpl::filter([=](::Media::Player::TrackState state) { + using State = ::Media::Player::State; + const auto badState = state.state == State::Stopped + || state.state == State::StoppedAtEnd + || state.state == State::StoppedAtError + || state.state == State::StoppedAtStart; + return (state.id.contextId() != fullId) && !badState; + }) | rpl::to_empty, + ::Media::Player::instance()->tracksFinished( + ) | rpl::filter([=](AudioMsgId::Type type) { + return (type == AudioMsgId::Type::Voice); + }) | rpl::to_empty, + ::Media::Player::instance()->stops(AudioMsgId::Type::Voice) + ); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 5fa8dc1da..3d29651c4 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -181,4 +181,6 @@ bool DrawThumbnailAsSongCover( const QRect &rect, bool selected = false); +rpl::producer<> TTLVoiceStops(FullMsgId fullId); + } // namespace HistoryView