diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 02cb6def36..4a1e9ca75d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5003,6 +5003,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_to_msg" = "Go To Message"; "lng_context_reply_msg" = "Reply"; "lng_context_quote_and_reply" = "Quote & Reply"; +"lng_context_reply_with_timecode" = "Reply with timecode"; "lng_context_reply_to_task" = "Reply to Task"; "lng_context_reply_to_poll_option" = "Reply to Option"; "lng_context_copy_poll_option" = "Copy Option"; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index c2278d5f67..1a68d3d36e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -88,6 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_item_download_files.h" #include "menu/menu_item_rate_transcribe.h" #include "menu/menu_item_rate_transcribe_session.h" +#include "menu/menu_timecode_action.h" #include "menu/menu_sponsored.h" #include "core/application.h" #include "apiwrap.h" @@ -2963,6 +2964,24 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _widget->clearSelected(); } }, &st::menuIconReply); + const auto media = item->media(); + const auto document = media + ? media->document() + : nullptr; + if (canSendReply && document && document->isVoiceMessage()) { + const auto msgId = item->fullId(); + if (const auto t = HistoryView::CurrentVoiceTimecode(msgId)) { + Menu::AddTimecodeAction( + _menu.get(), + *t, + HistoryView::VoiceTimecodeUpdates(msgId), + [=] { + const auto cur + = HistoryView::CurrentVoiceTimecode(msgId); + _widget->insertTextAtCursor(cur.value_or(*t)); + }); + } + } } }; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index aa64b1282e..5deab81ab1 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -142,6 +142,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/bot_keyboard.h" #include "chat_helpers/message_field.h" #include "menu/menu_send.h" +#include "menu/menu_timecode_action.h" #include "mtproto/mtproto_config.h" #include "lang/lang_keys.h" #include "settings/business/settings_quick_replies.h" @@ -5677,6 +5678,13 @@ bool HistoryWidget::insertBotCommand(const QString &cmd) { return false; } +void HistoryWidget::insertTextAtCursor(const QString &text) { + if (!_canSendTexts) { + return; + } + Menu::InsertTextAtCursor(_field, text); +} + bool HistoryWidget::eventFilter(QObject *obj, QEvent *e) { if (e->type() == QEvent::KeyPress) { const auto k = static_cast(e); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 755ed32866..0fec3cff7b 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -242,6 +242,7 @@ public: void sendBotCommand(const Bot::SendCommandRequest &request); void hideSingleUseKeyboard(FullMsgId replyToId); bool insertBotCommand(const QString &cmd); + void insertTextAtCursor(const QString &text); bool eventFilter(QObject *obj, QEvent *e) override; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 45a5599e14..9f040afc27 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -62,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/mime_type.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "menu/menu_timecode_action.h" #include "data/components/scheduled_messages.h" #include "data/data_histories.h" #include "data/data_saved_messages.h" @@ -395,6 +396,12 @@ ChatWidget::ChatWidget( } }, _inner->lifetime()); + _inner->setInsertTextCallback([=](const QString &text) { + if (const auto field = _composeControls->fieldForMention()) { + Menu::InsertTextAtCursor(field, text); + } + }); + _composeControls->sendActionUpdates( ) | rpl::on_next([=](ComposeControls::SendActionUpdate &&data) { if (!_repliesRootId) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 04ca626c27..e325fdbb66 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -52,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_item_download_files.h" #include "menu/menu_item_rate_transcribe.h" #include "menu/menu_item_rate_transcribe_session.h" +#include "menu/menu_timecode_action.h" #include "menu/menu_send.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/show_or_premium_box.h" @@ -94,6 +95,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "media/audio/media_audio.h" +#include "media/player/media_player_instance.h" #include "spellcheck/spellcheck_types.h" #include "apiwrap.h" #include "styles/style_chat.h" @@ -1275,6 +1278,30 @@ void ShowWhoReadInfo( } // namespace +std::optional CurrentVoiceTimecode(FullMsgId itemId) { + const auto state = ::Media::Player::instance()->getState( + AudioMsgId::Type::Voice); + if (state.id.contextId() == itemId + && !::Media::Player::IsStoppedOrStopping(state.state) + && state.frequency > 0) { + return Ui::FormatDurationText(state.position / state.frequency); + } + return std::nullopt; +} + +rpl::producer VoiceTimecodeUpdates(FullMsgId itemId) { + return ::Media::Player::instance()->updatedNotifier( + ) | rpl::filter([=](const ::Media::Player::TrackState &state) { + return (state.id.type() == AudioMsgId::Type::Voice) + && (state.id.contextId() == itemId); + }) | rpl::filter([](const ::Media::Player::TrackState &state) { + return !::Media::Player::IsStoppedOrStopping(state.state) + && state.frequency > 0; + }) | rpl::map([](const ::Media::Player::TrackState &state) { + return Ui::FormatDurationText(state.position / state.frequency); + }) | rpl::distinct_until_changed(); +} + void InsertPollHiddenResultsLabel(not_null menu) { auto label = base::make_unique_q( menu->menu(), @@ -1324,6 +1351,32 @@ void FillContextMenuItems( && Api::WhoReactedExists(item, Api::WhoReactedList::All); AddReplyToMessageAction(result, request, list); + if (item) { + const auto media = item->media(); + const auto document = media ? media->document() : nullptr; + const auto topic = item->topic(); + const auto peer = item->history()->peer.get(); + const auto canSendText = topic + ? Data::CanSendAnything(topic) + : Data::CanSendAnything(peer); + if (canSendText && document && document->isVoiceMessage()) { + const auto msgId = item->fullId(); + if (const auto timecode = CurrentVoiceTimecode(msgId)) { + const auto weak = base::make_weak(list.get()); + Menu::AddTimecodeAction( + result, + *timecode, + VoiceTimecodeUpdates(msgId), + [=] { + const auto tc = CurrentVoiceTimecode(msgId); + if (const auto strong = weak.get()) { + strong->insertTextAtCursor( + tc.value_or(*timecode)); + } + }); + } + } + } AddTodoListAction(result, request, list); if (request.overSelection diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 3ddb3e1b97..c159de227d 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -84,6 +84,9 @@ void AttachPollOptionTabs( not_null menu, QPoint desiredPosition); +[[nodiscard]] std::optional CurrentVoiceTimecode(FullMsgId itemId); +[[nodiscard]] rpl::producer VoiceTimecodeUpdates(FullMsgId itemId); + void AddPollActions( not_null menu, not_null poll, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index cdc1840367..2055c8396d 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -4522,6 +4522,16 @@ rpl::producer ListWidget::showMessageRequested() const { return _requestedToShowMessage.events(); } +void ListWidget::setInsertTextCallback(Fn callback) { + _insertTextCallback = std::move(callback); +} + +void ListWidget::insertTextAtCursor(const QString &text) { + if (_insertTextCallback) { + _insertTextCallback(text); + } +} + void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { const auto reply = [&](Element *view) { if (view) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index e577b46630..effc5f36c0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -389,6 +389,8 @@ public: bool forceAnotherChat = false); [[nodiscard]] rpl::producer readMessageRequested() const; [[nodiscard]] rpl::producer showMessageRequested() const; + void setInsertTextCallback(Fn callback); + void insertTextAtCursor(const QString &text); void replyNextMessage(FullMsgId fullId, bool next = true); [[nodiscard]] Reactions::ButtonParameters reactionButtonParameters( @@ -899,6 +901,7 @@ private: rpl::event_stream _requestedToReplyToMessage; rpl::event_stream _requestedToReadMessage; rpl::event_stream _requestedToShowMessage; + Fn _insertTextCallback; rpl::event_stream> _scrollKeyEvents; [[nodiscard]] ElementOverlayHost &ensureOverlayHost(); diff --git a/Telegram/SourceFiles/menu/menu_timecode_action.cpp b/Telegram/SourceFiles/menu/menu_timecode_action.cpp new file mode 100644 index 0000000000..06f166111c --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_timecode_action.cpp @@ -0,0 +1,126 @@ +/* +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 "menu/menu_timecode_action.h" + +#include "base/unique_qptr.h" +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "ui/text/format_values.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/menu/menu_common.h" +#include "ui/widgets/popup_menu.h" + +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" + +namespace { + +class TimecodeAction final : public Ui::Menu::Action { +public: + TimecodeAction( + not_null parent, + const style::Menu &st, + not_null action, + const style::icon *icon, + const QString &timecode, + rpl::producer updates); + +private: + void paintEvent(QPaintEvent *e) override; + + const style::icon *_replyIcon = nullptr; + QString _timecode; + +}; + +TimecodeAction::TimecodeAction( + not_null parent, + const style::Menu &st, + not_null action, + const style::icon *icon, + const QString &timecode, + rpl::producer updates) +: Ui::Menu::Action(parent, st, action, nullptr, nullptr) +, _replyIcon(icon) +, _timecode(timecode) { + std::move(updates) | rpl::on_next([=](const QString &value) { + _timecode = value; + update(); + }, lifetime()); +} + +void TimecodeAction::paintEvent(QPaintEvent *e) { + Ui::Menu::Action::paintEvent(e); + + auto p = Painter(this); + if (_replyIcon) { + const auto pos = st().itemIconPosition; + const auto iconW = _replyIcon->width(); + constexpr auto kScale = 0.8; + const auto dx = pos.x() + (iconW * (1. - kScale)) / 2.; + const auto dy = pos.y(); + auto hq = PainterHighQualityEnabler(p); + p.translate(dx, dy); + p.scale(kScale, kScale); + _replyIcon->paint(p, 0, 0, width() / kScale); + p.resetTransform(); + } + const auto &font = st::menuTimecodeFont; + p.setFont(font); + p.setPen(st::menuIconColor); + const auto iconRight = st().itemIconPosition.x() + + st::menuIconReply.width(); + const auto textWidth = font->width(_timecode); + const auto x = iconRight - textWidth; + const auto y = st().itemIconPosition.y() + + st::menuIconReply.height() + - font->descent; + p.drawText(x, y, _timecode); +} + +} // namespace + +namespace Menu { + +void InsertTextAtCursor( + not_null field, + const QString &text) { + auto cursor = field->textCursor(); + const auto pos = cursor.position(); + const auto doc = field->getTextWithTags().text; + const auto needSpaceBefore = pos > 0 + && !doc[pos - 1].isSpace(); + auto insert = QString(); + if (needSpaceBefore) { + insert += ' '; + } + insert += text + ' '; + cursor.insertText(insert); + field->setTextCursor(cursor); +} + +not_null AddTimecodeAction( + not_null menu, + const QString &timecode, + rpl::producer updates, + Fn callback) { + auto item = base::make_unique_q( + menu->menu(), + menu->st().menu, + Ui::Menu::CreateAction( + menu->menu().get(), + tr::lng_context_reply_with_timecode(tr::now), + std::move(callback)), + &st::menuIconReply, + timecode, + std::move(updates)); + return menu->addAction(std::move(item)); +} + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_timecode_action.h b/Telegram/SourceFiles/menu/menu_timecode_action.h new file mode 100644 index 0000000000..ac8b77cdd9 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_timecode_action.h @@ -0,0 +1,31 @@ +/* +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/basic_types.h" + +class QAction; + +namespace Ui { +class InputField; +class PopupMenu; +} // namespace Ui + +namespace Menu { + +void InsertTextAtCursor( + not_null field, + const QString &text); + +not_null AddTimecodeAction( + not_null menu, + const QString &timecode, + rpl::producer updates, + Fn callback); + +} // namespace Menu diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d6314a68a1..6a6808299e 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -808,6 +808,8 @@ popupMenuExpandedSeparator: PopupMenu(popupMenuWithIcons) { } } +menuTimecodeFont: font(7px semibold); + whoReadMenu: PopupMenu(popupMenuExpandedSeparator) { scrollPadding: margins(0px, 6px, 0px, 4px); maxHeight: 400px; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 1d7b748d10..49197d8c25 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -225,6 +225,8 @@ PRIVATE menu/menu_check_item.h menu/menu_item_rate_transcribe.cpp menu/menu_item_rate_transcribe.h + menu/menu_timecode_action.cpp + menu/menu_timecode_action.h menu/menu_ttl.cpp menu/menu_ttl.h