Added ability to reply with timecode of currently playing voice message.

This commit is contained in:
23rd
2026-03-31 11:33:17 +03:00
parent 94b8685f23
commit 73d0b4cb84
13 changed files with 266 additions and 0 deletions
+1
View File
@@ -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";
@@ -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));
});
}
}
}
};
@@ -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<QKeyEvent*>(e);
@@ -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;
@@ -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) {
@@ -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<QString> 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<QString> 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<Ui::PopupMenu*> menu) {
auto label = base::make_unique_q<Ui::Menu::MultilineAction>(
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
@@ -84,6 +84,9 @@ void AttachPollOptionTabs(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition);
[[nodiscard]] std::optional<QString> CurrentVoiceTimecode(FullMsgId itemId);
[[nodiscard]] rpl::producer<QString> VoiceTimecodeUpdates(FullMsgId itemId);
void AddPollActions(
not_null<Ui::PopupMenu*> menu,
not_null<PollData*> poll,
@@ -4522,6 +4522,16 @@ rpl::producer<FullMsgId> ListWidget::showMessageRequested() const {
return _requestedToShowMessage.events();
}
void ListWidget::setInsertTextCallback(Fn<void(QString)> 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) {
@@ -389,6 +389,8 @@ public:
bool forceAnotherChat = false);
[[nodiscard]] rpl::producer<FullMsgId> readMessageRequested() const;
[[nodiscard]] rpl::producer<FullMsgId> showMessageRequested() const;
void setInsertTextCallback(Fn<void(QString)> 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<ReplyToMessageRequest> _requestedToReplyToMessage;
rpl::event_stream<FullMsgId> _requestedToReadMessage;
rpl::event_stream<FullMsgId> _requestedToShowMessage;
Fn<void(QString)> _insertTextCallback;
rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;
[[nodiscard]] ElementOverlayHost &ensureOverlayHost();
@@ -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<Ui::Menu::Menu*> parent,
const style::Menu &st,
not_null<QAction*> action,
const style::icon *icon,
const QString &timecode,
rpl::producer<QString> updates);
private:
void paintEvent(QPaintEvent *e) override;
const style::icon *_replyIcon = nullptr;
QString _timecode;
};
TimecodeAction::TimecodeAction(
not_null<Ui::Menu::Menu*> parent,
const style::Menu &st,
not_null<QAction*> action,
const style::icon *icon,
const QString &timecode,
rpl::producer<QString> 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<Ui::InputField*> 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<QAction*> AddTimecodeAction(
not_null<Ui::PopupMenu*> menu,
const QString &timecode,
rpl::producer<QString> updates,
Fn<void()> callback) {
auto item = base::make_unique_q<TimecodeAction>(
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
@@ -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<Ui::InputField*> field,
const QString &text);
not_null<QAction*> AddTimecodeAction(
not_null<Ui::PopupMenu*> menu,
const QString &timecode,
rpl::producer<QString> updates,
Fn<void()> callback);
} // namespace Menu
+2
View File
@@ -808,6 +808,8 @@ popupMenuExpandedSeparator: PopupMenu(popupMenuWithIcons) {
}
}
menuTimecodeFont: font(7px semibold);
whoReadMenu: PopupMenu(popupMenuExpandedSeparator) {
scrollPadding: margins(0px, 6px, 0px, 4px);
maxHeight: 400px;
+2
View File
@@ -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