Allow sharing video at a timestamp.

This commit is contained in:
John Preston 2025-01-28 14:11:21 +04:00
parent 2077f51084
commit 999a13358e
12 changed files with 200 additions and 16 deletions

View file

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

View file

@ -142,6 +142,16 @@ void ShowChannelsLimitBox(not_null<PeerData*> 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<Main::Session*> session)
@ -717,7 +727,8 @@ void ApiWrap::finalizeMessageDataRequest(
QString ApiWrap::exportDirectMessageLink(
not_null<HistoryItem*> item,
bool inRepliesContext,
bool forceNonPublicLink) {
bool forceNonPublicLink,
std::optional<TimeId> 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<Data::Story*> story) {

View file

@ -166,7 +166,8 @@ public:
QString exportDirectMessageLink(
not_null<HistoryItem*> item,
bool inRepliesContext,
bool forceNonPublicLink = false);
bool forceNonPublicLink = false,
std::optional<TimeId> videoTimestamp = {});
QString exportDirectStoryLink(not_null<Data::Story*> item);
void requestContacts();

View file

@ -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<Inner>(this, _descriptor, uiShow()),
@ -1495,7 +1497,8 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
MessageIdsList msgIds) {
MessageIdsList msgIds,
std::optional<TimeId> videoTimestamp) {
struct State final {
base::flat_set<mtpRequestId> 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<MTPint>();
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);

View file

@ -104,7 +104,8 @@ public:
[[nodiscard]] static SubmitCallback DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
MessageIdsList msgIds);
MessageIdsList msgIds,
std::optional<TimeId> videoTimestamp = {});
struct Descriptor {
not_null<Main::Session*> session;
@ -113,7 +114,9 @@ public:
FilterCallback filterCallback;
object_ptr<Ui::RpWidget> bottomWidget = { nullptr };
rpl::producer<QString> copyLinkText;
rpl::producer<QString> titleOverride;
ShareBoxStyleOverrides st;
std::optional<TimeId> videoTimestamp;
struct {
int sendersCount = 0;
int captionsCount = 0;

View file

@ -1295,25 +1295,28 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
void CopyPostLink(
not_null<Window::SessionController*> controller,
FullMsgId itemId,
Context context) {
CopyPostLink(controller->uiShow(), itemId, context);
Context context,
std::optional<TimeId> videoTimestamp) {
CopyPostLink(controller->uiShow(), itemId, context, videoTimestamp);
}
void CopyPostLink(
std::shared_ptr<Main::SessionShow> show,
FullMsgId itemId,
Context context) {
Context context,
std::optional<TimeId> 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

View file

@ -60,11 +60,13 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
void CopyPostLink(
not_null<Window::SessionController*> controller,
FullMsgId itemId,
Context context);
Context context,
std::optional<TimeId> videoTimestamp = {});
void CopyPostLink(
std::shared_ptr<Main::SessionShow> show,
FullMsgId itemId,
Context context);
Context context,
std::optional<TimeId> videoTimestamp = {});
void CopyStoryLink(
std::shared_ptr<Main::SessionShow> show,
FullStoryId storyId);

View file

@ -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<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->canSendIgnoreRequirePremium()) {
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),
.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

View file

@ -24,4 +24,11 @@ namespace Media::Stories {
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

@ -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<QString> text;
rpl::lifetime lifetime;
};
const auto state = Ui::CreateChild<State>(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;

View file

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

View file

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