diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp index 4bdbdf865b..f22c2436ef 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp @@ -22,33 +22,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_web_page.h" #include "facades.h" +#include "boxes/confirm_box.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_webpage_preview.h" #include "inline_bots/inline_results_widget.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "media/audio/media_audio_capture.h" +#include "media/audio/media_audio.h" #include "styles/style_history.h" #include "ui/special_buttons.h" #include "ui/text_options.h" #include "ui/ui_utility.h" #include "ui/widgets/input_fields.h" #include "window/window_session_controller.h" +#include "mainwindow.h" namespace HistoryView { - namespace { -using FileChosen = ComposeControls::FileChosen; -using PhotoChosen = ComposeControls::PhotoChosen; -using MessageToEdit = ComposeControls::MessageToEdit; - -constexpr auto kMouseEvent = { +constexpr auto kRecordingUpdateDelta = crl::time(100); +constexpr auto kMouseEvents = { QEvent::MouseMove, QEvent::MouseButtonPress, QEvent::MouseButtonRelease }; +using FileChosen = ComposeControls::FileChosen; +using PhotoChosen = ComposeControls::PhotoChosen; +using MessageToEdit = ComposeControls::MessageToEdit; +using VoiceToSend = ComposeControls::VoiceToSend; +using SendActionUpdate = ComposeControls::SendActionUpdate; + [[nodiscard]] auto ShowWebPagePreview(WebPageData *page) { return page && (page->pendingTill >= 0); } @@ -223,7 +229,7 @@ void FieldHeader::init() { const auto inClickable = lifetime().make_state(false); events( ) | rpl::filter([=](not_null event) { - return ranges::contains(kMouseEvent, event->type()) + return ranges::contains(kMouseEvents, event->type()) && isEditingMessage(); }) | rpl::start_with_next([=](not_null event) { const auto type = event->type(); @@ -502,11 +508,18 @@ ComposeControls::ComposeControls( , _header(std::make_unique( _wrap.get(), &_window->session().data())) -, _textUpdateEvents(TextUpdateEvent::SendTyping) { +, _textUpdateEvents(TextUpdateEvent::SendTyping) +, _recordCancelWidth(st::historyRecordFont->width(tr::lng_record_cancel(tr::now))) +, _recordingAnimation([=](crl::time now) { + return recordingAnimationCallback(now); +}) { init(); } ComposeControls::~ComposeControls() { + if (_recording) { + stopRecording(false); + } setTabbedPanel(nullptr); } @@ -541,8 +554,89 @@ int ComposeControls::heightCurrent() const { return _wrap->height(); } -void ComposeControls::focus() { +bool ComposeControls::focus() { + if (_recording) { + return false; + } _field->setFocus(); + return true; +} + +void ComposeControls::updateControlsVisibility() { + if (_recording) { + _field->hide(); + _tabbedSelectorToggle->hide(); + //_botKeyboardShow->hide(); + //_botKeyboardHide->hide(); + //_botCommandStart->hide(); + _attachToggle->hide(); + //if (_silent) { + // _silent->hide(); + //} + //if (_scheduled) { + // _scheduled->hide(); + //} + //if (_kbShown) { + // _kbScroll->show(); + //} else { + // _kbScroll->hide(); + //} + } else { + _field->show(); + //if (_kbShown) { + // _kbScroll->show(); + // _tabbedSelectorToggle->hide(); + // _botKeyboardHide->show(); + // _botKeyboardShow->hide(); + // _botCommandStart->hide(); + //} else if (_kbReplyTo) { + // _kbScroll->hide(); + // _tabbedSelectorToggle->show(); + // _botKeyboardHide->hide(); + // _botKeyboardShow->hide(); + // _botCommandStart->hide(); + //} else { + // _kbScroll->hide(); + // _tabbedSelectorToggle->show(); + // _botKeyboardHide->hide(); + // if (_keyboard->hasMarkup()) { + // _botKeyboardShow->show(); + // _botCommandStart->hide(); + // } else { + // _botKeyboardShow->hide(); + // if (_cmdStartShown) { + // _botCommandStart->show(); + // } else { + // _botCommandStart->hide(); + // } + // } + //} + _attachToggle->show(); + //if (_silent) { + // _silent->show(); + //} + //if (_scheduled) { + // _scheduled->show(); + //} + //updateFieldPlaceholder(); + } + +} + +bool ComposeControls::recordingAnimationCallback(crl::time now) { + const auto dt = anim::Disabled() + ? 1. + : ((now - _recordingAnimation.started()) + / float64(kRecordingUpdateDelta)); + if (dt >= 1.) { + _recordingLevel.finish(); + } else { + _recordingLevel.update(dt, anim::linear); + } + if (!anim::Disabled()) { + _wrap->update(_attachToggle->geometry()); + } + return (dt < 1.); } rpl::producer<> ComposeControls::cancelRequests() const { @@ -573,6 +667,10 @@ rpl::producer<> ComposeControls::sendRequests() const { std::move(submits) | filter | rpl::to_empty); } +rpl::producer ComposeControls::sendVoiceRequests() const { + return _sendVoiceRequests.events(); +} + rpl::producer ComposeControls::editRequests() const { auto toValue = rpl::map([=] { return _header->queryToEdit(); }); auto filter = rpl::filter([=] { @@ -668,6 +766,25 @@ void ComposeControls::init() { initTabbedSelector(); initSendButton(); + QObject::connect( + ::Media::Capture::instance(), + &::Media::Capture::Instance::error, + _wrap.get(), + [=] { recordError(); }); + QObject::connect( + ::Media::Capture::instance(), + &::Media::Capture::Instance::updated, + _wrap.get(), + [=](quint16 level, int samples) { recordUpdated(level, samples); }); + qRegisterMetaType(); + QObject::connect( + ::Media::Capture::instance(), + &::Media::Capture::Instance::done, + _wrap.get(), + [=](QByteArray result, VoiceWaveform waveform, int samples) { + recordDone(result, waveform, samples); + }); + _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { updateControlsGeometry(size); @@ -686,7 +803,7 @@ void ComposeControls::init() { _header->editMsgId( ) | rpl::start_with_next([=](const auto &id) { if (_header->isEditingMessage()) { - setTextFromEditingMessage(_window->session().data().message(id)); + setTextFromEditingMessage(session().data().message(id)); } else { setText(_localSavedText); _localSavedText = {}; @@ -709,7 +826,7 @@ void ComposeControls::init() { *lastMsgId = id; }, _wrap->lifetime()); - _window->session().data().itemRemoved( + session().data().itemRemoved( ) | rpl::filter([=](not_null item) { return item->id && ((*lastMsgId) == item->fullId()); }) | rpl::start_with_next([=] { @@ -718,6 +835,120 @@ void ComposeControls::init() { } } +void ComposeControls::recordError() { + stopRecording(false); +} + +void ComposeControls::recordDone( + QByteArray result, + VoiceWaveform waveform, + int samples) { + if (result.isEmpty()) { + return; + } + + Window::ActivateWindow(_window); + const auto duration = samples / ::Media::Player::kDefaultFrequency; + _sendVoiceRequests.fire({ result, waveform, duration }); +} + +void ComposeControls::recordUpdated(quint16 level, int samples) { + if (!_recording) { + return; + } + + _recordingLevel.start(level); + _recordingAnimation.start(); + _recordingSamples = samples; + if (samples < 0 || samples >= ::Media::Player::kDefaultFrequency * AudioVoiceMsgMaxLength) { + stopRecording(samples > 0 && _inField); + } + Core::App().updateNonIdle(); + _wrap->update(); + _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice }); +} + +void ComposeControls::recordStartCallback() { + //const auto error = _peer // #TODO restrictions + // ? Data::RestrictionError(_peer, ChatRestriction::f_send_media) + // : std::nullopt; + const auto error = std::optional(); + if (error) { + Ui::show(Box(*error)); + return; + //} else if (showSlowmodeError()) { // #TODO slowmode + // return; + } else if (!::Media::Capture::instance()->available()) { + return; + } + + emit ::Media::Capture::instance()->start(); + + _recording = _inField = true; + updateControlsVisibility(); + _window->widget()->setInnerFocus(); + + _wrap->update(); + + _send->setRecordActive(true); +} + +void ComposeControls::recordStopCallback(bool active) { + stopRecording(active); +} + +void ComposeControls::recordUpdateCallback(QPoint globalPos) { + updateOverStates(_wrap->mapFromGlobal(globalPos)); +} + +void ComposeControls::stopRecording(bool send) { + emit ::Media::Capture::instance()->stop(send); + + _recordingLevel = anim::value(); + _recordingAnimation.stop(); + + _recording = false; + _recordingSamples = 0; + _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); + + updateControlsVisibility(); + _window->widget()->setInnerFocus(); + + _wrap->update(); + _send->setRecordActive(false); +} + +bool ComposeControls::showRecordButton() const { + return ::Media::Capture::instance()->available() + && !HasSendText(_field) + //&& !readyToForward() + && !isEditingMessage(); +} + +void ComposeControls::drawRecording(Painter &p, float64 recordActive) { + p.setPen(Qt::NoPen); + p.setBrush(st::historyRecordSignalColor); + + auto delta = qMin(_recordingLevel.current() / 0x4000, 1.); + auto d = 2 * qRound(st::historyRecordSignalMin + (delta * (st::historyRecordSignalMax - st::historyRecordSignalMin))); + { + PainterHighQualityEnabler hq(p); + p.drawEllipse(_attachToggle->x() + (_tabbedSelectorToggle->width() - d) / 2, _attachToggle->y() + (_attachToggle->height() - d) / 2, d, d); + } + + auto duration = formatDurationText(_recordingSamples / ::Media::Player::kDefaultFrequency); + p.setFont(st::historyRecordFont); + + p.setPen(st::historyRecordDurationFg); + p.drawText(_attachToggle->x() + _tabbedSelectorToggle->width(), _attachToggle->y() + st::historyRecordTextTop + st::historyRecordFont->ascent, duration); + + int32 left = _attachToggle->x() + _tabbedSelectorToggle->width() + st::historyRecordFont->width(duration) + ((_send->width() - st::historyRecordVoice.width()) / 2); + int32 right = _wrap->width() - _send->width(); + + p.setPen(anim::pen(st::historyRecordCancel, st::historyRecordCancelActive, 1. - recordActive)); + p.drawText(left + (right - left - _recordCancelWidth) / 2, _attachToggle->y() + st::historyRecordTextTop + st::historyRecordFont->ascent, tr::lng_record_cancel(tr::now)); +} + void ComposeControls::setTextFromEditingMessage(not_null item) { if (!_header->isEditingMessage()) { return; @@ -752,14 +983,15 @@ void ComposeControls::fieldChanged() { if (/*!_inlineBot && */!_header->isEditingMessage() && (_textUpdateEvents & TextUpdateEvent::SendTyping)) { - _sendActionUpdates.fire(Api::SendProgress{ - Api::SendProgressType::Typing, - crl::now() + 5 * crl::time(1000), - }); + _sendActionUpdates.fire({ Api::SendProgressType::Typing }); + } + updateSendButtonType(); + if (showRecordButton()) { + //_previewCancelled = false; } } -rpl::producer ComposeControls::sendActionUpdates() const { +rpl::producer ComposeControls::sendActionUpdates() const { return _sendActionUpdates.events(); } @@ -811,8 +1043,8 @@ void ComposeControls::updateSendButtonType() { return Type::Save; //} else if (_isInlineBot) { // return Type::Cancel; - //} else if (showRecordButton()) { - // return Type::Record; + } else if (showRecordButton()) { + return Type::Record; } return (_mode == Mode::Normal) ? Type::Send : Type::Schedule; }(); @@ -835,6 +1067,11 @@ void ComposeControls::updateSendButtonType() { // this, // [=] { updateSendButtonType(); }); //} + + _send->setRecordStartCallback([=] { recordStartCallback(); }); + _send->setRecordStopCallback([=](bool active) { recordStopCallback(active); }); + _send->setRecordUpdateCallback([=](QPoint globalPos) { recordUpdateCallback(globalPos); }); + _send->setRecordAnimationCallback([=] { _wrap->update(); }); } void ComposeControls::updateControlsGeometry(QSize size) { @@ -879,10 +1116,26 @@ void ComposeControls::updateOuterGeometry(QRect rect) { } } +void ComposeControls::updateOverStates(QPoint pos) { + const auto inField = _wrap->rect().contains(pos); + if (inField != _inField && _recording) { + _inField = inField; + _send->setRecordActive(_inField); + } +} + void ComposeControls::paintBackground(QRect clip) { Painter p(_wrap.get()); p.fillRect(clip, st::historyComposeAreaBg); + if (!_field->isHidden() || _recording) { + //drawField(p, clip); + if (!_send->isHidden() && _recording) { + drawRecording(p, _send->recordActiveRatio()); + } + //} else if (const auto error = writeRestriction()) { + // drawRestrictedWrite(p, *error); + } } void ComposeControls::escape() { diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/history_view_compose_controls.h index 45697979ae..2fd4214df4 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.h @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "api/api_common.h" -#include "api/api_send_progress.h" #include "base/unique_qptr.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" @@ -49,6 +48,10 @@ class SessionController; struct SectionShow; } // namespace Window +namespace Api { +enum class SendProgressType; +} // namespace Api + namespace HistoryView { class FieldHeader; @@ -67,6 +70,15 @@ public: Api::SendOptions options; TextWithTags textWithTags; }; + struct VoiceToSend { + QByteArray bytes; + VoiceWaveform waveform; + int duration = 0; + }; + struct SendActionUpdate { + Api::SendProgressType type = Api::SendProgressType(); + int progress = 0; + }; ComposeControls( not_null parent, @@ -83,9 +95,10 @@ public: [[nodiscard]] rpl::producer height() const; [[nodiscard]] int heightCurrent() const; - void focus(); + bool focus(); [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer<> sendRequests() const; + [[nodiscard]] rpl::producer sendVoiceRequests() const; [[nodiscard]] rpl::producer editRequests() const; [[nodiscard]] rpl::producer<> attachRequests() const; [[nodiscard]] rpl::producer fileChosen() const; @@ -94,7 +107,7 @@ public: [[nodiscard]] rpl::producer> keyEvents() const; [[nodiscard]] auto inlineResultChosen() const -> rpl::producer; - [[nodiscard]] rpl::producer sendActionUpdates() const; + [[nodiscard]] rpl::producer sendActionUpdates() const; using MimeDataHook = Fn data, @@ -140,6 +153,7 @@ private: void initWebpageProcess(); void updateSendButtonType(); void updateHeight(); + void updateControlsVisibility(); void updateControlsGeometry(QSize size); void updateOuterGeometry(QRect rect); void paintBackground(QRect clip); @@ -152,6 +166,21 @@ private: void setTextFromEditingMessage(not_null item); + void recordError(); + void recordUpdated(quint16 level, int samples); + void recordDone(QByteArray result, VoiceWaveform waveform, int samples); + + bool recordingAnimationCallback(crl::time now); + void stopRecording(bool send); + + void recordStartCallback(); + void recordStopCallback(bool active); + void recordUpdateCallback(QPoint globalPos); + + bool showRecordButton() const; + void drawRecording(Painter &p, float64 recordActive); + void updateOverStates(QPoint pos); + const not_null _parent; const not_null _window; History *_history = nullptr; @@ -173,17 +202,18 @@ private: rpl::event_stream _fileChosen; rpl::event_stream _photoChosen; rpl::event_stream _inlineResultChosen; - rpl::event_stream _sendActionUpdates; + rpl::event_stream _sendActionUpdates; + rpl::event_stream _sendVoiceRequests; TextWithTags _localSavedText; TextUpdateEvents _textUpdateEvents; - //bool _recording = false; - //bool _inField = false; + bool _recording = false; + bool _inField = false; //bool _inReplyEditForward = false; //bool _inClickable = false; - //int _recordingSamples = 0; - //int _recordCancelWidth; + int _recordingSamples = 0; + int _recordCancelWidth; rpl::lifetime _uploaderSubscriptions; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 58cc32e690..2cf6bafb5b 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -204,11 +204,12 @@ RepliesWidget::RepliesWidget( }, _inner->lifetime()); _composeControls->sendActionUpdates( - ) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=](ComposeControls::SendActionUpdate &&data) { session().sendProgressManager().update( _history, _rootId, - Api::SendProgressType::Typing); + data.type, + data.progress); }, lifetime()); _history->session().changes().messageUpdates( @@ -423,6 +424,11 @@ void RepliesWidget::setupComposeControls() { send(); }, lifetime()); + _composeControls->sendVoiceRequests( + ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { + sendVoice(data.bytes, data.waveform, data.duration); + }, lifetime()); + const auto saveEditMsgRequestId = lifetime().make_state(0); _composeControls->editRequests( ) | rpl::start_with_next([=](auto data) { @@ -816,6 +822,15 @@ void RepliesWidget::send() { // Ui::LayerOption::KeepOther); } +void RepliesWidget::sendVoice( + QByteArray bytes, + VoiceWaveform waveform, + int duration) { + auto action = Api::SendAction(_history); + action.replyTo = replyToId(); + session().api().sendVoiceMessage(bytes, waveform, duration, action); +} + void RepliesWidget::send(Api::SendOptions options) { const auto webPageId = _composeControls->webPageId();/* _previewCancelled ? CancelledWebPageId @@ -875,7 +890,7 @@ void RepliesWidget::edit( if (item) { Ui::show(Box(item, false)); } else { - _composeControls->focus(); + doSetInnerFocus(); } return; } else if (!left.text.isEmpty()) { @@ -908,7 +923,7 @@ void RepliesWidget::edit( } else if (err == u"MESSAGE_NOT_MODIFIED"_q) { _composeControls->cancelEditMessage(); } else if (err == u"MESSAGE_EMPTY"_q) { - _composeControls->focus(); + doSetInnerFocus(); } else { Ui::show(Box(tr::lng_edit_error(tr::now))); } @@ -924,7 +939,7 @@ void RepliesWidget::edit( crl::guard(this, fail)); _composeControls->hidePanelsAnimated(); - _composeControls->focus(); + doSetInnerFocus(); } void RepliesWidget::sendExistingDocument( @@ -1096,7 +1111,7 @@ void RepliesWidget::showAtEnd() { void RepliesWidget::finishSending() { _composeControls->hidePanelsAnimated(); //if (_previewData && _previewData->pendingTill) previewCancel(); - _composeControls->focus(); + doSetInnerFocus(); showAtEnd(); } @@ -1223,7 +1238,11 @@ QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &pa } void RepliesWidget::doSetInnerFocus() { - _composeControls->focus(); + if (!_inner->getSelectedText().rich.text.isEmpty() + || !_inner->getSelectedItems().empty() + || !_composeControls->focus()) { + _inner->setFocus(); + } } bool RepliesWidget::showInternal( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 46265705f1..f3a029002f 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -177,6 +177,7 @@ private: void send(); void send(Api::SendOptions options); + void sendVoice(QByteArray bytes, VoiceWaveform waveform, int duration); void edit( not_null item, Api::SendOptions options, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index ac4d01e425..6c0295c3e5 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -180,6 +180,11 @@ void ScheduledWidget::setupComposeControls() { send(); }, lifetime()); + _composeControls->sendVoiceRequests( + ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { + sendVoice(data.bytes, data.waveform, data.duration); + }, lifetime()); + const auto saveEditMsgRequestId = lifetime().make_state(0); _composeControls->editRequests( ) | rpl::start_with_next([=](auto data) { @@ -559,6 +564,28 @@ void ScheduledWidget::send(Api::SendOptions options) { _composeControls->focus(); } +void ScheduledWidget::sendVoice( + QByteArray bytes, + VoiceWaveform waveform, + int duration) { + const auto callback = [=](Api::SendOptions options) { + sendVoice(bytes, waveform, duration, options); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +void ScheduledWidget::sendVoice( + QByteArray bytes, + VoiceWaveform waveform, + int duration, + Api::SendOptions options) { + auto action = Api::SendAction(_history); + action.options = options; + session().api().sendVoiceMessage(bytes, waveform, duration, action); +} + void ScheduledWidget::edit( not_null item, Api::SendOptions options, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index c64a57f1f9..1cc916625c 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -149,6 +149,12 @@ private: void send(); void send(Api::SendOptions options); + void sendVoice(QByteArray bytes, VoiceWaveform waveform, int duration); + void sendVoice( + QByteArray bytes, + VoiceWaveform waveform, + int duration, + Api::SendOptions options); void edit( not_null item, Api::SendOptions options,