1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/view/controls/history_view_compose_controls.h"
9
10 #include "base/event_filter.h"
11 #include "base/platform/base_platform_info.h"
12 #include "base/qt_signal_producer.h"
13 #include "base/unixtime.h"
14 #include "chat_helpers/emoji_suggestions_widget.h"
15 #include "chat_helpers/message_field.h"
16 #include "chat_helpers/send_context_menu.h"
17 #include "chat_helpers/tabbed_panel.h"
18 #include "chat_helpers/tabbed_section.h"
19 #include "chat_helpers/tabbed_selector.h"
20 #include "chat_helpers/field_autocomplete.h"
21 #include "core/application.h"
22 #include "core/core_settings.h"
23 #include "data/data_changes.h"
24 #include "data/data_drafts.h"
25 #include "data/data_messages.h"
26 #include "data/data_session.h"
27 #include "data/data_user.h"
28 #include "data/data_chat.h"
29 #include "data/data_channel.h"
30 #include "data/stickers/data_stickers.h"
31 #include "data/data_web_page.h"
32 #include "storage/storage_account.h"
33 #include "apiwrap.h"
34 #include "ui/boxes/confirm_box.h"
35 #include "history/history.h"
36 #include "history/history_item.h"
37 #include "history/view/controls/history_view_voice_record_bar.h"
38 #include "history/view/controls/history_view_ttl_button.h"
39 #include "history/view/history_view_webpage_preview.h"
40 #include "inline_bots/inline_results_widget.h"
41 #include "inline_bots/inline_bot_result.h"
42 #include "lang/lang_keys.h"
43 #include "main/main_session.h"
44 #include "media/audio/media_audio_capture.h"
45 #include "media/audio/media_audio.h"
46 #include "styles/style_chat.h"
47 #include "ui/text/text_options.h"
48 #include "ui/ui_utility.h"
49 #include "ui/widgets/input_fields.h"
50 #include "ui/text/format_values.h"
51 #include "ui/controls/emoji_button.h"
52 #include "ui/controls/send_button.h"
53 #include "ui/special_buttons.h"
54 #include "window/window_adaptive.h"
55 #include "window/window_session_controller.h"
56 #include "mainwindow.h"
57
58 namespace HistoryView {
59 namespace {
60
61 constexpr auto kSaveDraftTimeout = crl::time(1000);
62 constexpr auto kSaveDraftAnywayTimeout = 5 * crl::time(1000);
63 constexpr auto kMouseEvents = {
64 QEvent::MouseMove,
65 QEvent::MouseButtonPress,
66 QEvent::MouseButtonRelease
67 };
68
69 constexpr auto kCommonModifiers = 0
70 | Qt::ShiftModifier
71 | Qt::MetaModifier
72 | Qt::ControlModifier;
73
74 using FileChosen = ComposeControls::FileChosen;
75 using PhotoChosen = ComposeControls::PhotoChosen;
76 using MessageToEdit = ComposeControls::MessageToEdit;
77 using VoiceToSend = ComposeControls::VoiceToSend;
78 using SendActionUpdate = ComposeControls::SendActionUpdate;
79 using SetHistoryArgs = ComposeControls::SetHistoryArgs;
80 using VoiceRecordBar = HistoryView::Controls::VoiceRecordBar;
81
ShowWebPagePreview(WebPageData * page)82 [[nodiscard]] auto ShowWebPagePreview(WebPageData *page) {
83 return page && (page->pendingTill >= 0);
84 }
85
ProcessWebPageData(WebPageData * page)86 WebPageText ProcessWebPageData(WebPageData *page) {
87 auto previewText = HistoryView::TitleAndDescriptionFromWebPage(page);
88 if (previewText.title.isEmpty()) {
89 if (page->document) {
90 previewText.title = tr::lng_attach_file(tr::now);
91 } else if (page->photo) {
92 previewText.title = tr::lng_attach_photo(tr::now);
93 }
94 }
95 return previewText;
96 }
97
98 } // namespace
99
100 class FieldHeader final : public Ui::RpWidget {
101 public:
102 FieldHeader(QWidget *parent, not_null<Data::Session*> data);
103
104 void init();
105
106 void editMessage(FullMsgId id);
107 void replyToMessage(FullMsgId id);
108 void previewRequested(
109 rpl::producer<QString> title,
110 rpl::producer<QString> description,
111 rpl::producer<WebPageData*> page);
112
113 [[nodiscard]] bool isDisplayed() const;
114 [[nodiscard]] bool isEditingMessage() const;
115 [[nodiscard]] FullMsgId replyingToMessage() const;
116 [[nodiscard]] rpl::producer<FullMsgId> editMsgId() const;
117 [[nodiscard]] rpl::producer<FullMsgId> scrollToItemRequests() const;
118 [[nodiscard]] MessageToEdit queryToEdit();
119 [[nodiscard]] WebPageId webPageId() const;
120
121 [[nodiscard]] MsgId getDraftMessageId() const;
editCancelled() const122 [[nodiscard]] rpl::producer<> editCancelled() const {
123 return _editCancelled.events();
124 }
replyCancelled() const125 [[nodiscard]] rpl::producer<> replyCancelled() const {
126 return _replyCancelled.events();
127 }
previewCancelled() const128 [[nodiscard]] rpl::producer<> previewCancelled() const {
129 return _previewCancelled.events();
130 }
131
132 [[nodiscard]] rpl::producer<bool> visibleChanged();
133
134 private:
135 void updateControlsGeometry(QSize size);
136 void updateVisible();
137 void setShownMessage(HistoryItem *message);
138 void resolveMessageData();
139 void updateShownMessageText();
140
141 void paintWebPage(Painter &p);
142 void paintEditOrReplyToMessage(Painter &p);
143
144 struct Preview {
145 WebPageData *data = nullptr;
146 Ui::Text::String title;
147 Ui::Text::String description;
148 bool cancelled = false;
149 };
150
151 rpl::variable<QString> _title;
152 rpl::variable<QString> _description;
153
154 Preview _preview;
155 rpl::event_stream<> _editCancelled;
156 rpl::event_stream<> _replyCancelled;
157 rpl::event_stream<> _previewCancelled;
158
159 bool hasPreview() const;
160
161 rpl::variable<FullMsgId> _editMsgId;
162 rpl::variable<FullMsgId> _replyToId;
163
164 HistoryItem *_shownMessage = nullptr;
165 Ui::Text::String _shownMessageName;
166 Ui::Text::String _shownMessageText;
167 int _shownMessageNameVersion = -1;
168
169 const not_null<Data::Session*> _data;
170 const not_null<Ui::IconButton*> _cancel;
171
172 QRect _clickableRect;
173
174 rpl::event_stream<bool> _visibleChanged;
175 rpl::event_stream<FullMsgId> _scrollToItemRequests;
176
177 };
178
FieldHeader(QWidget * parent,not_null<Data::Session * > data)179 FieldHeader::FieldHeader(QWidget *parent, not_null<Data::Session*> data)
180 : RpWidget(parent)
181 , _data(data)
182 , _cancel(Ui::CreateChild<Ui::IconButton>(this, st::historyReplyCancel)) {
183 resize(QSize(parent->width(), st::historyReplyHeight));
184 init();
185 }
186
init()187 void FieldHeader::init() {
188 sizeValue(
189 ) | rpl::start_with_next([=](QSize size) {
190 updateControlsGeometry(size);
191 }, lifetime());
192
193 const auto leftIconPressed = lifetime().make_state<bool>(false);
194 paintRequest(
195 ) | rpl::start_with_next([=] {
196 Painter p(this);
197 p.fillRect(rect(), st::historyComposeAreaBg);
198
199 const auto position = st::historyReplyIconPosition;
200 if (isEditingMessage()) {
201 st::historyEditIcon.paint(p, position, width());
202 } else if (replyingToMessage()) {
203 st::historyReplyIcon.paint(p, position, width());
204 }
205
206 (!ShowWebPagePreview(_preview.data) || *leftIconPressed)
207 ? paintEditOrReplyToMessage(p)
208 : paintWebPage(p);
209 }, lifetime());
210
211 _editMsgId.value(
212 ) | rpl::start_with_next([=](FullMsgId value) {
213 const auto shown = value ? value : _replyToId.current();
214 setShownMessage(_data->message(shown));
215 }, lifetime());
216
217 _replyToId.value(
218 ) | rpl::start_with_next([=](FullMsgId value) {
219 if (!_editMsgId.current()) {
220 setShownMessage(_data->message(value));
221 }
222 }, lifetime());
223
224 _data->session().changes().messageUpdates(
225 Data::MessageUpdate::Flag::Edited
226 | Data::MessageUpdate::Flag::Destroyed
227 ) | rpl::filter([=](const Data::MessageUpdate &update) {
228 return (update.item == _shownMessage);
229 }) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
230 if (update.flags & Data::MessageUpdate::Flag::Destroyed) {
231 if (_editMsgId.current() == update.item->fullId()) {
232 _editCancelled.fire({});
233 }
234 if (_replyToId.current() == update.item->fullId()) {
235 _replyCancelled.fire({});
236 }
237 } else {
238 updateShownMessageText();
239 }
240 }, lifetime());
241
242 _cancel->addClickHandler([=] {
243 if (hasPreview()) {
244 _preview = {};
245 _previewCancelled.fire({});
246 } else if (_editMsgId.current()) {
247 _editCancelled.fire({});
248 } else if (_replyToId.current()) {
249 _replyCancelled.fire({});
250 }
251 updateVisible();
252 update();
253 });
254
255 _title.value(
256 ) | rpl::start_with_next([=](const auto &t) {
257 _preview.title.setText(
258 st::msgNameStyle,
259 t,
260 Ui::NameTextOptions());
261 }, lifetime());
262
263 _description.value(
264 ) | rpl::start_with_next([=](const auto &d) {
265 _preview.description.setText(
266 st::messageTextStyle,
267 TextUtilities::Clean(d),
268 Ui::DialogTextOptions());
269 }, lifetime());
270
271 setMouseTracking(true);
272 const auto inClickable = lifetime().make_state<bool>(false);
273 events(
274 ) | rpl::filter([=](not_null<QEvent*> event) {
275 return ranges::contains(kMouseEvents, event->type())
276 && (isEditingMessage() || replyingToMessage());
277 }) | rpl::start_with_next([=](not_null<QEvent*> event) {
278 const auto type = event->type();
279 const auto e = static_cast<QMouseEvent*>(event.get());
280 const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos());
281 const auto inPreviewRect = _clickableRect.contains(pos);
282
283 if (type == QEvent::MouseMove) {
284 if (inPreviewRect != *inClickable) {
285 *inClickable = inPreviewRect;
286 setCursor(*inClickable
287 ? style::cur_pointer
288 : style::cur_default);
289 }
290 return;
291 }
292 const auto isLeftIcon = (pos.x() < st::historyReplySkip);
293 const auto isLeftButton = (e->button() == Qt::LeftButton);
294 if (type == QEvent::MouseButtonPress) {
295 if (isLeftButton && isLeftIcon) {
296 *leftIconPressed = true;
297 update();
298 } else if (isLeftButton && inPreviewRect) {
299 auto id = isEditingMessage()
300 ? _editMsgId.current()
301 : replyingToMessage();
302 _scrollToItemRequests.fire(std::move(id));
303 }
304 } else if (type == QEvent::MouseButtonRelease) {
305 if (isLeftButton && *leftIconPressed) {
306 *leftIconPressed = false;
307 update();
308 }
309 }
310 }, lifetime());
311 }
312
updateShownMessageText()313 void FieldHeader::updateShownMessageText() {
314 Expects(_shownMessage != nullptr);
315
316 _shownMessageText.setText(
317 st::messageTextStyle,
318 _shownMessage->inReplyText(),
319 Ui::DialogTextOptions());
320 }
321
setShownMessage(HistoryItem * item)322 void FieldHeader::setShownMessage(HistoryItem *item) {
323 _shownMessage = item;
324 if (item) {
325 updateShownMessageText();
326 if (item->fullId() == _editMsgId.current()) {
327 _preview = {};
328 if (const auto media = item->media()) {
329 if (const auto page = media->webpage()) {
330 const auto preview = ProcessWebPageData(page);
331 _title = preview.title;
332 _description = preview.description;
333 _preview.data = page;
334 }
335 }
336 }
337 } else {
338 _shownMessageText.clear();
339 resolveMessageData();
340 }
341 if (isEditingMessage()) {
342 _shownMessageName.setText(
343 st::msgNameStyle,
344 tr::lng_edit_message(tr::now),
345 Ui::NameTextOptions());
346 } else {
347 _shownMessageName.clear();
348 _shownMessageNameVersion = -1;
349 }
350 updateVisible();
351 update();
352 }
353
resolveMessageData()354 void FieldHeader::resolveMessageData() {
355 const auto id = (isEditingMessage() ? _editMsgId : _replyToId).current();
356 if (!id) {
357 return;
358 }
359 const auto channel = id.channel
360 ? _data->channel(id.channel).get()
361 : nullptr;
362 const auto callback = [=](ChannelData *channel, MsgId msgId) {
363 const auto now = (isEditingMessage()
364 ? _editMsgId
365 : _replyToId).current();
366 if (now == id && !_shownMessage) {
367 if (const auto message = _data->message(channel, msgId)) {
368 setShownMessage(message);
369 } else if (isEditingMessage()) {
370 _editCancelled.fire({});
371 } else {
372 _replyCancelled.fire({});
373 }
374 }
375 };
376 _data->session().api().requestMessageData(
377 channel,
378 id.msg,
379 crl::guard(this, callback));
380 }
381
previewRequested(rpl::producer<QString> title,rpl::producer<QString> description,rpl::producer<WebPageData * > page)382 void FieldHeader::previewRequested(
383 rpl::producer<QString> title,
384 rpl::producer<QString> description,
385 rpl::producer<WebPageData*> page) {
386
387 std::move(
388 title
389 ) | rpl::filter([=] {
390 return !_preview.cancelled;
391 }) | start_with_next([=](const QString &t) {
392 _title = t;
393 }, lifetime());
394
395 std::move(
396 description
397 ) | rpl::filter([=] {
398 return !_preview.cancelled;
399 }) | rpl::start_with_next([=](const QString &d) {
400 _description = d;
401 }, lifetime());
402
403 std::move(
404 page
405 ) | rpl::filter([=] {
406 return !_preview.cancelled;
407 }) | rpl::start_with_next([=](WebPageData *p) {
408 _preview.data = p;
409 updateVisible();
410 }, lifetime());
411
412 }
413
paintWebPage(Painter & p)414 void FieldHeader::paintWebPage(Painter &p) {
415 Expects(ShowWebPagePreview(_preview.data));
416
417 const auto textTop = st::msgReplyPadding.top();
418 auto previewLeft = st::historyReplySkip + st::webPageLeft;
419 p.fillRect(
420 st::historyReplySkip,
421 textTop,
422 st::webPageBar,
423 st::msgReplyBarSize.height(),
424 st::msgInReplyBarColor);
425
426 const QRect to(
427 previewLeft,
428 textTop,
429 st::msgReplyBarSize.height(),
430 st::msgReplyBarSize.height());
431 if (HistoryView::DrawWebPageDataPreview(p, _preview.data, to)) {
432 previewLeft += st::msgReplyBarSize.height()
433 + st::msgReplyBarSkip
434 - st::msgReplyBarSize.width()
435 - st::msgReplyBarPos.x();
436 }
437 const auto elidedWidth = width()
438 - previewLeft
439 - _cancel->width()
440 - st::msgReplyPadding.right();
441
442 p.setPen(st::historyReplyNameFg);
443 _preview.title.drawElided(
444 p,
445 previewLeft,
446 textTop,
447 elidedWidth);
448
449 p.setPen(st::historyComposeAreaFg);
450 _preview.description.drawElided(
451 p,
452 previewLeft,
453 textTop + st::msgServiceNameFont->height,
454 elidedWidth);
455 }
456
paintEditOrReplyToMessage(Painter & p)457 void FieldHeader::paintEditOrReplyToMessage(Painter &p) {
458 const auto replySkip = st::historyReplySkip;
459 const auto availableWidth = width()
460 - replySkip
461 - _cancel->width()
462 - st::msgReplyPadding.right();
463
464 if (!_shownMessage) {
465 p.setFont(st::msgDateFont);
466 p.setPen(st::historyComposeAreaFgService);
467 const auto top = (st::msgReplyPadding.top()
468 + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2);
469 p.drawText(
470 replySkip,
471 top + st::msgDateFont->ascent,
472 st::msgDateFont->elided(
473 tr::lng_profile_loading(tr::now),
474 availableWidth));
475 return;
476 }
477
478 if (!isEditingMessage()) {
479 const auto user = _shownMessage->displayFrom()
480 ? _shownMessage->displayFrom()
481 : _shownMessage->author().get();
482 if (user->nameVersion > _shownMessageNameVersion) {
483 _shownMessageName.setText(
484 st::msgNameStyle,
485 user->name,
486 Ui::NameTextOptions());
487 _shownMessageNameVersion = user->nameVersion;
488 }
489 }
490
491 p.setPen(st::historyReplyNameFg);
492 p.setFont(st::msgServiceNameFont);
493 _shownMessageName.drawElided(
494 p,
495 replySkip,
496 st::msgReplyPadding.top(),
497 availableWidth);
498
499 p.setPen(st::historyComposeAreaFg);
500 p.setTextPalette(st::historyComposeAreaPalette);
501 _shownMessageText.drawElided(
502 p,
503 replySkip,
504 st::msgReplyPadding.top() + st::msgServiceNameFont->height,
505 availableWidth);
506 p.restoreTextPalette();
507 }
508
updateVisible()509 void FieldHeader::updateVisible() {
510 isDisplayed() ? show() : hide();
511 _visibleChanged.fire(isVisible());
512 }
513
visibleChanged()514 rpl::producer<bool> FieldHeader::visibleChanged() {
515 return _visibleChanged.events();
516 }
517
isDisplayed() const518 bool FieldHeader::isDisplayed() const {
519 return isEditingMessage() || replyingToMessage() || hasPreview();
520 }
521
isEditingMessage() const522 bool FieldHeader::isEditingMessage() const {
523 return !!_editMsgId.current();
524 }
525
replyingToMessage() const526 FullMsgId FieldHeader::replyingToMessage() const {
527 return _replyToId.current();
528 }
529
hasPreview() const530 bool FieldHeader::hasPreview() const {
531 return ShowWebPagePreview(_preview.data);
532 }
533
webPageId() const534 WebPageId FieldHeader::webPageId() const {
535 return hasPreview() ? _preview.data->id : CancelledWebPageId;
536 }
537
getDraftMessageId() const538 MsgId FieldHeader::getDraftMessageId() const {
539 return (isEditingMessage() ? _editMsgId : _replyToId).current().msg;
540 }
541
updateControlsGeometry(QSize size)542 void FieldHeader::updateControlsGeometry(QSize size) {
543 _cancel->moveToRight(0, 0);
544 _clickableRect = QRect(
545 st::historyReplySkip,
546 0,
547 width() - st::historyReplySkip - _cancel->width(),
548 height());
549 }
550
editMessage(FullMsgId id)551 void FieldHeader::editMessage(FullMsgId id) {
552 _editMsgId = id;
553 }
554
replyToMessage(FullMsgId id)555 void FieldHeader::replyToMessage(FullMsgId id) {
556 _replyToId = id;
557 }
558
editMsgId() const559 rpl::producer<FullMsgId> FieldHeader::editMsgId() const {
560 return _editMsgId.value();
561 }
562
scrollToItemRequests() const563 rpl::producer<FullMsgId> FieldHeader::scrollToItemRequests() const {
564 return _scrollToItemRequests.events();
565 }
566
queryToEdit()567 MessageToEdit FieldHeader::queryToEdit() {
568 const auto item = _data->message(_editMsgId.current());
569 if (!isEditingMessage() || !item) {
570 return {};
571 }
572 return {
573 item->fullId(),
574 {
575 item->isScheduled() ? item->date() : 0,
576 false,
577 false,
578 !hasPreview(),
579 },
580 };
581 }
582
ComposeControls(not_null<Ui::RpWidget * > parent,not_null<Window::SessionController * > window,Mode mode,SendMenu::Type sendMenuType)583 ComposeControls::ComposeControls(
584 not_null<Ui::RpWidget*> parent,
585 not_null<Window::SessionController*> window,
586 Mode mode,
587 SendMenu::Type sendMenuType)
588 : _parent(parent)
589 , _window(window)
590 , _mode(mode)
591 , _wrap(std::make_unique<Ui::RpWidget>(parent))
592 , _writeRestricted(std::make_unique<Ui::RpWidget>(parent))
593 , _send(std::make_shared<Ui::SendButton>(_wrap.get()))
594 , _attachToggle(Ui::CreateChild<Ui::IconButton>(
595 _wrap.get(),
596 st::historyAttach))
597 , _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>(
598 _wrap.get(),
599 st::historyAttachEmoji))
600 , _field(
601 Ui::CreateChild<Ui::InputField>(
602 _wrap.get(),
603 st::historyComposeField,
604 Ui::InputField::Mode::MultiLine,
605 tr::lng_message_ph()))
606 , _botCommandStart(Ui::CreateChild<Ui::IconButton>(
607 _wrap.get(),
608 st::historyBotCommandStart))
609 , _autocomplete(std::make_unique<FieldAutocomplete>(
610 parent,
611 window))
612 , _header(std::make_unique<FieldHeader>(
613 _wrap.get(),
614 &_window->session().data()))
615 , _voiceRecordBar(std::make_unique<VoiceRecordBar>(
616 _wrap.get(),
617 parent,
618 window,
619 _send,
620 st::historySendSize.height()))
621 , _sendMenuType(sendMenuType)
622 , _saveDraftTimer([=] { saveDraft(); })
623 , _previewState(Data::PreviewState::Allowed) {
624 init();
625 }
626
~ComposeControls()627 ComposeControls::~ComposeControls() {
628 saveFieldToHistoryLocalDraft();
629 unregisterDraftSources();
630 setTabbedPanel(nullptr);
631 session().api().request(_inlineBotResolveRequestId).cancel();
632 }
633
session() const634 Main::Session &ComposeControls::session() const {
635 return _window->session();
636 }
637
setHistory(SetHistoryArgs && args)638 void ComposeControls::setHistory(SetHistoryArgs &&args) {
639 // Right now only single non-null set of history is supported.
640 // Otherwise initWebpageProcess should be updated / rewritten.
641 Expects(!_history && *args.history);
642
643 _showSlowmodeError = std::move(args.showSlowmodeError);
644 _slowmodeSecondsLeft = rpl::single(0)
645 | rpl::then(std::move(args.slowmodeSecondsLeft));
646 _sendDisabledBySlowmode = rpl::single(false)
647 | rpl::then(std::move(args.sendDisabledBySlowmode));
648 _writeRestriction = rpl::single(std::optional<QString>())
649 | rpl::then(std::move(args.writeRestriction));
650 const auto history = *args.history;
651 //if (_history == history) {
652 // return;
653 //}
654 unregisterDraftSources();
655 _history = history;
656 registerDraftSource();
657 _window->tabbedSelector()->setCurrentPeer(
658 history ? history->peer.get() : nullptr);
659 initWebpageProcess();
660 updateBotCommandShown();
661 updateMessagesTTLShown();
662 updateControlsGeometry(_wrap->size());
663 updateControlsVisibility();
664 updateFieldPlaceholder();
665 //if (!_history) {
666 // return;
667 //}
668 const auto peer = _history->peer;
669 if (peer->isChat() && peer->asChat()->noParticipantInfo()) {
670 session().api().requestFullPeer(peer);
671 } else if (const auto channel = peer->asMegagroup()) {
672 if (!channel->mgInfo->botStatus) {
673 session().api().requestBots(channel);
674 }
675 } else if (hasSilentBroadcastToggle()) {
676 _silent = std::make_unique<Ui::SilentToggle>(
677 _wrap.get(),
678 peer->asChannel());
679 }
680 session().local().readDraftsWithCursors(_history);
681 applyDraft();
682 }
683
setCurrentDialogsEntryState(Dialogs::EntryState state)684 void ComposeControls::setCurrentDialogsEntryState(Dialogs::EntryState state) {
685 _currentDialogsEntryState = state;
686 if (_inlineResults) {
687 _inlineResults->setCurrentDialogsEntryState(state);
688 }
689 }
690
move(int x,int y)691 void ComposeControls::move(int x, int y) {
692 _wrap->move(x, y);
693 _writeRestricted->move(x, y);
694 }
695
resizeToWidth(int width)696 void ComposeControls::resizeToWidth(int width) {
697 _wrap->resizeToWidth(width);
698 _writeRestricted->resizeToWidth(width);
699 updateHeight();
700 }
701
setAutocompleteBoundingRect(QRect rect)702 void ComposeControls::setAutocompleteBoundingRect(QRect rect) {
703 if (_autocomplete) {
704 _autocomplete->setBoundings(rect);
705 }
706 }
707
height() const708 rpl::producer<int> ComposeControls::height() const {
709 using namespace rpl::mappers;
710 return rpl::conditional(
711 _writeRestriction.value() | rpl::map(!_1),
712 _wrap->heightValue(),
713 _writeRestricted->heightValue());
714 }
715
heightCurrent() const716 int ComposeControls::heightCurrent() const {
717 return _writeRestriction.current()
718 ? _writeRestricted->height()
719 : _wrap->height();
720 }
721
focus()722 bool ComposeControls::focus() {
723 if (isRecording()) {
724 return false;
725 }
726 _field->setFocus();
727 return true;
728 }
729
cancelRequests() const730 rpl::producer<> ComposeControls::cancelRequests() const {
731 return _cancelRequests.events();
732 }
733
scrollKeyEvents() const734 auto ComposeControls::scrollKeyEvents() const
735 -> rpl::producer<not_null<QKeyEvent*>> {
736 return _scrollKeyEvents.events();
737 }
738
editLastMessageRequests() const739 auto ComposeControls::editLastMessageRequests() const
740 -> rpl::producer<not_null<QKeyEvent*>> {
741 return _editLastMessageRequests.events();
742 }
743
replyNextRequests() const744 auto ComposeControls::replyNextRequests() const
745 -> rpl::producer<ReplyNextRequest> {
746 return _replyNextRequests.events();
747 }
748
sendContentRequests(SendRequestType requestType) const749 auto ComposeControls::sendContentRequests(SendRequestType requestType) const {
750 auto filter = rpl::filter([=] {
751 const auto type = (_mode == Mode::Normal)
752 ? Ui::SendButton::Type::Send
753 : Ui::SendButton::Type::Schedule;
754 const auto sendRequestType = _voiceRecordBar->isListenState()
755 ? SendRequestType::Voice
756 : SendRequestType::Text;
757 return (_send->type() == type) && (sendRequestType == requestType);
758 });
759 auto map = rpl::map_to(Api::SendOptions());
760 auto submits = base::qt_signal_producer(
761 _field.get(),
762 &Ui::InputField::submitted);
763 return rpl::merge(
764 _send->clicks() | filter | map,
765 std::move(submits) | filter | map,
766 _sendCustomRequests.events());
767 }
768
sendRequests() const769 rpl::producer<Api::SendOptions> ComposeControls::sendRequests() const {
770 return sendContentRequests(SendRequestType::Text);
771 }
772
sendVoiceRequests() const773 rpl::producer<VoiceToSend> ComposeControls::sendVoiceRequests() const {
774 return _voiceRecordBar->sendVoiceRequests();
775 }
776
sendCommandRequests() const777 rpl::producer<QString> ComposeControls::sendCommandRequests() const {
778 return _sendCommandRequests.events();
779 }
780
editRequests() const781 rpl::producer<MessageToEdit> ComposeControls::editRequests() const {
782 auto toValue = rpl::map([=] { return _header->queryToEdit(); });
783 auto filter = rpl::filter([=] {
784 return _send->type() == Ui::SendButton::Type::Save;
785 });
786 auto submits = base::qt_signal_producer(
787 _field.get(),
788 &Ui::InputField::submitted);
789 return rpl::merge(
790 _send->clicks() | filter | toValue,
791 std::move(submits) | filter | toValue);
792 }
793
attachRequests() const794 rpl::producer<> ComposeControls::attachRequests() const {
795 return rpl::merge(
796 _attachToggle->clicks() | rpl::to_empty,
797 _attachRequests.events()
798 ) | rpl::filter([=] {
799 if (isEditingMessage()) {
800 _window->show(
801 Box<Ui::InformBox>(tr::lng_edit_caption_attach(tr::now)));
802 return false;
803 }
804 return true;
805 });
806 }
807
setMimeDataHook(MimeDataHook hook)808 void ComposeControls::setMimeDataHook(MimeDataHook hook) {
809 _field->setMimeDataHook(std::move(hook));
810 }
811
fileChosen() const812 rpl::producer<FileChosen> ComposeControls::fileChosen() const {
813 return _fileChosen.events();
814 }
815
photoChosen() const816 rpl::producer<PhotoChosen> ComposeControls::photoChosen() const {
817 return _photoChosen.events();
818 }
819
inlineResultChosen() const820 auto ComposeControls::inlineResultChosen() const
821 ->rpl::producer<ChatHelpers::TabbedSelector::InlineChosen> {
822 return _inlineResultChosen.events();
823 }
824
showStarted()825 void ComposeControls::showStarted() {
826 if (_inlineResults) {
827 _inlineResults->hideFast();
828 }
829 if (_tabbedPanel) {
830 _tabbedPanel->hideFast();
831 }
832 if (_voiceRecordBar) {
833 _voiceRecordBar->hideFast();
834 }
835 if (_autocomplete) {
836 _autocomplete->hideFast();
837 }
838 _wrap->hide();
839 _writeRestricted->hide();
840 }
841
showFinished()842 void ComposeControls::showFinished() {
843 if (_inlineResults) {
844 _inlineResults->hideFast();
845 }
846 if (_tabbedPanel) {
847 _tabbedPanel->hideFast();
848 }
849 if (_voiceRecordBar) {
850 _voiceRecordBar->hideFast();
851 }
852 if (_autocomplete) {
853 _autocomplete->hideFast();
854 }
855 updateWrappingVisibility();
856 _voiceRecordBar->orderControls();
857 }
858
raisePanels()859 void ComposeControls::raisePanels() {
860 if (_autocomplete) {
861 _autocomplete->raise();
862 }
863 if (_inlineResults) {
864 _inlineResults->raise();
865 }
866 if (_tabbedPanel) {
867 _tabbedPanel->raise();
868 }
869 if (_raiseEmojiSuggestions) {
870 _raiseEmojiSuggestions();
871 }
872 }
873
showForGrab()874 void ComposeControls::showForGrab() {
875 showFinished();
876 }
877
getTextWithAppliedMarkdown() const878 TextWithTags ComposeControls::getTextWithAppliedMarkdown() const {
879 return _field->getTextWithAppliedMarkdown();
880 }
881
clear()882 void ComposeControls::clear() {
883 // Otherwise cancelReplyMessage() will save the draft.
884 const auto saveTextDraft = !replyingToMessage();
885 setFieldText(
886 {},
887 saveTextDraft ? TextUpdateEvent::SaveDraft : TextUpdateEvent());
888 cancelReplyMessage();
889 }
890
setText(const TextWithTags & textWithTags)891 void ComposeControls::setText(const TextWithTags &textWithTags) {
892 setFieldText(textWithTags);
893 }
894
setFieldText(const TextWithTags & textWithTags,TextUpdateEvents events,FieldHistoryAction fieldHistoryAction)895 void ComposeControls::setFieldText(
896 const TextWithTags &textWithTags,
897 TextUpdateEvents events,
898 FieldHistoryAction fieldHistoryAction) {
899 _textUpdateEvents = events;
900 _field->setTextWithTags(textWithTags, fieldHistoryAction);
901 auto cursor = _field->textCursor();
902 cursor.movePosition(QTextCursor::End);
903 _field->setTextCursor(cursor);
904 _textUpdateEvents = TextUpdateEvent::SaveDraft
905 | TextUpdateEvent::SendTyping;
906
907 _previewCancel();
908 _previewState = Data::PreviewState::Allowed;
909 }
910
saveFieldToHistoryLocalDraft()911 void ComposeControls::saveFieldToHistoryLocalDraft() {
912 const auto key = draftKeyCurrent();
913 if (!_history || key == Data::DraftKey::None()) {
914 return;
915 }
916 const auto id = _header->getDraftMessageId();
917 if (id || !_field->empty()) {
918 _history->setDraft(
919 draftKeyCurrent(),
920 std::make_unique<Data::Draft>(
921 _field,
922 _header->getDraftMessageId(),
923 _previewState));
924 } else {
925 _history->clearDraft(draftKeyCurrent());
926 }
927 }
928
clearFieldText(TextUpdateEvents events,FieldHistoryAction fieldHistoryAction)929 void ComposeControls::clearFieldText(
930 TextUpdateEvents events,
931 FieldHistoryAction fieldHistoryAction) {
932 setFieldText({}, events, fieldHistoryAction);
933 }
934
hidePanelsAnimated()935 void ComposeControls::hidePanelsAnimated() {
936 if (_autocomplete) {
937 _autocomplete->hideAnimated();
938 }
939 if (_tabbedPanel) {
940 _tabbedPanel->hideAnimated();
941 }
942 if (_inlineResults) {
943 _inlineResults->hideAnimated();
944 }
945 }
946
checkAutocomplete()947 void ComposeControls::checkAutocomplete() {
948 if (!_history) {
949 return;
950 }
951
952 const auto peer = _history->peer;
953 const auto autocomplete = _isInlineBot
954 ? AutocompleteQuery()
955 : ParseMentionHashtagBotCommandQuery(_field);
956 if (!autocomplete.query.isEmpty()) {
957 if (autocomplete.query[0] == '#'
958 && cRecentWriteHashtags().isEmpty()
959 && cRecentSearchHashtags().isEmpty()) {
960 peer->session().local().readRecentHashtagsAndBots();
961 } else if (autocomplete.query[0] == '@'
962 && cRecentInlineBots().isEmpty()) {
963 peer->session().local().readRecentHashtagsAndBots();
964 } else if (autocomplete.query[0] == '/'
965 && peer->isUser()
966 && !peer->asUser()->isBot()) {
967 return;
968 }
969 }
970 _autocomplete->showFiltered(
971 peer,
972 autocomplete.query,
973 autocomplete.fromStart);
974 }
975
init()976 void ComposeControls::init() {
977 initField();
978 initTabbedSelector();
979 initSendButton();
980 initWriteRestriction();
981 initVoiceRecordBar();
982 initKeyHandler();
983
984 _botCommandStart->setClickedCallback([=] { setText({ "/" }); });
985
986 _wrap->sizeValue(
987 ) | rpl::start_with_next([=](QSize size) {
988 updateControlsGeometry(size);
989 }, _wrap->lifetime());
990
991 _wrap->geometryValue(
992 ) | rpl::start_with_next([=](QRect rect) {
993 updateOuterGeometry(rect);
994 }, _wrap->lifetime());
995
996 _wrap->paintRequest(
997 ) | rpl::start_with_next([=](QRect clip) {
998 paintBackground(clip);
999 }, _wrap->lifetime());
1000
1001 _header->editMsgId(
1002 ) | rpl::start_with_next([=](const auto &id) {
1003 unregisterDraftSources();
1004 updateSendButtonType();
1005 registerDraftSource();
1006 }, _wrap->lifetime());
1007
1008 _header->previewCancelled(
1009 ) | rpl::start_with_next([=] {
1010 _previewState = Data::PreviewState::Cancelled;
1011 _saveDraftText = true;
1012 _saveDraftStart = crl::now();
1013 saveDraft();
1014 }, _wrap->lifetime());
1015
1016 _header->editCancelled(
1017 ) | rpl::start_with_next([=] {
1018 cancelEditMessage();
1019 }, _wrap->lifetime());
1020
1021 _header->replyCancelled(
1022 ) | rpl::start_with_next([=] {
1023 cancelReplyMessage();
1024 }, _wrap->lifetime());
1025
1026 _header->visibleChanged(
1027 ) | rpl::start_with_next([=](bool shown) {
1028 updateHeight();
1029 if (shown) {
1030 raisePanels();
1031 }
1032 }, _wrap->lifetime());
1033
1034 sendContentRequests(
1035 SendRequestType::Voice
1036 ) | rpl::start_with_next([=](Api::SendOptions options) {
1037 _voiceRecordBar->requestToSendWithOptions(options);
1038 }, _wrap->lifetime());
1039
1040 {
1041 const auto lastMsgId = _wrap->lifetime().make_state<FullMsgId>();
1042
1043 _header->editMsgId(
1044 ) | rpl::filter([=](const auto &id) {
1045 return !!id;
1046 }) | rpl::start_with_next([=](const auto &id) {
1047 *lastMsgId = id;
1048 }, _wrap->lifetime());
1049
1050 session().data().itemRemoved(
1051 ) | rpl::filter([=](not_null<const HistoryItem*> item) {
1052 return item->id && ((*lastMsgId) == item->fullId());
1053 }) | rpl::start_with_next([=] {
1054 cancelEditMessage();
1055 }, _wrap->lifetime());
1056 }
1057
1058 orderControls();
1059 }
1060
orderControls()1061 void ComposeControls::orderControls() {
1062 _voiceRecordBar->raise();
1063 _send->raise();
1064 }
1065
showRecordButton() const1066 bool ComposeControls::showRecordButton() const {
1067 return ::Media::Capture::instance()->available()
1068 && !_voiceRecordBar->isListenState()
1069 && !HasSendText(_field)
1070 //&& !readyToForward()
1071 && !isEditingMessage();
1072 }
1073
clearListenState()1074 void ComposeControls::clearListenState() {
1075 _voiceRecordBar->clearListenState();
1076 }
1077
drawRestrictedWrite(Painter & p,const QString & error)1078 void ComposeControls::drawRestrictedWrite(Painter &p, const QString &error) {
1079 p.fillRect(_writeRestricted->rect(), st::historyReplyBg);
1080
1081 p.setFont(st::normalFont);
1082 p.setPen(st::windowSubTextFg);
1083 p.drawText(
1084 _writeRestricted->rect().marginsRemoved(
1085 QMargins(st::historySendPadding, 0, st::historySendPadding, 0)),
1086 error,
1087 style::al_center);
1088 }
1089
initKeyHandler()1090 void ComposeControls::initKeyHandler() {
1091 _wrap->events(
1092 ) | rpl::filter([=](not_null<QEvent*> event) {
1093 return (event->type() == QEvent::KeyPress);
1094 }) | rpl::start_with_next([=](not_null<QEvent*> e) {
1095 auto keyEvent = static_cast<QKeyEvent*>(e.get());
1096 const auto key = keyEvent->key();
1097 const auto isCtrl = keyEvent->modifiers() == Qt::ControlModifier;
1098 const auto hasModifiers = (Qt::NoModifier !=
1099 (keyEvent->modifiers()
1100 & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier)));
1101 if (key == Qt::Key_O && isCtrl) {
1102 _attachRequests.fire({});
1103 return;
1104 }
1105 if (key == Qt::Key_Up && !hasModifiers) {
1106 if (!isEditingMessage() && _field->empty()) {
1107 _editLastMessageRequests.fire(std::move(keyEvent));
1108 return;
1109 }
1110 }
1111 if (!hasModifiers
1112 && ((key == Qt::Key_Up)
1113 || (key == Qt::Key_Down)
1114 || (key == Qt::Key_PageUp)
1115 || (key == Qt::Key_PageDown))) {
1116 _scrollKeyEvents.fire(std::move(keyEvent));
1117 }
1118 }, _wrap->lifetime());
1119
1120 base::install_event_filter(_wrap.get(), _field, [=](not_null<QEvent*> e) {
1121 using Result = base::EventFilterResult;
1122 if (e->type() != QEvent::KeyPress) {
1123 return Result::Continue;
1124 }
1125 const auto k = static_cast<QKeyEvent*>(e.get());
1126
1127 if ((k->modifiers() & kCommonModifiers) == Qt::ControlModifier) {
1128 const auto isUp = (k->key() == Qt::Key_Up);
1129 const auto isDown = (k->key() == Qt::Key_Down);
1130 if (isUp || isDown) {
1131 if (Platform::IsMac()) {
1132 // Cmd + Up is used instead of Home.
1133 if ((isUp && (!_field->textCursor().atStart()))
1134 // Cmd + Down is used instead of End.
1135 || (isDown && (!_field->textCursor().atEnd()))) {
1136 return Result::Continue;
1137 }
1138 }
1139 _replyNextRequests.fire({
1140 .replyId = replyingToMessage(),
1141 .direction = (isDown
1142 ? ReplyNextRequest::Direction::Next
1143 : ReplyNextRequest::Direction::Previous)
1144 });
1145 return Result::Cancel;
1146 }
1147 }
1148 return Result::Continue;
1149 });
1150 }
1151
initField()1152 void ComposeControls::initField() {
1153 _field->setMaxHeight(st::historyComposeFieldMaxHeight);
1154 updateSubmitSettings();
1155 //Ui::Connect(_field, &Ui::InputField::submitted, [=] { send(); });
1156 Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); });
1157 Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); });
1158 Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); });
1159 //Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); });
1160 Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); });
1161 InitMessageField(_window, _field);
1162 initAutocomplete();
1163 const auto suggestions = Ui::Emoji::SuggestionsController::Init(
1164 _parent,
1165 _field,
1166 &_window->session());
1167 _raiseEmojiSuggestions = [=] { suggestions->raise(); };
1168 InitSpellchecker(_window, _field);
1169
1170 const auto rawTextEdit = _field->rawTextEdit().get();
1171 rpl::merge(
1172 _field->scrollTop().changes() | rpl::to_empty,
1173 base::qt_signal_producer(
1174 rawTextEdit,
1175 &QTextEdit::cursorPositionChanged)
1176 ) | rpl::start_with_next([=] {
1177 saveDraftDelayed();
1178 }, _field->lifetime());
1179 }
1180
updateSubmitSettings()1181 void ComposeControls::updateSubmitSettings() {
1182 const auto settings = _isInlineBot
1183 ? Ui::InputField::SubmitSettings::None
1184 : Core::App().settings().sendSubmitWay();
1185 _field->setSubmitSettings(settings);
1186 }
1187
initAutocomplete()1188 void ComposeControls::initAutocomplete() {
1189 const auto insertHashtagOrBotCommand = [=](
1190 const QString &string,
1191 FieldAutocomplete::ChooseMethod method) {
1192 // Send bot command at once, if it was not inserted by pressing Tab.
1193 if (string.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) {
1194 _sendCommandRequests.fire_copy(string);
1195 setText(
1196 _field->getTextWithTagsPart(_field->textCursor().position()));
1197 } else {
1198 _field->insertTag(string);
1199 }
1200 };
1201 const auto insertMention = [=](not_null<UserData*> user) {
1202 if (user->username.isEmpty()) {
1203 _field->insertTag(
1204 user->firstName.isEmpty() ? user->name : user->firstName,
1205 PrepareMentionTag(user));
1206 } else {
1207 _field->insertTag('@' + user->username);
1208 }
1209 };
1210
1211 _autocomplete->mentionChosen(
1212 ) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) {
1213 insertMention(data.user);
1214 }, _autocomplete->lifetime());
1215
1216 _autocomplete->hashtagChosen(
1217 ) | rpl::start_with_next([=](FieldAutocomplete::HashtagChosen data) {
1218 insertHashtagOrBotCommand(data.hashtag, data.method);
1219 }, _autocomplete->lifetime());
1220
1221 _autocomplete->botCommandChosen(
1222 ) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) {
1223 insertHashtagOrBotCommand(data.command, data.method);
1224 }, _autocomplete->lifetime());
1225
1226 _autocomplete->stickerChosen(
1227 ) | rpl::start_with_next([=](FieldAutocomplete::StickerChosen data) {
1228 if (!_showSlowmodeError || !_showSlowmodeError()) {
1229 setText({});
1230 }
1231 //_saveDraftText = true;
1232 //_saveDraftStart = crl::now();
1233 //saveDraft();
1234 //saveCloudDraft(); // won't be needed if SendInlineBotResult will clear the cloud draft
1235 _fileChosen.fire(FileChosen{
1236 .document = data.sticker,
1237 .options = data.options,
1238 });
1239 }, _autocomplete->lifetime());
1240
1241 _autocomplete->choosingProcesses(
1242 ) | rpl::start_with_next([=](FieldAutocomplete::Type type) {
1243 if (type == FieldAutocomplete::Type::Stickers) {
1244 _sendActionUpdates.fire({
1245 .type = Api::SendProgressType::ChooseSticker,
1246 });
1247 }
1248 }, _autocomplete->lifetime());
1249
1250 _autocomplete->setSendMenuType([=] { return sendMenuType(); });
1251
1252 //_autocomplete->setModerateKeyActivateCallback([=](int key) {
1253 // return _keyboard->isHidden()
1254 // ? false
1255 // : _keyboard->moderateKeyActivate(key);
1256 //});
1257
1258 _field->rawTextEdit()->installEventFilter(_autocomplete.get());
1259
1260 _window->session().data().botCommandsChanges(
1261 ) | rpl::filter([=](not_null<PeerData*> peer) {
1262 return _history && (_history->peer == peer);
1263 }) | rpl::start_with_next([=] {
1264 if (_autocomplete->clearFilteredBotCommands()) {
1265 checkAutocomplete();
1266 }
1267 }, _autocomplete->lifetime());
1268
1269 _window->session().data().stickers().updated(
1270 ) | rpl::start_with_next([=] {
1271 updateStickersByEmoji();
1272 }, _autocomplete->lifetime());
1273
1274 QObject::connect(
1275 _field->rawTextEdit(),
1276 &QTextEdit::cursorPositionChanged,
1277 _autocomplete.get(),
1278 [=] { checkAutocomplete(); },
1279 Qt::QueuedConnection);
1280
1281 _autocomplete->hideFast();
1282 }
1283
updateStickersByEmoji()1284 bool ComposeControls::updateStickersByEmoji() {
1285 if (!_history) {
1286 return false;
1287 }
1288 const auto emoji = [&] {
1289 const auto errorForStickers = Data::RestrictionError(
1290 _history->peer,
1291 ChatRestriction::SendStickers);
1292 if (!isEditingMessage() && !errorForStickers) {
1293 const auto &text = _field->getTextWithTags().text;
1294 auto length = 0;
1295 if (const auto emoji = Ui::Emoji::Find(text, &length)) {
1296 if (text.size() <= length) {
1297 return emoji;
1298 }
1299 }
1300 }
1301 return EmojiPtr(nullptr);
1302 }();
1303 _autocomplete->showStickers(emoji);
1304 return (emoji != nullptr);
1305 }
1306
updateFieldPlaceholder()1307 void ComposeControls::updateFieldPlaceholder() {
1308 if (!isEditingMessage() && _isInlineBot) {
1309 _field->setPlaceholder(
1310 rpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)),
1311 _inlineBot->username.size() + 2);
1312 return;
1313 }
1314
1315 _field->setPlaceholder([&] {
1316 if (isEditingMessage()) {
1317 return tr::lng_edit_message_text();
1318 } else if (!_history) {
1319 return tr::lng_message_ph();
1320 } else if (const auto channel = _history->peer->asChannel()) {
1321 if (channel->isBroadcast()) {
1322 return session().data().notifySilentPosts(channel)
1323 ? tr::lng_broadcast_silent_ph()
1324 : tr::lng_broadcast_ph();
1325 } else if (channel->adminRights() & ChatAdminRight::Anonymous) {
1326 return tr::lng_send_anonymous_ph();
1327 } else {
1328 return tr::lng_message_ph();
1329 }
1330 } else {
1331 return tr::lng_message_ph();
1332 }
1333 }());
1334 updateSendButtonType();
1335 }
1336
updateSilentBroadcast()1337 void ComposeControls::updateSilentBroadcast() {
1338 if (!_silent || !_history) {
1339 return;
1340 }
1341 const auto &peer = _history->peer;
1342 if (!session().data().notifySilentPostsUnknown(peer)) {
1343 _silent->setChecked(session().data().notifySilentPosts(peer));
1344 updateFieldPlaceholder();
1345 }
1346 }
1347
fieldChanged()1348 void ComposeControls::fieldChanged() {
1349 const auto typing = (!_inlineBot
1350 && !_header->isEditingMessage()
1351 && (_textUpdateEvents & TextUpdateEvent::SendTyping));
1352 updateSendButtonType();
1353 if (!HasSendText(_field)) {
1354 _previewState = Data::PreviewState::Allowed;
1355 }
1356 if (updateBotCommandShown()) {
1357 updateControlsVisibility();
1358 updateControlsGeometry(_wrap->size());
1359 }
1360 InvokeQueued(_autocomplete.get(), [=] {
1361 updateInlineBotQuery();
1362 const auto choosingSticker = updateStickersByEmoji();
1363 if (!choosingSticker && typing) {
1364 _sendActionUpdates.fire({ Api::SendProgressType::Typing });
1365 }
1366 });
1367
1368 if (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {
1369 return;
1370 }
1371 _saveDraftText = true;
1372 saveDraft(true);
1373 }
1374
saveDraftDelayed()1375 void ComposeControls::saveDraftDelayed() {
1376 if (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {
1377 return;
1378 }
1379 saveDraft(true);
1380 }
1381
draftKey(DraftType type) const1382 Data::DraftKey ComposeControls::draftKey(DraftType type) const {
1383 using Section = Dialogs::EntryState::Section;
1384 using Key = Data::DraftKey;
1385
1386 switch (_currentDialogsEntryState.section) {
1387 case Section::History:
1388 return (type == DraftType::Edit) ? Key::LocalEdit() : Key::Local();
1389 case Section::Scheduled:
1390 return (type == DraftType::Edit)
1391 ? Key::ScheduledEdit()
1392 : Key::Scheduled();
1393 case Section::Replies:
1394 return (type == DraftType::Edit)
1395 ? Key::RepliesEdit(_currentDialogsEntryState.rootId)
1396 : Key::Replies(_currentDialogsEntryState.rootId);
1397 }
1398 return Key::None();
1399 }
1400
draftKeyCurrent() const1401 Data::DraftKey ComposeControls::draftKeyCurrent() const {
1402 return draftKey(isEditingMessage() ? DraftType::Edit : DraftType::Normal);
1403 }
1404
saveDraft(bool delayed)1405 void ComposeControls::saveDraft(bool delayed) {
1406 if (delayed) {
1407 const auto now = crl::now();
1408 if (!_saveDraftStart) {
1409 _saveDraftStart = now;
1410 return _saveDraftTimer.callOnce(kSaveDraftTimeout);
1411 } else if (now - _saveDraftStart < kSaveDraftAnywayTimeout) {
1412 return _saveDraftTimer.callOnce(kSaveDraftTimeout);
1413 }
1414 }
1415 writeDrafts();
1416 }
1417
writeDraftTexts()1418 void ComposeControls::writeDraftTexts() {
1419 Expects(_history != nullptr);
1420
1421 session().local().writeDrafts(_history);
1422 }
1423
writeDraftCursors()1424 void ComposeControls::writeDraftCursors() {
1425 Expects(_history != nullptr);
1426
1427 session().local().writeDraftCursors(_history);
1428 }
1429
unregisterDraftSources()1430 void ComposeControls::unregisterDraftSources() {
1431 if (!_history) {
1432 return;
1433 }
1434 const auto normal = draftKey(DraftType::Normal);
1435 const auto edit = draftKey(DraftType::Edit);
1436 if (normal != Data::DraftKey::None()) {
1437 session().local().unregisterDraftSource(_history, normal);
1438 }
1439 if (edit != Data::DraftKey::None()) {
1440 session().local().unregisterDraftSource(_history, edit);
1441 }
1442 }
1443
registerDraftSource()1444 void ComposeControls::registerDraftSource() {
1445 if (!_history) {
1446 return;
1447 }
1448 const auto key = draftKeyCurrent();
1449 if (key != Data::DraftKey::None()) {
1450 const auto draft = [=] {
1451 return Storage::MessageDraft{
1452 _header->getDraftMessageId(),
1453 _field->getTextWithTags(),
1454 _previewState,
1455 };
1456 };
1457 auto draftSource = Storage::MessageDraftSource{
1458 .draft = draft,
1459 .cursor = [=] { return MessageCursor(_field); },
1460 };
1461 session().local().registerDraftSource(
1462 _history,
1463 key,
1464 std::move(draftSource));
1465 }
1466 }
1467
writeDrafts()1468 void ComposeControls::writeDrafts() {
1469 const auto save = (_history != nullptr)
1470 && (_saveDraftStart > 0)
1471 && (draftKeyCurrent() != Data::DraftKey::None());
1472 _saveDraftStart = 0;
1473 _saveDraftTimer.cancel();
1474 if (save) {
1475 if (_saveDraftText) {
1476 writeDraftTexts();
1477 }
1478 writeDraftCursors();
1479 }
1480 _saveDraftText = false;
1481
1482 //if (!isEditingMessage() && !_inlineBot) {
1483 // _saveCloudDraftTimer.callOnce(kSaveCloudDraftIdleTimeout);
1484 //}
1485 }
1486
applyDraft(FieldHistoryAction fieldHistoryAction)1487 void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
1488 Expects(_history != nullptr);
1489
1490 InvokeQueued(_autocomplete.get(), [=] { updateStickersByEmoji(); });
1491 const auto guard = gsl::finally([&] {
1492 updateSendButtonType();
1493 updateControlsVisibility();
1494 updateControlsGeometry(_wrap->size());
1495 });
1496
1497 const auto editDraft = _history->draft(draftKey(DraftType::Edit));
1498 const auto draft = editDraft
1499 ? editDraft
1500 : _history->draft(draftKey(DraftType::Normal));
1501 if (!draft) {
1502 clearFieldText(0, fieldHistoryAction);
1503 _field->setFocus();
1504 _header->editMessage({});
1505 _header->replyToMessage({});
1506 return;
1507 }
1508
1509 _textUpdateEvents = 0;
1510 setFieldText(draft->textWithTags, 0, fieldHistoryAction);
1511 _field->setFocus();
1512 draft->cursor.applyTo(_field);
1513 _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
1514 _previewSetState(draft->previewState);
1515
1516 if (draft == editDraft) {
1517 _header->editMessage({ _history->channelId(), draft->msgId });
1518 _header->replyToMessage({});
1519 } else {
1520 _header->replyToMessage({ _history->channelId(), draft->msgId });
1521 _header->editMessage({});
1522 }
1523 }
1524
fieldTabbed()1525 void ComposeControls::fieldTabbed() {
1526 if (!_autocomplete->isHidden()) {
1527 _autocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
1528 }
1529 }
1530
sendActionUpdates() const1531 rpl::producer<SendActionUpdate> ComposeControls::sendActionUpdates() const {
1532 return rpl::merge(
1533 _sendActionUpdates.events(),
1534 _voiceRecordBar->sendActionUpdates());
1535 }
1536
initTabbedSelector()1537 void ComposeControls::initTabbedSelector() {
1538 if (_window->hasTabbedSelectorOwnership()) {
1539 createTabbedPanel();
1540 } else {
1541 setTabbedPanel(nullptr);
1542 }
1543
1544 _tabbedSelectorToggle->addClickHandler([=] {
1545 toggleTabbedSelectorMode();
1546 });
1547
1548 const auto selector = _window->tabbedSelector();
1549 const auto wrap = _wrap.get();
1550
1551 base::install_event_filter(wrap, selector, [=](not_null<QEvent*> e) {
1552 if (_tabbedPanel && e->type() == QEvent::ParentChange) {
1553 setTabbedPanel(nullptr);
1554 }
1555 return base::EventFilterResult::Continue;
1556 });
1557
1558 selector->emojiChosen(
1559 ) | rpl::start_with_next([=](EmojiPtr emoji) {
1560 Ui::InsertEmojiAtCursor(_field->textCursor(), emoji);
1561 }, wrap->lifetime());
1562
1563 selector->fileChosen(
1564 ) | rpl::start_to_stream(_fileChosen, wrap->lifetime());
1565
1566 selector->photoChosen(
1567 ) | rpl::start_to_stream(_photoChosen, wrap->lifetime());
1568
1569 selector->inlineResultChosen(
1570 ) | rpl::start_to_stream(_inlineResultChosen, wrap->lifetime());
1571
1572 selector->contextMenuRequested(
1573 ) | rpl::start_with_next([=] {
1574 selector->showMenuWithType(sendMenuType());
1575 }, wrap->lifetime());
1576
1577 selector->choosingStickerUpdated(
1578 ) | rpl::start_with_next([=](ChatHelpers::TabbedSelector::Action action) {
1579 _sendActionUpdates.fire({
1580 .type = Api::SendProgressType::ChooseSticker,
1581 .cancel = (action == ChatHelpers::TabbedSelector::Action::Cancel),
1582 });
1583 }, wrap->lifetime());
1584 }
1585
initSendButton()1586 void ComposeControls::initSendButton() {
1587 rpl::combine(
1588 _slowmodeSecondsLeft.value(),
1589 _sendDisabledBySlowmode.value()
1590 ) | rpl::start_with_next([=] {
1591 updateSendButtonType();
1592 }, _send->lifetime());
1593
1594 _send->finishAnimating();
1595
1596 _send->clicks(
1597 ) | rpl::filter([=] {
1598 return (_send->type() == Ui::SendButton::Type::Cancel);
1599 }) | rpl::start_with_next([=] {
1600 cancelInlineBot();
1601 }, _send->lifetime());
1602
1603 const auto send = [=](Api::SendOptions options) {
1604 _sendCustomRequests.fire(std::move(options));
1605 };
1606
1607 SendMenu::SetupMenuAndShortcuts(
1608 _send.get(),
1609 [=] { return sendButtonMenuType(); },
1610 SendMenu::DefaultSilentCallback(send),
1611 SendMenu::DefaultScheduleCallback(_wrap.get(), sendMenuType(), send));
1612 }
1613
inlineBotResolveDone(const MTPcontacts_ResolvedPeer & result)1614 void ComposeControls::inlineBotResolveDone(
1615 const MTPcontacts_ResolvedPeer &result) {
1616 Expects(result.type() == mtpc_contacts_resolvedPeer);
1617
1618 _inlineBotResolveRequestId = 0;
1619 const auto &data = result.c_contacts_resolvedPeer();
1620 const auto resolvedBot = [&]() -> UserData* {
1621 if (const auto result = session().data().processUsers(data.vusers())) {
1622 if (result->isBot()
1623 && !result->botInfo->inlinePlaceholder.isEmpty()) {
1624 return result;
1625 }
1626 }
1627 return nullptr;
1628 }();
1629 session().data().processChats(data.vchats());
1630
1631 const auto query = ParseInlineBotQuery(&session(), _field);
1632 if (_inlineBotUsername == query.username) {
1633 applyInlineBotQuery(
1634 query.lookingUpBot ? resolvedBot : query.bot,
1635 query.query);
1636 } else {
1637 clearInlineBot();
1638 }
1639 }
1640
inlineBotResolveFail(const MTP::Error & error,const QString & username)1641 void ComposeControls::inlineBotResolveFail(
1642 const MTP::Error &error,
1643 const QString &username) {
1644 _inlineBotResolveRequestId = 0;
1645 if (username == _inlineBotUsername) {
1646 clearInlineBot();
1647 }
1648 }
1649
cancelInlineBot()1650 void ComposeControls::cancelInlineBot() {
1651 const auto &textWithTags = _field->getTextWithTags();
1652 if (textWithTags.text.size() > _inlineBotUsername.size() + 2) {
1653 setFieldText(
1654 { '@' + _inlineBotUsername + ' ', TextWithTags::Tags() },
1655 TextUpdateEvent::SaveDraft,
1656 Ui::InputField::HistoryAction::NewEntry);
1657 } else {
1658 clearFieldText(
1659 TextUpdateEvent::SaveDraft,
1660 Ui::InputField::HistoryAction::NewEntry);
1661 }
1662 }
1663
clearInlineBot()1664 void ComposeControls::clearInlineBot() {
1665 if (_inlineBot || _inlineLookingUpBot) {
1666 _inlineBot = nullptr;
1667 _inlineLookingUpBot = false;
1668 inlineBotChanged();
1669 _field->finishAnimating();
1670 }
1671 if (_inlineResults) {
1672 _inlineResults->clearInlineBot();
1673 }
1674 checkAutocomplete();
1675 }
1676
inlineBotChanged()1677 void ComposeControls::inlineBotChanged() {
1678 const auto isInlineBot = (_inlineBot && !_inlineLookingUpBot);
1679 if (_isInlineBot != isInlineBot) {
1680 _isInlineBot = isInlineBot;
1681 updateFieldPlaceholder();
1682 updateSubmitSettings();
1683 checkAutocomplete();
1684 }
1685 }
1686
initWriteRestriction()1687 void ComposeControls::initWriteRestriction() {
1688 _writeRestricted->resize(
1689 _writeRestricted->width(),
1690 st::historyUnblock.height);
1691 _writeRestricted->paintRequest(
1692 ) | rpl::start_with_next([=] {
1693 if (const auto error = _writeRestriction.current()) {
1694 auto p = Painter(_writeRestricted.get());
1695 drawRestrictedWrite(p, *error);
1696 }
1697 }, _wrap->lifetime());
1698
1699 _writeRestriction.value(
1700 ) | rpl::filter([=] {
1701 return _wrap->isHidden() || _writeRestricted->isHidden();
1702 }) | rpl::start_with_next([=] {
1703 updateWrappingVisibility();
1704 }, _wrap->lifetime());
1705 }
1706
initVoiceRecordBar()1707 void ComposeControls::initVoiceRecordBar() {
1708 _voiceRecordBar->recordingStateChanges(
1709 ) | rpl::start_with_next([=](bool active) {
1710 _field->setVisible(!active);
1711 }, _wrap->lifetime());
1712
1713 _voiceRecordBar->setStartRecordingFilter([=] {
1714 const auto error = _history
1715 ? Data::RestrictionError(
1716 _history->peer,
1717 ChatRestriction::SendMedia)
1718 : std::nullopt;
1719 if (error) {
1720 _window->show(Box<Ui::InformBox>(*error));
1721 return true;
1722 } else if (_showSlowmodeError && _showSlowmodeError()) {
1723 return true;
1724 }
1725 return false;
1726 });
1727
1728 {
1729 auto geometry = rpl::merge(
1730 _wrap->geometryValue(),
1731 _send->geometryValue()
1732 ) | rpl::map([=](QRect geometry) {
1733 auto r = _send->geometry();
1734 r.setY(r.y() + _wrap->y());
1735 return r;
1736 });
1737 _voiceRecordBar->setSendButtonGeometryValue(std::move(geometry));
1738 }
1739
1740 {
1741 auto bottom = _wrap->geometryValue(
1742 ) | rpl::map([=](QRect geometry) {
1743 return geometry.y() - st::historyRecordLockPosition.y();
1744 });
1745 _voiceRecordBar->setLockBottom(std::move(bottom));
1746 }
1747
1748 _voiceRecordBar->updateSendButtonTypeRequests(
1749 ) | rpl::start_with_next([=] {
1750 updateSendButtonType();
1751 }, _wrap->lifetime());
1752 }
1753
updateWrappingVisibility()1754 void ComposeControls::updateWrappingVisibility() {
1755 const auto restricted = _writeRestriction.current().has_value();
1756 _writeRestricted->setVisible(restricted);
1757 _wrap->setVisible(!restricted);
1758 if (!restricted) {
1759 _wrap->raise();
1760 }
1761 }
1762
computeSendButtonType() const1763 auto ComposeControls::computeSendButtonType() const {
1764 using Type = Ui::SendButton::Type;
1765
1766 if (_header->isEditingMessage()) {
1767 return Type::Save;
1768 } else if (_isInlineBot) {
1769 return Type::Cancel;
1770 } else if (showRecordButton()) {
1771 return Type::Record;
1772 }
1773 return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
1774 }
1775
sendMenuType() const1776 SendMenu::Type ComposeControls::sendMenuType() const {
1777 return !_history ? SendMenu::Type::Disabled : _sendMenuType;
1778 }
1779
sendButtonMenuType() const1780 SendMenu::Type ComposeControls::sendButtonMenuType() const {
1781 return (computeSendButtonType() == Ui::SendButton::Type::Send)
1782 ? sendMenuType()
1783 : SendMenu::Type::Disabled;
1784 }
1785
updateSendButtonType()1786 void ComposeControls::updateSendButtonType() {
1787 using Type = Ui::SendButton::Type;
1788 const auto type = computeSendButtonType();
1789 _send->setType(type);
1790
1791 const auto delay = [&] {
1792 return (type != Type::Cancel && type != Type::Save)
1793 ? _slowmodeSecondsLeft.current()
1794 : 0;
1795 }();
1796 _send->setSlowmodeDelay(delay);
1797 _send->setDisabled(_sendDisabledBySlowmode.current()
1798 && (type == Type::Send || type == Type::Record));
1799 }
1800
finishAnimating()1801 void ComposeControls::finishAnimating() {
1802 _send->finishAnimating();
1803 _voiceRecordBar->finishAnimating();
1804 }
1805
updateControlsGeometry(QSize size)1806 void ComposeControls::updateControlsGeometry(QSize size) {
1807 // _attachToggle -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel
1808 // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_silent|_botCommandStart) _tabbedSelectorToggle _send
1809
1810 const auto fieldWidth = size.width()
1811 - _attachToggle->width()
1812 - st::historySendRight
1813 - _send->width()
1814 - _tabbedSelectorToggle->width()
1815 - (_botCommandShown ? _botCommandStart->width() : 0)
1816 - (_silent ? _silent->width() : 0)
1817 - (_ttlInfo ? _ttlInfo->width() : 0);
1818 {
1819 const auto oldFieldHeight = _field->height();
1820 _field->resizeToWidth(fieldWidth);
1821 // If a height of the field is changed
1822 // then this method will be called with the updated size.
1823 if (oldFieldHeight != _field->height()) {
1824 return;
1825 }
1826 }
1827
1828 const auto buttonsTop = size.height() - _attachToggle->height();
1829
1830 auto left = st::historySendRight;
1831 _attachToggle->moveToLeft(left, buttonsTop);
1832 left += _attachToggle->width();
1833 _field->moveToLeft(
1834 left,
1835 size.height() - _field->height() - st::historySendPadding);
1836
1837 _header->resizeToWidth(size.width());
1838 _header->moveToLeft(
1839 0,
1840 _field->y() - _header->height() - st::historySendPadding);
1841
1842 auto right = st::historySendRight;
1843 _send->moveToRight(right, buttonsTop);
1844 right += _send->width();
1845 _tabbedSelectorToggle->moveToRight(right, buttonsTop);
1846 right += _tabbedSelectorToggle->width();
1847 _botCommandStart->moveToRight(right, buttonsTop);
1848 if (_botCommandShown) {
1849 right += _botCommandStart->width();
1850 }
1851 if (_silent) {
1852 _silent->moveToRight(right, buttonsTop);
1853 right += _silent->width();
1854 }
1855 if (_ttlInfo) {
1856 _ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop);
1857 }
1858
1859 _voiceRecordBar->resizeToWidth(size.width());
1860 _voiceRecordBar->moveToLeft(
1861 0,
1862 size.height() - _voiceRecordBar->height());
1863 }
1864
updateControlsVisibility()1865 void ComposeControls::updateControlsVisibility() {
1866 _botCommandStart->setVisible(_botCommandShown);
1867 if (_ttlInfo) {
1868 _ttlInfo->show();
1869 }
1870 }
1871
updateBotCommandShown()1872 bool ComposeControls::updateBotCommandShown() {
1873 auto shown = false;
1874 const auto peer = _history ? _history->peer.get() : nullptr;
1875 if (peer
1876 && ((peer->isChat() && peer->asChat()->botStatus > 0)
1877 || (peer->isMegagroup() && peer->asChannel()->mgInfo->botStatus > 0)
1878 || (peer->isUser() && peer->asUser()->isBot()))) {
1879 if (!HasSendText(_field)) {
1880 shown = true;
1881 }
1882 }
1883 if (_botCommandShown != shown) {
1884 _botCommandShown = shown;
1885 return true;
1886 }
1887 return false;
1888 }
1889
updateOuterGeometry(QRect rect)1890 void ComposeControls::updateOuterGeometry(QRect rect) {
1891 if (_inlineResults) {
1892 _inlineResults->moveBottom(rect.y());
1893 }
1894 if (_tabbedPanel) {
1895 _tabbedPanel->moveBottomRight(
1896 rect.y() + rect.height() - _attachToggle->height(),
1897 rect.x() + rect.width());
1898 }
1899 }
1900
updateMessagesTTLShown()1901 void ComposeControls::updateMessagesTTLShown() {
1902 const auto peer = _history ? _history->peer.get() : nullptr;
1903 const auto shown = peer && (peer->messagesTTL() > 0);
1904 if (!shown && _ttlInfo) {
1905 _ttlInfo = nullptr;
1906 updateControlsVisibility();
1907 updateControlsGeometry(_wrap->size());
1908 } else if (shown && !_ttlInfo) {
1909 _ttlInfo = std::make_unique<Controls::TTLButton>(_wrap.get(), peer);
1910 orderControls();
1911 updateControlsVisibility();
1912 updateControlsGeometry(_wrap->size());
1913 }
1914 }
1915
paintBackground(QRect clip)1916 void ComposeControls::paintBackground(QRect clip) {
1917 Painter p(_wrap.get());
1918
1919 p.fillRect(clip, st::historyComposeAreaBg);
1920 }
1921
escape()1922 void ComposeControls::escape() {
1923 if (const auto voice = _voiceRecordBar.get(); voice->isActive()) {
1924 voice->showDiscardBox(nullptr, anim::type::normal);
1925 } else {
1926 _cancelRequests.fire({});
1927 }
1928 }
1929
pushTabbedSelectorToThirdSection(not_null<PeerData * > peer,const Window::SectionShow & params)1930 bool ComposeControls::pushTabbedSelectorToThirdSection(
1931 not_null<PeerData*> peer,
1932 const Window::SectionShow ¶ms) {
1933 if (!_tabbedPanel) {
1934 return true;
1935 //} else if (!_canSendMessages) {
1936 // Core::App().settings().setTabbedReplacedWithInfo(true);
1937 // _window->showPeerInfo(_peer, params.withThirdColumn());
1938 // return;
1939 }
1940 Core::App().settings().setTabbedReplacedWithInfo(false);
1941 _tabbedSelectorToggle->setColorOverrides(
1942 &st::historyAttachEmojiActive,
1943 &st::historyRecordVoiceFgActive,
1944 &st::historyRecordVoiceRippleBgActive);
1945 _window->resizeForThirdSection();
1946 _window->showSection(
1947 std::make_shared<ChatHelpers::TabbedMemento>(),
1948 params.withThirdColumn());
1949 return true;
1950 }
1951
returnTabbedSelector()1952 bool ComposeControls::returnTabbedSelector() {
1953 createTabbedPanel();
1954 updateOuterGeometry(_wrap->geometry());
1955 return true;
1956 }
1957
createTabbedPanel()1958 void ComposeControls::createTabbedPanel() {
1959 setTabbedPanel(std::make_unique<ChatHelpers::TabbedPanel>(
1960 _parent,
1961 _window,
1962 _window->tabbedSelector()));
1963 }
1964
setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel)1965 void ComposeControls::setTabbedPanel(
1966 std::unique_ptr<ChatHelpers::TabbedPanel> panel) {
1967 _tabbedPanel = std::move(panel);
1968 if (const auto raw = _tabbedPanel.get()) {
1969 _tabbedSelectorToggle->installEventFilter(raw);
1970 _tabbedSelectorToggle->setColorOverrides(nullptr, nullptr, nullptr);
1971 } else {
1972 _tabbedSelectorToggle->setColorOverrides(
1973 &st::historyAttachEmojiActive,
1974 &st::historyRecordVoiceFgActive,
1975 &st::historyRecordVoiceRippleBgActive);
1976 }
1977 }
1978
toggleTabbedSelectorMode()1979 void ComposeControls::toggleTabbedSelectorMode() {
1980 if (!_history) {
1981 return;
1982 }
1983 if (_tabbedPanel) {
1984 if (_window->canShowThirdSection()
1985 && !_window->adaptive().isOneColumn()) {
1986 Core::App().settings().setTabbedSelectorSectionEnabled(true);
1987 Core::App().saveSettingsDelayed();
1988 pushTabbedSelectorToThirdSection(
1989 _history->peer,
1990 Window::SectionShow::Way::ClearStack);
1991 } else {
1992 _tabbedPanel->toggleAnimated();
1993 }
1994 } else {
1995 _window->closeThirdSection();
1996 }
1997 }
1998
updateHeight()1999 void ComposeControls::updateHeight() {
2000 const auto height = _field->height()
2001 + (_header->isDisplayed() ? _header->height() : 0)
2002 + 2 * st::historySendPadding;
2003 if (height != _wrap->height()) {
2004 _wrap->resize(_wrap->width(), height);
2005 }
2006 }
2007
editMessage(FullMsgId id)2008 void ComposeControls::editMessage(FullMsgId id) {
2009 if (const auto item = session().data().message(id)) {
2010 editMessage(item);
2011 }
2012 }
2013
editMessage(not_null<HistoryItem * > item)2014 void ComposeControls::editMessage(not_null<HistoryItem*> item) {
2015 Expects(_history != nullptr);
2016 Expects(draftKeyCurrent() != Data::DraftKey::None());
2017
2018 if (_voiceRecordBar->isActive()) {
2019 _window->show(Box<Ui::InformBox>(
2020 tr::lng_edit_caption_voice(tr::now)));
2021 return;
2022 }
2023
2024 if (!isEditingMessage()) {
2025 saveFieldToHistoryLocalDraft();
2026 }
2027 const auto editData = PrepareEditText(item);
2028 const auto cursor = MessageCursor{
2029 int(editData.text.size()),
2030 int(editData.text.size()),
2031 QFIXED_MAX
2032 };
2033 const auto previewPage = [&]() -> WebPageData* {
2034 if (const auto media = item->media()) {
2035 return media->webpage();
2036 }
2037 return nullptr;
2038 }();
2039 const auto previewState = previewPage
2040 ? Data::PreviewState::Allowed
2041 : Data::PreviewState::EmptyOnEdit;
2042 _history->setDraft(
2043 draftKey(DraftType::Edit),
2044 std::make_unique<Data::Draft>(
2045 editData,
2046 item->id,
2047 cursor,
2048 previewState));
2049 applyDraft();
2050
2051 if (_autocomplete) {
2052 InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); });
2053 }
2054 }
2055
cancelEditMessage()2056 void ComposeControls::cancelEditMessage() {
2057 Expects(_history != nullptr);
2058 Expects(draftKeyCurrent() != Data::DraftKey::None());
2059
2060 _history->clearDraft(draftKey(DraftType::Edit));
2061 applyDraft();
2062
2063 _saveDraftText = true;
2064 _saveDraftStart = crl::now();
2065 saveDraft();
2066 }
2067
replyToMessage(FullMsgId id)2068 void ComposeControls::replyToMessage(FullMsgId id) {
2069 Expects(_history != nullptr);
2070 Expects(draftKeyCurrent() != Data::DraftKey::None());
2071
2072 if (!id) {
2073 cancelReplyMessage();
2074 return;
2075 }
2076 if (isEditingMessage()) {
2077 const auto key = draftKey(DraftType::Normal);
2078 if (const auto localDraft = _history->draft(key)) {
2079 localDraft->msgId = id.msg;
2080 } else {
2081 _history->setDraft(
2082 key,
2083 std::make_unique<Data::Draft>(
2084 TextWithTags(),
2085 id.msg,
2086 MessageCursor(),
2087 Data::PreviewState::Allowed));
2088 }
2089 } else {
2090 _header->replyToMessage(id);
2091 }
2092
2093 _saveDraftText = true;
2094 _saveDraftStart = crl::now();
2095 saveDraft();
2096 }
2097
cancelReplyMessage()2098 void ComposeControls::cancelReplyMessage() {
2099 Expects(_history != nullptr);
2100 Expects(draftKeyCurrent() != Data::DraftKey::None());
2101
2102 const auto wasReply = replyingToMessage();
2103 _header->replyToMessage({});
2104 const auto key = draftKey(DraftType::Normal);
2105 if (const auto localDraft = _history->draft(key)) {
2106 if (localDraft->msgId) {
2107 if (localDraft->textWithTags.text.isEmpty()) {
2108 _history->clearDraft(key);
2109 } else {
2110 localDraft->msgId = 0;
2111 }
2112 }
2113 }
2114 if (wasReply) {
2115 _saveDraftText = true;
2116 _saveDraftStart = crl::now();
2117 saveDraft();
2118 }
2119 }
2120
handleCancelRequest()2121 bool ComposeControls::handleCancelRequest() {
2122 if (_isInlineBot) {
2123 cancelInlineBot();
2124 return true;
2125 } else if (isEditingMessage()) {
2126 cancelEditMessage();
2127 return true;
2128 } else if (_autocomplete && !_autocomplete->isHidden()) {
2129 _autocomplete->hideAnimated();
2130 return true;
2131 } else if (replyingToMessage()) {
2132 cancelReplyMessage();
2133 return true;
2134 }
2135 return false;
2136 }
2137
initWebpageProcess()2138 void ComposeControls::initWebpageProcess() {
2139 Expects(_history);
2140
2141 const auto peer = _history->peer;
2142 auto &lifetime = _wrap->lifetime();
2143 const auto requestRepaint = crl::guard(_header.get(), [=] {
2144 _header->update();
2145 });
2146
2147 const auto parsedLinks = lifetime.make_state<QStringList>();
2148 const auto previewLinks = lifetime.make_state<QString>();
2149 const auto previewData = lifetime.make_state<WebPageData*>(nullptr);
2150 using PreviewCache = std::map<QString, WebPageId>;
2151 const auto previewCache = lifetime.make_state<PreviewCache>();
2152 const auto previewRequest = lifetime.make_state<mtpRequestId>(0);
2153 const auto mtpSender =
2154 lifetime.make_state<MTP::Sender>(&_window->session().mtp());
2155
2156 const auto title = std::make_shared<rpl::event_stream<QString>>();
2157 const auto description = std::make_shared<rpl::event_stream<QString>>();
2158 const auto pageData = std::make_shared<rpl::event_stream<WebPageData*>>();
2159
2160 const auto previewTimer = lifetime.make_state<base::Timer>();
2161
2162 const auto updatePreview = [=] {
2163 previewTimer->cancel();
2164 auto t = QString();
2165 auto d = QString();
2166 if (ShowWebPagePreview(*previewData)) {
2167 if (const auto till = (*previewData)->pendingTill) {
2168 t = tr::lng_preview_loading(tr::now);
2169 d = QStringView(*previewLinks).split(' ').at(0).toString();
2170
2171 const auto timeout = till - base::unixtime::now();
2172 previewTimer->callOnce(
2173 std::max(timeout, 0) * crl::time(1000));
2174 } else {
2175 const auto preview = ProcessWebPageData(*previewData);
2176 t = preview.title;
2177 d = preview.description;
2178 }
2179 }
2180 title->fire_copy(t);
2181 description->fire_copy(d);
2182 pageData->fire_copy(*previewData);
2183 requestRepaint();
2184 };
2185
2186 const auto gotPreview = crl::guard(_wrap.get(), [=](
2187 const auto &result,
2188 QString links) {
2189 if (*previewRequest) {
2190 *previewRequest = 0;
2191 }
2192 result.match([=](const MTPDmessageMediaWebPage &d) {
2193 const auto page = _history->owner().processWebpage(d.vwebpage());
2194 previewCache->insert({ links, page->id });
2195 auto &till = page->pendingTill;
2196 if (till > 0 && till <= base::unixtime::now()) {
2197 till = -1;
2198 }
2199 if (links == *previewLinks
2200 && _previewState == Data::PreviewState::Allowed) {
2201 *previewData = (page->id && page->pendingTill >= 0)
2202 ? page.get()
2203 : nullptr;
2204 updatePreview();
2205 }
2206 }, [=](const MTPDmessageMediaEmpty &d) {
2207 previewCache->insert({ links, 0 });
2208 if (links == *previewLinks
2209 && _previewState == Data::PreviewState::Allowed) {
2210 *previewData = nullptr;
2211 updatePreview();
2212 }
2213 }, [](const auto &d) {
2214 });
2215 });
2216
2217 _previewCancel = [=] {
2218 mtpSender->request(base::take(*previewRequest)).cancel();
2219 *previewData = nullptr;
2220 previewLinks->clear();
2221 updatePreview();
2222 };
2223
2224 const auto getWebPagePreview = [=] {
2225 const auto links = *previewLinks;
2226 *previewRequest = mtpSender->request(MTPmessages_GetWebPagePreview(
2227 MTP_flags(0),
2228 MTP_string(links),
2229 MTPVector<MTPMessageEntity>()
2230 )).done([=](const MTPMessageMedia &result) {
2231 gotPreview(result, links);
2232 }).send();
2233 };
2234
2235 const auto checkPreview = crl::guard(_wrap.get(), [=] {
2236 const auto previewRestricted = peer
2237 && peer->amRestricted(ChatRestriction::EmbedLinks);
2238 if (_previewState != Data::PreviewState::Allowed
2239 || previewRestricted) {
2240 _previewCancel();
2241 return;
2242 }
2243 const auto newLinks = parsedLinks->join(' ');
2244 if (*previewLinks == newLinks) {
2245 return;
2246 }
2247 mtpSender->request(base::take(*previewRequest)).cancel();
2248 *previewLinks = newLinks;
2249 if (previewLinks->isEmpty()) {
2250 if (ShowWebPagePreview(*previewData)) {
2251 _previewCancel();
2252 }
2253 } else {
2254 const auto i = previewCache->find(*previewLinks);
2255 if (i == previewCache->end()) {
2256 getWebPagePreview();
2257 } else if (i->second) {
2258 *previewData = _history->owner().webpage(i->second);
2259 updatePreview();
2260 } else if (ShowWebPagePreview(*previewData)) {
2261 _previewCancel();
2262 }
2263 }
2264 });
2265
2266 previewTimer->setCallback([=] {
2267 if (!ShowWebPagePreview(*previewData) || previewLinks->isEmpty()) {
2268 return;
2269 }
2270 getWebPagePreview();
2271 });
2272
2273 session().changes().peerUpdates(
2274 Data::PeerUpdate::Flag::Rights
2275 | Data::PeerUpdate::Flag::Notifications
2276 | Data::PeerUpdate::Flag::MessagesTTL
2277 | Data::PeerUpdate::Flag::FullInfo
2278 ) | rpl::filter([=](const Data::PeerUpdate &update) {
2279 return (update.peer.get() == peer);
2280 }) | rpl::map([](const Data::PeerUpdate &update) {
2281 return update.flags;
2282 }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) {
2283 if (flags & Data::PeerUpdate::Flag::Rights) {
2284 checkPreview();
2285 updateStickersByEmoji();
2286 updateFieldPlaceholder();
2287 }
2288 if (flags & Data::PeerUpdate::Flag::Notifications) {
2289 updateSilentBroadcast();
2290 }
2291 if (flags & Data::PeerUpdate::Flag::MessagesTTL) {
2292 updateMessagesTTLShown();
2293 }
2294 if (flags & Data::PeerUpdate::Flag::FullInfo) {
2295 if (updateBotCommandShown()) {
2296 updateControlsVisibility();
2297 updateControlsGeometry(_wrap->size());
2298 }
2299 }
2300 }, lifetime);
2301
2302 session().downloaderTaskFinished(
2303 ) | rpl::filter([=] {
2304 return (*previewData)
2305 && ((*previewData)->document || (*previewData)->photo);
2306 }) | rpl::start_with_next((
2307 requestRepaint
2308 ), lifetime);
2309
2310 session().data().webPageUpdates(
2311 ) | rpl::filter([=](not_null<WebPageData*> page) {
2312 return (*previewData == page.get());
2313 }) | rpl::start_with_next([=] {
2314 updatePreview();
2315 }, lifetime);
2316
2317 const auto fieldLinksParser =
2318 lifetime.make_state<MessageLinksParser>(_field);
2319
2320 _previewSetState = [=](Data::PreviewState state) {
2321 // Save links from _field to _parsedLinks without generating preview.
2322 _previewState = Data::PreviewState::Cancelled;
2323 fieldLinksParser->parseNow();
2324 *parsedLinks = fieldLinksParser->list().current();
2325 _previewState = state;
2326 };
2327
2328 fieldLinksParser->list().changes(
2329 ) | rpl::start_with_next([=](QStringList &&parsed) {
2330 if (_previewState == Data::PreviewState::EmptyOnEdit
2331 && *parsedLinks != parsed) {
2332 _previewState = Data::PreviewState::Allowed;
2333 }
2334 *parsedLinks = std::move(parsed);
2335
2336 checkPreview();
2337 }, lifetime);
2338
2339 _header->previewRequested(
2340 title->events(),
2341 description->events(),
2342 pageData->events());
2343 }
2344
webPageId() const2345 WebPageId ComposeControls::webPageId() const {
2346 return _header->webPageId();
2347 }
2348
scrollRequests() const2349 rpl::producer<Data::MessagePosition> ComposeControls::scrollRequests() const {
2350 return _header->scrollToItemRequests(
2351 ) | rpl::map([=](FullMsgId id) -> Data::MessagePosition {
2352 if (const auto item = session().data().message(id)) {
2353 return item->position();
2354 }
2355 return {};
2356 });
2357 }
2358
isEditingMessage() const2359 bool ComposeControls::isEditingMessage() const {
2360 return _header->isEditingMessage();
2361 }
2362
replyingToMessage() const2363 FullMsgId ComposeControls::replyingToMessage() const {
2364 return _header->replyingToMessage();
2365 }
2366
isLockPresent() const2367 bool ComposeControls::isLockPresent() const {
2368 return _voiceRecordBar->isLockPresent();
2369 }
2370
lockShowStarts() const2371 rpl::producer<bool> ComposeControls::lockShowStarts() const {
2372 return _voiceRecordBar->lockShowStarts();
2373 }
2374
viewportEvents() const2375 rpl::producer<not_null<QEvent*>> ComposeControls::viewportEvents() const {
2376 return _voiceRecordBar->lockViewportEvents();
2377 }
2378
isRecording() const2379 bool ComposeControls::isRecording() const {
2380 return _voiceRecordBar->isRecording();
2381 }
2382
preventsClose(Fn<void ()> && continueCallback) const2383 bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const {
2384 if (_voiceRecordBar->isActive()) {
2385 _voiceRecordBar->showDiscardBox(std::move(continueCallback));
2386 return true;
2387 }
2388 return false;
2389 }
2390
hasSilentBroadcastToggle() const2391 bool ComposeControls::hasSilentBroadcastToggle() const {
2392 if (!_history) {
2393 return false;
2394 }
2395 const auto &peer = _history->peer;
2396 return peer
2397 && peer->isChannel()
2398 && !peer->isMegagroup()
2399 && peer->canWrite()
2400 && !session().data().notifySilentPostsUnknown(peer);
2401 }
2402
updateInlineBotQuery()2403 void ComposeControls::updateInlineBotQuery() {
2404 if (!_history) {
2405 return;
2406 }
2407 const auto query = ParseInlineBotQuery(&session(), _field);
2408 if (_inlineBotUsername != query.username) {
2409 _inlineBotUsername = query.username;
2410 auto &api = session().api();
2411 if (_inlineBotResolveRequestId) {
2412 api.request(_inlineBotResolveRequestId).cancel();
2413 _inlineBotResolveRequestId = 0;
2414 }
2415 if (query.lookingUpBot) {
2416 _inlineBot = nullptr;
2417 _inlineLookingUpBot = true;
2418 const auto username = _inlineBotUsername;
2419 _inlineBotResolveRequestId = api.request(
2420 MTPcontacts_ResolveUsername(MTP_string(username))
2421 ).done([=](const MTPcontacts_ResolvedPeer &result) {
2422 inlineBotResolveDone(result);
2423 }).fail([=](const MTP::Error &error) {
2424 inlineBotResolveFail(error, username);
2425 }).send();
2426 } else {
2427 applyInlineBotQuery(query.bot, query.query);
2428 }
2429 } else if (query.lookingUpBot) {
2430 if (!_inlineLookingUpBot) {
2431 applyInlineBotQuery(_inlineBot, query.query);
2432 }
2433 } else {
2434 applyInlineBotQuery(query.bot, query.query);
2435 }
2436 }
2437
applyInlineBotQuery(UserData * bot,const QString & query)2438 void ComposeControls::applyInlineBotQuery(
2439 UserData *bot,
2440 const QString &query) {
2441 if (_history && bot) {
2442 if (_inlineBot != bot) {
2443 _inlineBot = bot;
2444 _inlineLookingUpBot = false;
2445 inlineBotChanged();
2446 }
2447 if (!_inlineResults) {
2448 _inlineResults = std::make_unique<InlineBots::Layout::Widget>(
2449 _parent,
2450 _window);
2451 _inlineResults->setCurrentDialogsEntryState(
2452 _currentDialogsEntryState);
2453 _inlineResults->setResultSelectedCallback([=](
2454 InlineBots::ResultSelected result) {
2455 if (result.open) {
2456 const auto request = result.result->openRequest();
2457 if (const auto photo = request.photo()) {
2458 _window->openPhoto(photo, FullMsgId());
2459 } else if (const auto document = request.document()) {
2460 _window->openDocument(document, FullMsgId());
2461 }
2462 } else {
2463 _inlineResultChosen.fire_copy(result);
2464 }
2465 });
2466 _inlineResults->setSendMenuType([=] { return sendMenuType(); });
2467 _inlineResults->requesting(
2468 ) | rpl::start_with_next([=](bool requesting) {
2469 _tabbedSelectorToggle->setLoading(requesting);
2470 }, _inlineResults->lifetime());
2471 updateOuterGeometry(_wrap->geometry());
2472 }
2473 _inlineResults->queryInlineBot(_inlineBot, _history->peer, query);
2474 if (!_autocomplete->isHidden()) {
2475 _autocomplete->hideAnimated();
2476 }
2477 } else {
2478 clearInlineBot();
2479 }
2480 }
2481
restoreTextCallback(const QString & insertTextOnCancel) const2482 Fn<void()> ComposeControls::restoreTextCallback(
2483 const QString &insertTextOnCancel) const {
2484 const auto cursor = _field->textCursor();
2485 const auto position = cursor.position();
2486 const auto anchor = cursor.anchor();
2487 const auto text = getTextWithAppliedMarkdown();
2488
2489 _field->setTextWithTags({});
2490
2491 return crl::guard(_field, [=] {
2492 _field->setTextWithTags(text);
2493 auto cursor = _field->textCursor();
2494 cursor.setPosition(anchor);
2495 if (position != anchor) {
2496 cursor.setPosition(position, QTextCursor::KeepAnchor);
2497 }
2498 _field->setTextCursor(cursor);
2499 if (!insertTextOnCancel.isEmpty()) {
2500 _field->textCursor().insertText(insertTextOnCancel);
2501 }
2502 });
2503 }
2504
2505 } // namespace HistoryView
2506