Allow sending voice messages in Replies / Scheduled.

This commit is contained in:
John Preston 2020-09-29 18:05:48 +03:00
parent 62da24c20b
commit 405c8125da
6 changed files with 369 additions and 33 deletions

View File

@ -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<bool>(false);
events(
) | rpl::filter([=](not_null<QEvent*> event) {
return ranges::contains(kMouseEvent, event->type())
return ranges::contains(kMouseEvents, event->type())
&& isEditingMessage();
}) | rpl::start_with_next([=](not_null<QEvent*> event) {
const auto type = event->type();
@ -502,11 +508,18 @@ ComposeControls::ComposeControls(
, _header(std::make_unique<FieldHeader>(
_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<VoiceToSend> ComposeControls::sendVoiceRequests() const {
return _sendVoiceRequests.events();
}
rpl::producer<MessageToEdit> 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<VoiceWaveform>();
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<const HistoryItem*> 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<QString>();
if (error) {
Ui::show(Box<InformBox>(*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<HistoryItem*> 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<Api::SendProgress> ComposeControls::sendActionUpdates() const {
rpl::producer<SendActionUpdate> 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() {

View File

@ -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<QWidget*> parent,
@ -83,9 +95,10 @@ public:
[[nodiscard]] rpl::producer<int> height() const;
[[nodiscard]] int heightCurrent() const;
void focus();
bool focus();
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<> sendRequests() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
[[nodiscard]] rpl::producer<> attachRequests() const;
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
@ -94,7 +107,7 @@ public:
[[nodiscard]] rpl::producer<not_null<QKeyEvent*>> keyEvents() const;
[[nodiscard]] auto inlineResultChosen() const
-> rpl::producer<ChatHelpers::TabbedSelector::InlineChosen>;
[[nodiscard]] rpl::producer<Api::SendProgress> sendActionUpdates() const;
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
using MimeDataHook = Fn<bool(
not_null<const QMimeData*> 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<HistoryItem*> 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<QWidget*> _parent;
const not_null<Window::SessionController*> _window;
History *_history = nullptr;
@ -173,17 +202,18 @@ private:
rpl::event_stream<FileChosen> _fileChosen;
rpl::event_stream<PhotoChosen> _photoChosen;
rpl::event_stream<ChatHelpers::TabbedSelector::InlineChosen> _inlineResultChosen;
rpl::event_stream<Api::SendProgress> _sendActionUpdates;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<VoiceToSend> _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;

View File

@ -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<mtpRequestId>(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<DeleteMessagesBox>(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<InformBox>(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(

View File

@ -177,6 +177,7 @@ private:
void send();
void send(Api::SendOptions options);
void sendVoice(QByteArray bytes, VoiceWaveform waveform, int duration);
void edit(
not_null<HistoryItem*> item,
Api::SendOptions options,

View File

@ -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<mtpRequestId>(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<HistoryItem*> item,
Api::SendOptions options,

View File

@ -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<HistoryItem*> item,
Api::SendOptions options,