diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index eed73ee24..5d276d712 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4157,6 +4157,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_reply_cant_forward" = "Sorry, you can't reply to a message that was sent before the group was upgraded to a supergroup. Do you wish to forward it and add your comment?"; "lng_share_title" = "Share to"; +"lng_share_at_time_title" = "Share at {time} to"; "lng_share_copy_link" = "Copy share link"; "lng_share_confirm" = "Send"; "lng_share_wrong_user" = "This game was opened from a different user."; @@ -4280,7 +4281,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; "lng_mediaview_copy" = "Copy"; +"lng_mediaview_copy_frame" = "Copy Frame"; "lng_mediaview_forward" = "Forward"; +"lng_mediaview_share_at_time" = "Share at {time}"; "lng_mediaview_delete" = "Delete"; "lng_mediaview_save_to_profile" = "Post to Profile"; "lng_mediaview_pin_story_done" = "Story pinned"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a75d263c4..8378af367 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -142,6 +142,16 @@ void ShowChannelsLimitBox(not_null peer) { action.replaceMediaOf); } +[[nodiscard]] QString FormatVideoTimestamp(TimeId seconds) { + const auto minutes = seconds / 60; + const auto hours = minutes / 60; + return hours + ? u"%1h%2m%3s"_q.arg(hours).arg(minutes % 60).arg(seconds % 60) + : minutes + ? u"%1m%2s"_q.arg(minutes).arg(seconds % 60) + : QString::number(seconds); +} + } // namespace ApiWrap::ApiWrap(not_null session) @@ -717,7 +727,8 @@ void ApiWrap::finalizeMessageDataRequest( QString ApiWrap::exportDirectMessageLink( not_null item, bool inRepliesContext, - bool forceNonPublicLink) { + bool forceNonPublicLink, + std::optional videoTimestamp) { Expects(item->history()->peer->isChannel()); const auto itemId = item->fullId(); @@ -790,7 +801,14 @@ QString ApiWrap::exportDirectMessageLink( _unlikelyMessageLinks.emplace_or_assign(itemId, link); } }).send(); - return current; + const auto addTimestamp = channel->hasUsername() + && !inRepliesContext + && videoTimestamp.has_value(); + const auto addedSeparator = (current.indexOf('?') >= 0) ? '&' : '?'; + const auto addedTimestamp = addTimestamp + ? (addedSeparator + u"t="_q + FormatVideoTimestamp(*videoTimestamp)) + : QString(); + return current + addedTimestamp; } QString ApiWrap::exportDirectStoryLink(not_null story) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index cfbb71256..d6e2bbc67 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -166,7 +166,8 @@ public: QString exportDirectMessageLink( not_null item, bool inRepliesContext, - bool forceNonPublicLink = false); + bool forceNonPublicLink = false, + std::optional videoTimestamp = {}); QString exportDirectStoryLink(not_null item); void requestContacts(); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 2d80be497..437d7591b 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -277,7 +277,9 @@ void ShareBox::prepare() { _select->resizeToWidth(st::boxWideWidth); Ui::SendPendingMoveResizeEvents(_select); - setTitle(tr::lng_share_title()); + setTitle(_descriptor.titleOverride + ? std::move(_descriptor.titleOverride) + : tr::lng_share_title()); _inner = setInnerWidget( object_ptr(this, _descriptor, uiShow()), @@ -1495,7 +1497,8 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs( ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( std::shared_ptr show, not_null history, - MessageIdsList msgIds) { + MessageIdsList msgIds, + std::optional videoTimestamp) { struct State final { base::flat_set requests; }; @@ -1531,6 +1534,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( : Flag(0)) | ((forwardOptions == Data::ForwardOptions::NoNamesAndCaptions) ? Flag::f_drop_media_captions + : Flag(0)) + | (videoTimestamp.has_value() + ? Flag::f_video_timestamp : Flag(0)); auto mtpMsgIds = QVector(); mtpMsgIds.reserve(existingIds.size()); @@ -1588,7 +1594,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), - MTPint() // video_timestamp + MTP_int(videoTimestamp.value_or(0)) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index f21b4505b..779a60e0a 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -104,7 +104,8 @@ public: [[nodiscard]] static SubmitCallback DefaultForwardCallback( std::shared_ptr show, not_null history, - MessageIdsList msgIds); + MessageIdsList msgIds, + std::optional videoTimestamp = {}); struct Descriptor { not_null session; @@ -113,7 +114,9 @@ public: FilterCallback filterCallback; object_ptr bottomWidget = { nullptr }; rpl::producer copyLinkText; + rpl::producer titleOverride; ShareBoxStyleOverrides st; + std::optional videoTimestamp; struct { int sendersCount = 0; int captionsCount = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index a23f102a7..678f59ac9 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1295,25 +1295,28 @@ base::unique_qptr FillContextMenu( void CopyPostLink( not_null controller, FullMsgId itemId, - Context context) { - CopyPostLink(controller->uiShow(), itemId, context); + Context context, + std::optional videoTimestamp) { + CopyPostLink(controller->uiShow(), itemId, context, videoTimestamp); } void CopyPostLink( std::shared_ptr show, FullMsgId itemId, - Context context) { + Context context, + std::optional videoTimestamp) { const auto item = show->session().data().message(itemId); if (!item || !item->hasDirectLink()) { return; } const auto inRepliesContext = (context == Context::Replies); - const auto forceNonPublicLink = base::IsCtrlPressed(); + const auto forceNonPublicLink = !videoTimestamp && base::IsCtrlPressed(); QGuiApplication::clipboard()->setText( item->history()->session().api().exportDirectMessageLink( item, inRepliesContext, - forceNonPublicLink)); + forceNonPublicLink, + videoTimestamp)); const auto isPublicLink = [&] { if (forceNonPublicLink) { @@ -1334,7 +1337,7 @@ void CopyPostLink( } return channel->hasUsername(); }(); - if (isPublicLink) { + if (isPublicLink && !videoTimestamp) { show->showToast({ .text = tr::lng_channel_public_link_copied( tr::now, Ui::Text::Bold diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 1dac246bb..0e14fa803 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -60,11 +60,13 @@ base::unique_qptr FillContextMenu( void CopyPostLink( not_null controller, FullMsgId itemId, - Context context); + Context context, + std::optional videoTimestamp = {}); void CopyPostLink( std::shared_ptr show, FullMsgId itemId, - Context context); + Context context, + std::optional videoTimestamp = {}); void CopyStoryLink( std::shared_ptr show, FullStoryId storyId); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 02b379ed0..67b1dc925 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" @@ -181,4 +182,83 @@ namespace Media::Stories { }); } +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 PrepareShareAtTimeBox( + std::shared_ptr show, + not_null 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 thread) { + if (const auto user = thread->peer()->asUser()) { + if (user->canSendIgnoreRequirePremium()) { + return true; + } + } + return Data::CanSend(thread, requiredRight) + && (!requiresInline + || Data::CanSend(thread, ChatRestriction::SendInline)); + }; + auto copyLinkCallback = canCopyLink + ? Fn(std::move(copyCallback)) + : Fn(); + const auto st = ::Settings::DarkCreditsEntryBoxStyle(); + return Box(ShareBox::Descriptor{ + .session = session, + .copyCallback = std::move(copyLinkCallback), + .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, + }, + .premiumRequiredError = SharePremiumRequiredError(), + }); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.h b/Telegram/SourceFiles/media/stories/media_stories_share.h index efb633930..c7149b0ca 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.h +++ b/Telegram/SourceFiles/media/stories/media_stories_share.h @@ -24,4 +24,11 @@ namespace Media::Stories { FullStoryId id, bool viewerStyle = false); +[[nodiscard]] QString FormatShareAtTime(TimeId seconds); + +[[nodiscard]] object_ptr PrepareShareAtTimeBox( + std::shared_ptr show, + not_null item, + TimeId videoTimestamp); + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 0df0d0777..a3d6251fc 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_attached_stickers.h" #include "api/api_peer_photo.h" #include "base/qt/qt_common_adapters.h" +#include "base/timer_rpl.h" #include "lang/lang_keys.h" #include "menu/menu_sponsored.h" #include "boxes/premium_preview_box.h" @@ -50,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_pip.h" #include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_opengl.h" +#include "media/stories/media_stories_share.h" #include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_player.h" @@ -1646,7 +1648,9 @@ void OverlayWidget::fillContextMenuActions( if (!hasCopyMediaRestriction()) { if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) { addAction( - tr::lng_mediaview_copy(tr::now), + ((_document && _streamed) + ? tr::lng_mediaview_copy_frame(tr::now) + : tr::lng_mediaview_copy(tr::now)), [=] { copyMedia(); }, &st::mediaMenuIconCopy); } @@ -1663,6 +1667,31 @@ void OverlayWidget::fillContextMenuActions( tr::lng_mediaview_forward(tr::now), [=] { forwardMedia(); }, &st::mediaMenuIconForward); + if (canShareAtTime()) { + const auto now = [=] { + return tr::lng_mediaview_share_at_time( + tr::now, + lt_time, + Stories::FormatShareAtTime(shareAtVideoTimestamp())); + }; + const auto action = addAction( + now(), + [=] { shareAtTime(); }, + &st::mediaMenuIconShare); + struct State { + rpl::variable text; + rpl::lifetime lifetime; + }; + const auto state = Ui::CreateChild(action); + state->text = rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(120) + ) | rpl::map(now); + state->text.changes() | rpl::start_with_next([=](QString value) { + action->setText(value); + }, state->lifetime); + } } if (story && story->canShare()) { addAction(tr::lng_mediaview_forward(tr::now), [=] { @@ -2638,6 +2667,33 @@ void OverlayWidget::handleDocumentClick() { } } +bool OverlayWidget::canShareAtTime() const { + const auto media = _message ? _message->media() : nullptr; + return _document + && media + && _streamed + && (_document == media->document()) + && _document->isVideoFile() + && !media->webpage(); +} + +TimeId OverlayWidget::shareAtVideoTimestamp() const { + return _streamedPosition / crl::time(1000); +} + +void OverlayWidget::shareAtTime() { + if (!canShareAtTime()) { + return; + } + if (!_streamed->instance.player().paused() + && !_streamed->instance.player().finished()) { + playbackPauseResume(); + } + const auto show = uiShow(); + const auto timestamp = shareAtVideoTimestamp(); + show->show(Stories::PrepareShareAtTimeBox(show, _message, timestamp)); +} + void OverlayWidget::downloadMedia() { if (!_photo && !_document) { return; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 1a5064cfe..82c7163e6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -291,6 +291,10 @@ private: void handleTouchTimer(); void handleDocumentClick(); + [[nodiscard]] bool canShareAtTime() const; + [[nodiscard]] TimeId shareAtVideoTimestamp() const; + void shareAtTime(); + void showSaveMsgToast(const QString &path, auto phrase); void showSaveMsgToastWith( const QString &path, diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 74d0a795f..504065677 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -200,6 +200,7 @@ mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }}; mediaMenuIconDownloadLocked: icon {{ "menu/download_locked", mediaviewMenuFg }}; mediaMenuIconCopy: icon {{ "menu/copy", mediaviewMenuFg }}; mediaMenuIconForward: icon {{ "menu/forward", mediaviewMenuFg }}; +mediaMenuIconShare: icon {{ "menu/share2", mediaviewMenuFg }}; mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }}; mediaMenuIconShowAll: icon {{ "menu/all_media", mediaviewMenuFg }}; mediaMenuIconProfile: icon {{ "menu/profile", mediaviewMenuFg }};