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 &params) {
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