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/history_view_scheduled_section.h"
9 
10 #include "history/view/controls/history_view_compose_controls.h"
11 #include "history/view/history_view_empty_list_bubble.h"
12 #include "history/view/history_view_top_bar_widget.h"
13 #include "history/view/history_view_list_widget.h"
14 #include "history/view/history_view_schedule_box.h"
15 #include "history/history.h"
16 #include "history/history_drag_area.h"
17 #include "history/history_item.h"
18 #include "chat_helpers/send_context_menu.h" // SendMenu::Type.
19 #include "ui/widgets/scroll_area.h"
20 #include "ui/widgets/shadow.h"
21 #include "ui/layers/generic_box.h"
22 #include "ui/item_text_options.h"
23 #include "ui/toast/toast.h"
24 #include "ui/chat/chat_style.h"
25 #include "ui/chat/attach/attach_prepare.h"
26 #include "ui/chat/attach/attach_send_files_way.h"
27 #include "ui/special_buttons.h"
28 #include "ui/ui_utility.h"
29 #include "ui/text/text_utilities.h"
30 #include "ui/toasts/common_toasts.h"
31 #include "api/api_common.h"
32 #include "api/api_editing.h"
33 #include "api/api_sending.h"
34 #include "apiwrap.h"
35 #include "ui/boxes/confirm_box.h"
36 #include "boxes/delete_messages_box.h"
37 #include "boxes/edit_caption_box.h"
38 #include "boxes/send_files_box.h"
39 #include "window/window_adaptive.h"
40 #include "window/window_session_controller.h"
41 #include "window/window_peer_menu.h"
42 #include "base/event_filter.h"
43 #include "base/call_delayed.h"
44 #include "core/file_utilities.h"
45 #include "main/main_session.h"
46 #include "data/data_session.h"
47 #include "data/data_scheduled_messages.h"
48 #include "data/data_user.h"
49 #include "storage/storage_media_prepare.h"
50 #include "storage/storage_account.h"
51 #include "inline_bots/inline_bot_result.h"
52 #include "lang/lang_keys.h"
53 #include "facades.h"
54 #include "styles/style_chat.h"
55 #include "styles/style_window.h"
56 #include "styles/style_info.h"
57 #include "styles/style_boxes.h"
58 
59 #include <QtCore/QMimeData>
60 
61 namespace HistoryView {
62 namespace {
63 
CanSendFiles(not_null<const QMimeData * > data)64 bool CanSendFiles(not_null<const QMimeData*> data) {
65 	if (data->hasImage()) {
66 		return true;
67 	} else if (const auto urls = data->urls(); !urls.empty()) {
68 		if (ranges::all_of(urls, &QUrl::isLocalFile)) {
69 			return true;
70 		}
71 	}
72 	return false;
73 }
74 
75 } // namespace
76 
createWidget(QWidget * parent,not_null<Window::SessionController * > controller,Window::Column column,const QRect & geometry)77 object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
78 		QWidget *parent,
79 		not_null<Window::SessionController*> controller,
80 		Window::Column column,
81 		const QRect &geometry) {
82 	if (column == Window::Column::Third) {
83 		return nullptr;
84 	}
85 	auto result = object_ptr<ScheduledWidget>(parent, controller, _history);
86 	result->setInternalState(geometry, this);
87 	return result;
88 }
89 
ScheduledWidget(QWidget * parent,not_null<Window::SessionController * > controller,not_null<History * > history)90 ScheduledWidget::ScheduledWidget(
91 	QWidget *parent,
92 	not_null<Window::SessionController*> controller,
93 	not_null<History*> history)
94 : Window::SectionWidget(parent, controller, history->peer)
95 , _history(history)
96 , _scroll(
97 	this,
98 	controller->chatStyle()->value(lifetime(), st::historyScroll),
99 	false)
100 , _topBar(this, controller)
101 , _topBarShadow(this)
102 , _composeControls(std::make_unique<ComposeControls>(
103 	this,
104 	controller,
105 	ComposeControls::Mode::Scheduled,
106 	SendMenu::Type::Disabled))
107 , _scrollDown(
108 		_scroll,
109 		controller->chatStyle()->value(lifetime(), st::historyToDown)) {
110 	controller->chatStyle()->paletteChanged(
111 	) | rpl::start_with_next([=] {
112 		_scroll->updateBars();
113 	}, _scroll->lifetime());
114 
115 	Window::ChatThemeValueFromPeer(
116 		controller,
117 		history->peer
118 	) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
119 		_theme = std::move(theme);
120 		controller->setChatStyleTheme(_theme);
121 	}, lifetime());
122 
123 	const auto state = Dialogs::EntryState{
124 		.key = _history,
125 		.section = Dialogs::EntryState::Section::Scheduled,
126 	};
127 	_topBar->setActiveChat(state, nullptr);
128 	_composeControls->setCurrentDialogsEntryState(state);
129 
130 	_topBar->move(0, 0);
131 	_topBar->resizeToWidth(width());
132 	_topBar->show();
133 
134 	_topBar->sendNowSelectionRequest(
135 	) | rpl::start_with_next([=] {
136 		confirmSendNowSelected();
137 	}, _topBar->lifetime());
138 	_topBar->deleteSelectionRequest(
139 	) | rpl::start_with_next([=] {
140 		confirmDeleteSelected();
141 	}, _topBar->lifetime());
142 	_topBar->clearSelectionRequest(
143 	) | rpl::start_with_next([=] {
144 		clearSelected();
145 	}, _topBar->lifetime());
146 
147 	_topBarShadow->raise();
148 	controller->adaptive().value(
149 	) | rpl::start_with_next([=] {
150 		updateAdaptiveLayout();
151 	}, lifetime());
152 
153 	_inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(
154 		this,
155 		controller,
156 		static_cast<ListDelegate*>(this)));
157 	_scroll->move(0, _topBar->height());
158 	_scroll->show();
159 	_scroll->scrolls(
160 	) | rpl::start_with_next([=] {
161 		onScroll();
162 	}, lifetime());
163 
164 	_inner->editMessageRequested(
165 	) | rpl::start_with_next([=](auto fullId) {
166 		if (const auto item = session().data().message(fullId)) {
167 			const auto media = item->media();
168 			if (media && !media->webpage()) {
169 				if (media->allowsEditCaption()) {
170 					controller->show(Box<EditCaptionBox>(controller, item));
171 				}
172 			} else {
173 				_composeControls->editMessage(fullId);
174 			}
175 		}
176 	}, _inner->lifetime());
177 
178 	{
179 		auto emptyInfo = base::make_unique_q<EmptyListBubbleWidget>(
180 			_inner,
181 			controller->chatStyle(),
182 			st::msgServicePadding);
183 		const auto emptyText = Ui::Text::Semibold(
184 			tr::lng_scheduled_messages_empty(tr::now));
185 		emptyInfo->setText(emptyText);
186 		_inner->setEmptyInfoWidget(std::move(emptyInfo));
187 	}
188 
189 	setupScrollDownButton();
190 	setupComposeControls();
191 }
192 
193 ScheduledWidget::~ScheduledWidget() = default;
194 
setupComposeControls()195 void ScheduledWidget::setupComposeControls() {
196 	_composeControls->setHistory({ .history = _history.get() });
197 
198 	_composeControls->height(
199 	) | rpl::start_with_next([=] {
200 		const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());
201 		updateControlsGeometry();
202 		if (wasMax) {
203 			listScrollTo(_scroll->scrollTopMax());
204 		}
205 	}, lifetime());
206 
207 	_composeControls->cancelRequests(
208 	) | rpl::start_with_next([=] {
209 		listCancelRequest();
210 	}, lifetime());
211 
212 	_composeControls->sendRequests(
213 	) | rpl::start_with_next([=] {
214 		send();
215 	}, lifetime());
216 
217 	_composeControls->sendVoiceRequests(
218 	) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
219 		sendVoice(data.bytes, data.waveform, data.duration);
220 	}, lifetime());
221 
222 	_composeControls->sendCommandRequests(
223 	) | rpl::start_with_next([=](const QString &command) {
224 		listSendBotCommand(command, FullMsgId());
225 	}, lifetime());
226 
227 	const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
228 	_composeControls->editRequests(
229 	) | rpl::start_with_next([=](auto data) {
230 		if (const auto item = session().data().message(data.fullId)) {
231 			if (item->isScheduled()) {
232 				edit(item, data.options, saveEditMsgRequestId);
233 			}
234 		}
235 	}, lifetime());
236 
237 	_composeControls->attachRequests(
238 	) | rpl::filter([=] {
239 		return !_choosingAttach;
240 	}) | rpl::start_with_next([=] {
241 		_choosingAttach = true;
242 		base::call_delayed(
243 			st::historyAttach.ripple.hideDuration,
244 			this,
245 			[=] { _choosingAttach = false; chooseAttach(); });
246 	}, lifetime());
247 
248 	using Selector = ChatHelpers::TabbedSelector;
249 
250 	_composeControls->fileChosen(
251 	) | rpl::start_with_next([=](Selector::FileChosen chosen) {
252 		sendExistingDocument(chosen.document);
253 	}, lifetime());
254 
255 	_composeControls->photoChosen(
256 	) | rpl::start_with_next([=](Selector::PhotoChosen chosen) {
257 		sendExistingPhoto(chosen.photo);
258 	}, lifetime());
259 
260 	_composeControls->inlineResultChosen(
261 	) | rpl::start_with_next([=](Selector::InlineChosen chosen) {
262 		sendInlineResult(chosen.result, chosen.bot);
263 	}, lifetime());
264 
265 	_composeControls->scrollRequests(
266 	) | rpl::start_with_next([=](Data::MessagePosition pos) {
267 		showAtPosition(pos);
268 	}, lifetime());
269 
270 	_composeControls->scrollKeyEvents(
271 	) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
272 		_scroll->keyPressEvent(e);
273 	}, lifetime());
274 
275 	_composeControls->editLastMessageRequests(
276 	) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
277 		if (!_inner->lastMessageEditRequestNotify()) {
278 			_scroll->keyPressEvent(e);
279 		}
280 	}, lifetime());
281 
282 	_composeControls->setMimeDataHook([=](
283 			not_null<const QMimeData*> data,
284 			Ui::InputField::MimeAction action) {
285 		if (action == Ui::InputField::MimeAction::Check) {
286 			return CanSendFiles(data);
287 		} else if (action == Ui::InputField::MimeAction::Insert) {
288 			return confirmSendingFiles(data, std::nullopt, data->text());
289 		}
290 		Unexpected("action in MimeData hook.");
291 	});
292 
293 	_composeControls->lockShowStarts(
294 	) | rpl::start_with_next([=] {
295 		updateScrollDownVisibility();
296 	}, lifetime());
297 
298 	_composeControls->viewportEvents(
299 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
300 		_scroll->viewportEvent(e);
301 	}, lifetime());
302 }
303 
chooseAttach()304 void ScheduledWidget::chooseAttach() {
305 	if (const auto error = Data::RestrictionError(
306 			_history->peer,
307 			ChatRestriction::SendMedia)) {
308 		Ui::ShowMultilineToast({
309 			.text = { *error },
310 		});
311 		return;
312 	}
313 
314 	const auto filter = FileDialog::AllOrImagesFilter();
315 	FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](
316 			FileDialog::OpenResult &&result) {
317 		if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
318 			return;
319 		}
320 
321 		if (!result.remoteContent.isEmpty()) {
322 			auto read = Images::Read({
323 				.content = result.remoteContent,
324 			});
325 			if (!read.image.isNull() && !read.animated) {
326 				confirmSendingFiles(
327 					std::move(read.image),
328 					std::move(result.remoteContent));
329 			} else {
330 				uploadFile(result.remoteContent, SendMediaType::File);
331 			}
332 		} else {
333 			auto list = Storage::PrepareMediaList(
334 				result.paths,
335 				st::sendMediaPreviewSize);
336 			confirmSendingFiles(std::move(list));
337 		}
338 	}), nullptr);
339 }
340 
confirmSendingFiles(not_null<const QMimeData * > data,std::optional<bool> overrideSendImagesAsPhotos,const QString & insertTextOnCancel)341 bool ScheduledWidget::confirmSendingFiles(
342 		not_null<const QMimeData*> data,
343 		std::optional<bool> overrideSendImagesAsPhotos,
344 		const QString &insertTextOnCancel) {
345 	const auto hasImage = data->hasImage();
346 
347 	if (const auto urls = data->urls(); !urls.empty()) {
348 		auto list = Storage::PrepareMediaList(
349 			urls,
350 			st::sendMediaPreviewSize);
351 		if (list.error != Ui::PreparedList::Error::NonLocalUrl) {
352 			if (list.error == Ui::PreparedList::Error::None
353 				|| !hasImage) {
354 				const auto emptyTextOnCancel = QString();
355 				list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
356 				confirmSendingFiles(std::move(list), emptyTextOnCancel);
357 				return true;
358 			}
359 		}
360 	}
361 
362 	if (hasImage) {
363 		auto image = qvariant_cast<QImage>(data->imageData());
364 		if (!image.isNull()) {
365 			confirmSendingFiles(
366 				std::move(image),
367 				QByteArray(),
368 				overrideSendImagesAsPhotos,
369 				insertTextOnCancel);
370 			return true;
371 		}
372 	}
373 	return false;
374 }
375 
confirmSendingFiles(Ui::PreparedList && list,const QString & insertTextOnCancel)376 bool ScheduledWidget::confirmSendingFiles(
377 		Ui::PreparedList &&list,
378 		const QString &insertTextOnCancel) {
379 	if (showSendingFilesError(list)) {
380 		return false;
381 	}
382 
383 	using SendLimit = SendFilesBox::SendLimit;
384 	auto box = Box<SendFilesBox>(
385 		controller(),
386 		std::move(list),
387 		_composeControls->getTextWithAppliedMarkdown(),
388 		_history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many,
389 		CanScheduleUntilOnline(_history->peer)
390 			? Api::SendType::ScheduledToUser
391 			: Api::SendType::Scheduled,
392 		SendMenu::Type::Disabled);
393 
394 	box->setConfirmedCallback(crl::guard(this, [=](
395 			Ui::PreparedList &&list,
396 			Ui::SendFilesWay way,
397 			TextWithTags &&caption,
398 			Api::SendOptions options,
399 			bool ctrlShiftEnter) {
400 		sendingFilesConfirmed(
401 			std::move(list),
402 			way,
403 			std::move(caption),
404 			options,
405 			ctrlShiftEnter);
406 	}));
407 	box->setCancelledCallback(_composeControls->restoreTextCallback(
408 		insertTextOnCancel));
409 
410 	//ActivateWindow(controller());
411 	const auto shown = controller()->show(std::move(box));
412 	shown->setCloseByOutsideClick(false);
413 
414 	return true;
415 }
416 
sendingFilesConfirmed(Ui::PreparedList && list,Ui::SendFilesWay way,TextWithTags && caption,Api::SendOptions options,bool ctrlShiftEnter)417 void ScheduledWidget::sendingFilesConfirmed(
418 		Ui::PreparedList &&list,
419 		Ui::SendFilesWay way,
420 		TextWithTags &&caption,
421 		Api::SendOptions options,
422 		bool ctrlShiftEnter) {
423 	Expects(list.filesToProcess.empty());
424 
425 	if (showSendingFilesError(list)) {
426 		return;
427 	}
428 	auto groups = DivideByGroups(std::move(list), way, false);
429 	const auto type = way.sendImagesAsPhotos()
430 		? SendMediaType::Photo
431 		: SendMediaType::File;
432 	auto action = Api::SendAction(_history);
433 	action.options = options;
434 	action.clearDraft = false;
435 	if ((groups.size() != 1 || !groups.front().sentWithCaption())
436 		&& !caption.text.isEmpty()) {
437 		auto message = Api::MessageToSend(_history);
438 		message.textWithTags = base::take(caption);
439 		message.action = action;
440 		session().api().sendMessage(std::move(message));
441 	}
442 	for (auto &group : groups) {
443 		const auto album = (group.type != Ui::AlbumType::None)
444 			? std::make_shared<SendingAlbum>()
445 			: nullptr;
446 		session().api().sendFiles(
447 			std::move(group.list),
448 			type,
449 			base::take(caption),
450 			album,
451 			action);
452 	}
453 }
454 
confirmSendingFiles(QImage && image,QByteArray && content,std::optional<bool> overrideSendImagesAsPhotos,const QString & insertTextOnCancel)455 bool ScheduledWidget::confirmSendingFiles(
456 		QImage &&image,
457 		QByteArray &&content,
458 		std::optional<bool> overrideSendImagesAsPhotos,
459 		const QString &insertTextOnCancel) {
460 	if (image.isNull()) {
461 		return false;
462 	}
463 
464 	auto list = Storage::PrepareMediaFromImage(
465 		std::move(image),
466 		std::move(content),
467 		st::sendMediaPreviewSize);
468 	list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
469 	return confirmSendingFiles(std::move(list), insertTextOnCancel);
470 }
471 
uploadFile(const QByteArray & fileContent,SendMediaType type)472 void ScheduledWidget::uploadFile(
473 		const QByteArray &fileContent,
474 		SendMediaType type) {
475 	const auto callback = [=](Api::SendOptions options) {
476 		auto action = Api::SendAction(_history);
477 		//action.replyTo = replyToId();
478 		action.options = options;
479 		session().api().sendFile(fileContent, type, action);
480 	};
481 	controller()->show(
482 		PrepareScheduleBox(this, sendMenuType(), callback),
483 		Ui::LayerOption::KeepOther);
484 }
485 
showSendingFilesError(const Ui::PreparedList & list) const486 bool ScheduledWidget::showSendingFilesError(
487 		const Ui::PreparedList &list) const {
488 	const auto text = [&] {
489 		const auto error = Data::RestrictionError(
490 			_history->peer,
491 			ChatRestriction::SendMedia);
492 		if (error) {
493 			return *error;
494 		}
495 		using Error = Ui::PreparedList::Error;
496 		switch (list.error) {
497 		case Error::None: return QString();
498 		case Error::EmptyFile:
499 		case Error::Directory:
500 		case Error::NonLocalUrl: return tr::lng_send_image_empty(
501 			tr::now,
502 			lt_name,
503 			list.errorData);
504 		case Error::TooLargeFile: return tr::lng_send_image_too_large(
505 			tr::now,
506 			lt_name,
507 			list.errorData);
508 		}
509 		return tr::lng_forward_send_files_cant(tr::now);
510 	}();
511 	if (text.isEmpty()) {
512 		return false;
513 	}
514 
515 	Ui::ShowMultilineToast({
516 		.text = { text },
517 	});
518 	return true;
519 }
520 
send()521 void ScheduledWidget::send() {
522 	if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) {
523 		return;
524 	}
525 	const auto callback = [=](Api::SendOptions options) { send(options); };
526 	controller()->show(
527 		PrepareScheduleBox(this, sendMenuType(), callback),
528 		Ui::LayerOption::KeepOther);
529 }
530 
send(Api::SendOptions options)531 void ScheduledWidget::send(Api::SendOptions options) {
532 	const auto webPageId = _composeControls->webPageId();
533 
534 	auto message = ApiWrap::MessageToSend(_history);
535 	message.textWithTags = _composeControls->getTextWithAppliedMarkdown();
536 	message.action.options = options;
537 	//message.action.replyTo = replyToId();
538 	message.webPageId = webPageId;
539 
540 	//const auto error = GetErrorTextForSending(
541 	//	_peer,
542 	//	_toForward,
543 	//	message.textWithTags);
544 	//if (!error.isEmpty()) {
545 	//	Ui::ShowMultilineToast({
546 	//		.text = { error },
547 	//	});
548 	//	return;
549 	//}
550 
551 	session().api().sendMessage(std::move(message));
552 
553 	_composeControls->clear();
554 	//_saveDraftText = true;
555 	//_saveDraftStart = crl::now();
556 	//onDraftSave();
557 
558 	_composeControls->hidePanelsAnimated();
559 
560 	//if (_previewData && _previewData->pendingTill) previewCancel();
561 	_composeControls->focus();
562 }
563 
sendVoice(QByteArray bytes,VoiceWaveform waveform,int duration)564 void ScheduledWidget::sendVoice(
565 		QByteArray bytes,
566 		VoiceWaveform waveform,
567 		int duration) {
568 	const auto callback = [=](Api::SendOptions options) {
569 		sendVoice(bytes, waveform, duration, options);
570 	};
571 	controller()->show(
572 		PrepareScheduleBox(this, sendMenuType(), callback),
573 		Ui::LayerOption::KeepOther);
574 }
575 
sendVoice(QByteArray bytes,VoiceWaveform waveform,int duration,Api::SendOptions options)576 void ScheduledWidget::sendVoice(
577 		QByteArray bytes,
578 		VoiceWaveform waveform,
579 		int duration,
580 		Api::SendOptions options) {
581 	auto action = Api::SendAction(_history);
582 	action.options = options;
583 	session().api().sendVoiceMessage(bytes, waveform, duration, action);
584 	_composeControls->clearListenState();
585 }
586 
edit(not_null<HistoryItem * > item,Api::SendOptions options,mtpRequestId * const saveEditMsgRequestId)587 void ScheduledWidget::edit(
588 		not_null<HistoryItem*> item,
589 		Api::SendOptions options,
590 		mtpRequestId *const saveEditMsgRequestId) {
591 	if (*saveEditMsgRequestId) {
592 		return;
593 	}
594 	const auto textWithTags = _composeControls->getTextWithAppliedMarkdown();
595 	const auto prepareFlags = Ui::ItemTextOptions(
596 		_history,
597 		session().user()).flags;
598 	auto sending = TextWithEntities();
599 	auto left = TextWithEntities {
600 		textWithTags.text,
601 		TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };
602 	TextUtilities::PrepareForSending(left, prepareFlags);
603 
604 	if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) {
605 		if (item) {
606 			controller()->show(Box<DeleteMessagesBox>(item, false));
607 		} else {
608 			_composeControls->focus();
609 		}
610 		return;
611 	} else if (!left.text.isEmpty()) {
612 		controller()->show(Box<Ui::InformBox>(
613 			tr::lng_edit_too_long(tr::now)));
614 		return;
615 	}
616 
617 	lifetime().add([=] {
618 		if (!*saveEditMsgRequestId) {
619 			return;
620 		}
621 		session().api().request(base::take(*saveEditMsgRequestId)).cancel();
622 	});
623 
624 	const auto done = [=](const MTPUpdates &result, mtpRequestId requestId) {
625 		if (requestId == *saveEditMsgRequestId) {
626 			*saveEditMsgRequestId = 0;
627 			_composeControls->cancelEditMessage();
628 		}
629 	};
630 
631 	const auto fail = [=](const MTP::Error &error, mtpRequestId requestId) {
632 		if (requestId == *saveEditMsgRequestId) {
633 			*saveEditMsgRequestId = 0;
634 		}
635 
636 		const auto &err = error.type();
637 		if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) {
638 			controller()->show(Box<Ui::InformBox>(
639 				tr::lng_edit_error(tr::now)));
640 		} else if (err == u"MESSAGE_NOT_MODIFIED"_q) {
641 			_composeControls->cancelEditMessage();
642 		} else if (err == u"MESSAGE_EMPTY"_q) {
643 			_composeControls->focus();
644 		} else {
645 			controller()->show(Box<Ui::InformBox>(
646 				tr::lng_edit_error(tr::now)));
647 		}
648 		update();
649 		return true;
650 	};
651 
652 	*saveEditMsgRequestId = Api::EditTextMessage(
653 		item,
654 		sending,
655 		options,
656 		crl::guard(this, done),
657 		crl::guard(this, fail));
658 
659 	_composeControls->hidePanelsAnimated();
660 	_composeControls->focus();
661 }
662 
sendExistingDocument(not_null<DocumentData * > document)663 void ScheduledWidget::sendExistingDocument(
664 		not_null<DocumentData*> document) {
665 	const auto callback = [=](Api::SendOptions options) {
666 		sendExistingDocument(document, options);
667 	};
668 	controller()->show(
669 		PrepareScheduleBox(this, sendMenuType(), callback),
670 		Ui::LayerOption::KeepOther);
671 }
672 
sendExistingDocument(not_null<DocumentData * > document,Api::SendOptions options)673 bool ScheduledWidget::sendExistingDocument(
674 		not_null<DocumentData*> document,
675 		Api::SendOptions options) {
676 	const auto error = Data::RestrictionError(
677 		_history->peer,
678 		ChatRestriction::SendStickers);
679 	if (error) {
680 		controller()->show(
681 			Box<Ui::InformBox>(*error),
682 			Ui::LayerOption::KeepOther);
683 		return false;
684 	}
685 
686 	auto message = Api::MessageToSend(_history);
687 	//message.action.replyTo = replyToId();
688 	message.action.options = options;
689 	Api::SendExistingDocument(std::move(message), document);
690 
691 	_composeControls->hidePanelsAnimated();
692 	_composeControls->focus();
693 	return true;
694 }
695 
sendExistingPhoto(not_null<PhotoData * > photo)696 void ScheduledWidget::sendExistingPhoto(not_null<PhotoData*> photo) {
697 	const auto callback = [=](Api::SendOptions options) {
698 		sendExistingPhoto(photo, options);
699 	};
700 	controller()->show(
701 		PrepareScheduleBox(this, sendMenuType(), callback),
702 		Ui::LayerOption::KeepOther);
703 }
704 
sendExistingPhoto(not_null<PhotoData * > photo,Api::SendOptions options)705 bool ScheduledWidget::sendExistingPhoto(
706 		not_null<PhotoData*> photo,
707 		Api::SendOptions options) {
708 	const auto error = Data::RestrictionError(
709 		_history->peer,
710 		ChatRestriction::SendMedia);
711 	if (error) {
712 		controller()->show(
713 			Box<Ui::InformBox>(*error),
714 			Ui::LayerOption::KeepOther);
715 		return false;
716 	}
717 
718 	auto message = Api::MessageToSend(_history);
719 	//message.action.replyTo = replyToId();
720 	message.action.options = options;
721 	Api::SendExistingPhoto(std::move(message), photo);
722 
723 	_composeControls->hidePanelsAnimated();
724 	_composeControls->focus();
725 	return true;
726 }
727 
sendInlineResult(not_null<InlineBots::Result * > result,not_null<UserData * > bot)728 void ScheduledWidget::sendInlineResult(
729 		not_null<InlineBots::Result*> result,
730 		not_null<UserData*> bot) {
731 	const auto errorText = result->getErrorOnSend(_history);
732 	if (!errorText.isEmpty()) {
733 		controller()->show(Box<Ui::InformBox>(errorText));
734 		return;
735 	}
736 	const auto callback = [=](Api::SendOptions options) {
737 		sendInlineResult(result, bot, options);
738 	};
739 	controller()->show(
740 		PrepareScheduleBox(this, sendMenuType(), callback),
741 		Ui::LayerOption::KeepOther);
742 }
743 
sendInlineResult(not_null<InlineBots::Result * > result,not_null<UserData * > bot,Api::SendOptions options)744 void ScheduledWidget::sendInlineResult(
745 		not_null<InlineBots::Result*> result,
746 		not_null<UserData*> bot,
747 		Api::SendOptions options) {
748 	auto action = Api::SendAction(_history);
749 	//action.replyTo = replyToId();
750 	action.options = options;
751 	action.generateLocal = true;
752 	session().api().sendInlineResult(bot, result, action);
753 
754 	_composeControls->clear();
755 	//_saveDraftText = true;
756 	//_saveDraftStart = crl::now();
757 	//onDraftSave();
758 
759 	auto &bots = cRefRecentInlineBots();
760 	const auto index = bots.indexOf(bot);
761 	if (index) {
762 		if (index > 0) {
763 			bots.removeAt(index);
764 		} else if (bots.size() >= RecentInlineBotsLimit) {
765 			bots.resize(RecentInlineBotsLimit - 1);
766 		}
767 		bots.push_front(bot);
768 		bot->session().local().writeRecentHashtagsAndBots();
769 	}
770 
771 	_composeControls->hidePanelsAnimated();
772 	_composeControls->focus();
773 }
774 
sendMenuType() const775 SendMenu::Type ScheduledWidget::sendMenuType() const {
776 	return _history->peer->isSelf()
777 		? SendMenu::Type::Reminder
778 		: HistoryView::CanScheduleUntilOnline(_history->peer)
779 		? SendMenu::Type::ScheduledToUser
780 		: SendMenu::Type::Scheduled;
781 }
782 
setupScrollDownButton()783 void ScheduledWidget::setupScrollDownButton() {
784 	_scrollDown->setClickedCallback([=] {
785 		scrollDownClicked();
786 	});
787 	base::install_event_filter(_scrollDown, [=](not_null<QEvent*> event) {
788 		if (event->type() != QEvent::Wheel) {
789 			return base::EventFilterResult::Continue;
790 		}
791 		return _scroll->viewportEvent(event)
792 			? base::EventFilterResult::Cancel
793 			: base::EventFilterResult::Continue;
794 	});
795 	updateScrollDownVisibility();
796 }
797 
scrollDownClicked()798 void ScheduledWidget::scrollDownClicked() {
799 	showAtPosition(Data::MaxMessagePosition);
800 }
801 
showAtPosition(Data::MessagePosition position)802 void ScheduledWidget::showAtPosition(Data::MessagePosition position) {
803 	if (showAtPositionNow(position)) {
804 		if (const auto highlight = base::take(_highlightMessageId)) {
805 			_inner->highlightMessage(highlight);
806 		}
807 	} else {
808 		_nextAnimatedScrollPosition = position;
809 		_nextAnimatedScrollDelta = _inner->isBelowPosition(position)
810 			? -_scroll->height()
811 			: _inner->isAbovePosition(position)
812 			? _scroll->height()
813 			: 0;
814 		auto memento = HistoryView::ListMemento(position);
815 		_inner->restoreState(&memento);
816 	}
817 }
818 
showAtPositionNow(Data::MessagePosition position)819 bool ScheduledWidget::showAtPositionNow(Data::MessagePosition position) {
820 	if (const auto scrollTop = _inner->scrollTopForPosition(position)) {
821 		const auto currentScrollTop = _scroll->scrollTop();
822 		const auto wanted = std::clamp(
823 			*scrollTop,
824 			0,
825 			_scroll->scrollTopMax());
826 		const auto fullDelta = (wanted - currentScrollTop);
827 		const auto limit = _scroll->height();
828 		const auto scrollDelta = std::clamp(fullDelta, -limit, limit);
829 		_inner->scrollTo(
830 			wanted,
831 			position,
832 			scrollDelta,
833 			(std::abs(fullDelta) > limit
834 				? HistoryView::ListWidget::AnimatedScroll::Part
835 				: HistoryView::ListWidget::AnimatedScroll::Full));
836 		return true;
837 	}
838 	return false;
839 }
840 
updateScrollDownVisibility()841 void ScheduledWidget::updateScrollDownVisibility() {
842 	if (animating()) {
843 		return;
844 	}
845 
846 	const auto scrollDownIsVisible = [&]() -> std::optional<bool> {
847 		if (_composeControls->isLockPresent()) {
848 			return false;
849 		}
850 		const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
851 		if (top < _scroll->scrollTopMax()) {
852 			return true;
853 		}
854 		if (_inner->loadedAtBottomKnown()) {
855 			return !_inner->loadedAtBottom();
856 		}
857 		return std::nullopt;
858 	};
859 	const auto scrollDownIsShown = scrollDownIsVisible();
860 	if (!scrollDownIsShown) {
861 		return;
862 	}
863 	if (_scrollDownIsShown != *scrollDownIsShown) {
864 		_scrollDownIsShown = *scrollDownIsShown;
865 		_scrollDownShown.start(
866 			[=] { updateScrollDownPosition(); },
867 			_scrollDownIsShown ? 0. : 1.,
868 			_scrollDownIsShown ? 1. : 0.,
869 			st::historyToDownDuration);
870 	}
871 }
872 
updateScrollDownPosition()873 void ScheduledWidget::updateScrollDownPosition() {
874 	// _scrollDown is a child widget of _scroll, not me.
875 	auto top = anim::interpolate(
876 		0,
877 		_scrollDown->height() + st::historyToDownPosition.y(),
878 		_scrollDownShown.value(_scrollDownIsShown ? 1. : 0.));
879 	_scrollDown->moveToRight(
880 		st::historyToDownPosition.x(),
881 		_scroll->height() - top);
882 	auto shouldBeHidden = !_scrollDownIsShown && !_scrollDownShown.animating();
883 	if (shouldBeHidden != _scrollDown->isHidden()) {
884 		_scrollDown->setVisible(!shouldBeHidden);
885 	}
886 }
887 
scrollDownAnimationFinish()888 void ScheduledWidget::scrollDownAnimationFinish() {
889 	_scrollDownShown.stop();
890 	updateScrollDownPosition();
891 }
892 
updateAdaptiveLayout()893 void ScheduledWidget::updateAdaptiveLayout() {
894 	_topBarShadow->moveToLeft(
895 		controller()->adaptive().isOneColumn() ? 0 : st::lineWidth,
896 		_topBar->height());
897 }
898 
history() const899 not_null<History*> ScheduledWidget::history() const {
900 	return _history;
901 }
902 
activeChat() const903 Dialogs::RowDescriptor ScheduledWidget::activeChat() const {
904 	return {
905 		_history,
906 		FullMsgId(_history->channelId(), ShowAtUnreadMsgId)
907 	};
908 }
909 
preventsClose(Fn<void ()> && continueCallback) const910 bool ScheduledWidget::preventsClose(Fn<void()> &&continueCallback) const {
911 	return _composeControls->preventsClose(std::move(continueCallback));
912 }
913 
grabForShowAnimation(const Window::SectionSlideParams & params)914 QPixmap ScheduledWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {
915 	_topBar->updateControlsVisibility();
916 	if (params.withTopBarShadow) _topBarShadow->hide();
917 	_composeControls->showForGrab();
918 	auto result = Ui::GrabWidget(this);
919 	if (params.withTopBarShadow) _topBarShadow->show();
920 	return result;
921 }
922 
doSetInnerFocus()923 void ScheduledWidget::doSetInnerFocus() {
924 	_composeControls->focus();
925 }
926 
showInternal(not_null<Window::SectionMemento * > memento,const Window::SectionShow & params)927 bool ScheduledWidget::showInternal(
928 		not_null<Window::SectionMemento*> memento,
929 		const Window::SectionShow &params) {
930 	if (auto logMemento = dynamic_cast<ScheduledMemento*>(memento.get())) {
931 		if (logMemento->getHistory() == history()) {
932 			restoreState(logMemento);
933 			return true;
934 		}
935 	}
936 	return false;
937 }
938 
setInternalState(const QRect & geometry,not_null<ScheduledMemento * > memento)939 void ScheduledWidget::setInternalState(
940 		const QRect &geometry,
941 		not_null<ScheduledMemento*> memento) {
942 	setGeometry(geometry);
943 	Ui::SendPendingMoveResizeEvents(this);
944 	restoreState(memento);
945 }
946 
pushTabbedSelectorToThirdSection(not_null<PeerData * > peer,const Window::SectionShow & params)947 bool ScheduledWidget::pushTabbedSelectorToThirdSection(
948 		not_null<PeerData*> peer,
949 		const Window::SectionShow &params) {
950 	return _composeControls->pushTabbedSelectorToThirdSection(peer, params);
951 }
952 
returnTabbedSelector()953 bool ScheduledWidget::returnTabbedSelector() {
954 	return _composeControls->returnTabbedSelector();
955 }
956 
createMemento()957 std::shared_ptr<Window::SectionMemento> ScheduledWidget::createMemento() {
958 	auto result = std::make_shared<ScheduledMemento>(history());
959 	saveState(result.get());
960 	return result;
961 }
962 
saveState(not_null<ScheduledMemento * > memento)963 void ScheduledWidget::saveState(not_null<ScheduledMemento*> memento) {
964 	_inner->saveState(memento->list());
965 }
966 
restoreState(not_null<ScheduledMemento * > memento)967 void ScheduledWidget::restoreState(not_null<ScheduledMemento*> memento) {
968 	_inner->restoreState(memento->list());
969 }
970 
resizeEvent(QResizeEvent * e)971 void ScheduledWidget::resizeEvent(QResizeEvent *e) {
972 	if (!width() || !height()) {
973 		return;
974 	}
975 	_composeControls->resizeToWidth(width());
976 	updateControlsGeometry();
977 }
978 
updateControlsGeometry()979 void ScheduledWidget::updateControlsGeometry() {
980 	const auto contentWidth = width();
981 
982 	const auto newScrollTop = _scroll->isHidden()
983 		? std::nullopt
984 		: base::make_optional(_scroll->scrollTop() + topDelta());
985 	_topBar->resizeToWidth(contentWidth);
986 	_topBarShadow->resize(contentWidth, st::lineWidth);
987 
988 	const auto bottom = height();
989 	const auto controlsHeight = _composeControls->heightCurrent();
990 	const auto scrollHeight = bottom - _topBar->height() - controlsHeight;
991 	const auto scrollSize = QSize(contentWidth, scrollHeight);
992 	if (_scroll->size() != scrollSize) {
993 		_skipScrollEvent = true;
994 		_scroll->resize(scrollSize);
995 		_inner->resizeToWidth(scrollSize.width(), _scroll->height());
996 		_skipScrollEvent = false;
997 	}
998 	if (!_scroll->isHidden()) {
999 		if (newScrollTop) {
1000 			_scroll->scrollToY(*newScrollTop);
1001 		}
1002 		updateInnerVisibleArea();
1003 	}
1004 	_composeControls->move(0, bottom - controlsHeight);
1005 	_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
1006 
1007 	updateScrollDownPosition();
1008 }
1009 
paintEvent(QPaintEvent * e)1010 void ScheduledWidget::paintEvent(QPaintEvent *e) {
1011 	if (animating()) {
1012 		SectionWidget::paintEvent(e);
1013 		return;
1014 	}
1015 	if (Ui::skipPaintEvent(this, e)) {
1016 		return;
1017 	}
1018 	//if (hasPendingResizedItems()) {
1019 	//	updateListSize();
1020 	//}
1021 
1022 	//auto ms = crl::now();
1023 	//_historyDownShown.step(ms);
1024 
1025 	const auto clip = e->rect();
1026 	SectionWidget::PaintBackground(controller(), _theme.get(), this, clip);
1027 }
1028 
onScroll()1029 void ScheduledWidget::onScroll() {
1030 	if (_skipScrollEvent) {
1031 		return;
1032 	}
1033 	updateInnerVisibleArea();
1034 }
1035 
updateInnerVisibleArea()1036 void ScheduledWidget::updateInnerVisibleArea() {
1037 	const auto scrollTop = _scroll->scrollTop();
1038 	_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
1039 	updateScrollDownVisibility();
1040 }
1041 
showAnimatedHook(const Window::SectionSlideParams & params)1042 void ScheduledWidget::showAnimatedHook(
1043 		const Window::SectionSlideParams &params) {
1044 	_topBar->setAnimatingMode(true);
1045 	if (params.withTopBarShadow) {
1046 		_topBarShadow->show();
1047 	}
1048 	_composeControls->showStarted();
1049 }
1050 
showFinishedHook()1051 void ScheduledWidget::showFinishedHook() {
1052 	_topBar->setAnimatingMode(false);
1053 	_composeControls->showFinished();
1054 
1055 	// We should setup the drag area only after
1056 	// the section animation is finished,
1057 	// because after that the method showChildren() is called.
1058 	setupDragArea();
1059 }
1060 
floatPlayerHandleWheelEvent(QEvent * e)1061 bool ScheduledWidget::floatPlayerHandleWheelEvent(QEvent *e) {
1062 	return _scroll->viewportEvent(e);
1063 }
1064 
floatPlayerAvailableRect()1065 QRect ScheduledWidget::floatPlayerAvailableRect() {
1066 	return mapToGlobal(_scroll->geometry());
1067 }
1068 
listContext()1069 Context ScheduledWidget::listContext() {
1070 	return Context::History;
1071 }
1072 
listScrollTo(int top)1073 void ScheduledWidget::listScrollTo(int top) {
1074 	if (_scroll->scrollTop() != top) {
1075 		_scroll->scrollToY(top);
1076 	} else {
1077 		updateInnerVisibleArea();
1078 	}
1079 }
1080 
listCancelRequest()1081 void ScheduledWidget::listCancelRequest() {
1082 	if (_inner && !_inner->getSelectedItems().empty()) {
1083 		clearSelected();
1084 		return;
1085 	} else if (_composeControls->handleCancelRequest()) {
1086 		return;
1087 	}
1088 	controller()->showBackFromStack();
1089 }
1090 
listDeleteRequest()1091 void ScheduledWidget::listDeleteRequest() {
1092 	confirmDeleteSelected();
1093 }
1094 
listSource(Data::MessagePosition aroundId,int limitBefore,int limitAfter)1095 rpl::producer<Data::MessagesSlice> ScheduledWidget::listSource(
1096 		Data::MessagePosition aroundId,
1097 		int limitBefore,
1098 		int limitAfter) {
1099 	const auto data = &controller()->session().data();
1100 	return rpl::single(
1101 		rpl::empty_value()
1102 	) | rpl::then(
1103 		data->scheduledMessages().updates(_history)
1104 	) | rpl::map([=] {
1105 		return data->scheduledMessages().list(_history);
1106 	}) | rpl::after_next([=](const Data::MessagesSlice &slice) {
1107 		highlightSingleNewMessage(slice);
1108 	});
1109 }
1110 
highlightSingleNewMessage(const Data::MessagesSlice & slice)1111 void ScheduledWidget::highlightSingleNewMessage(
1112 		const Data::MessagesSlice &slice) {
1113 	const auto guard = gsl::finally([&] { _lastSlice = slice; });
1114 	if (_lastSlice.ids.empty()
1115 		|| (slice.ids.size() != _lastSlice.ids.size() + 1)) {
1116 		return;
1117 	}
1118 	auto firstDifferent = 0;
1119 	while (firstDifferent != _lastSlice.ids.size()) {
1120 		if (slice.ids[firstDifferent] != _lastSlice.ids[firstDifferent]) {
1121 			break;
1122 		}
1123 		++firstDifferent;
1124 	}
1125 	auto lastDifferent = slice.ids.size() - 1;
1126 	while (lastDifferent != firstDifferent) {
1127 		if (slice.ids[lastDifferent] != _lastSlice.ids[lastDifferent - 1]) {
1128 			break;
1129 		}
1130 		--lastDifferent;
1131 	}
1132 	if (firstDifferent != lastDifferent) {
1133 		return;
1134 	}
1135 	const auto newId = slice.ids[firstDifferent];
1136 	if (const auto item = session().data().message(newId)) {
1137 	//	_highlightMessageId = newId;
1138 		showAtPosition(item->position());
1139 	}
1140 }
1141 
listAllowsMultiSelect()1142 bool ScheduledWidget::listAllowsMultiSelect() {
1143 	return true;
1144 }
1145 
listIsItemGoodForSelection(not_null<HistoryItem * > item)1146 bool ScheduledWidget::listIsItemGoodForSelection(
1147 		not_null<HistoryItem*> item) {
1148 	return !item->isSending() && !item->hasFailed();
1149 }
1150 
listIsLessInOrder(not_null<HistoryItem * > first,not_null<HistoryItem * > second)1151 bool ScheduledWidget::listIsLessInOrder(
1152 		not_null<HistoryItem*> first,
1153 		not_null<HistoryItem*> second) {
1154 	return first->position() < second->position();
1155 }
1156 
listSelectionChanged(SelectedItems && items)1157 void ScheduledWidget::listSelectionChanged(SelectedItems &&items) {
1158 	HistoryView::TopBarWidget::SelectedState state;
1159 	state.count = items.size();
1160 	for (const auto &item : items) {
1161 		if (item.canDelete) {
1162 			++state.canDeleteCount;
1163 		}
1164 		if (item.canSendNow) {
1165 			++state.canSendNowCount;
1166 		}
1167 	}
1168 	_topBar->showSelected(state);
1169 }
1170 
listVisibleItemsChanged(HistoryItemsList && items)1171 void ScheduledWidget::listVisibleItemsChanged(HistoryItemsList &&items) {
1172 }
1173 
listMessagesBar(const std::vector<not_null<Element * >> & elements)1174 MessagesBarData ScheduledWidget::listMessagesBar(
1175 		const std::vector<not_null<Element*>> &elements) {
1176 	return MessagesBarData();
1177 }
1178 
listContentRefreshed()1179 void ScheduledWidget::listContentRefreshed() {
1180 }
1181 
listDateLink(not_null<Element * > view)1182 ClickHandlerPtr ScheduledWidget::listDateLink(not_null<Element*> view) {
1183 	return nullptr;
1184 }
1185 
listElementHideReply(not_null<const Element * > view)1186 bool ScheduledWidget::listElementHideReply(not_null<const Element*> view) {
1187 	return false;
1188 }
1189 
listElementShownUnread(not_null<const Element * > view)1190 bool ScheduledWidget::listElementShownUnread(not_null<const Element*> view) {
1191 	return true;
1192 }
1193 
listIsGoodForAroundPosition(not_null<const Element * > view)1194 bool ScheduledWidget::listIsGoodForAroundPosition(
1195 		not_null<const Element*> view) {
1196 	return true;
1197 }
1198 
sendBotCommand(Bot::SendCommandRequest request)1199 Window::SectionActionResult ScheduledWidget::sendBotCommand(
1200 		Bot::SendCommandRequest request) {
1201 	if (request.peer != _history->peer) {
1202 		return Window::SectionActionResult::Ignore;
1203 	}
1204 	listSendBotCommand(request.command, request.context);
1205 	return Window::SectionActionResult::Handle;
1206 }
1207 
listSendBotCommand(const QString & command,const FullMsgId & context)1208 void ScheduledWidget::listSendBotCommand(
1209 		const QString &command,
1210 		const FullMsgId &context) {
1211 	const auto callback = [=](Api::SendOptions options) {
1212 		const auto text = Bot::WrapCommandInChat(
1213 			_history->peer,
1214 			command,
1215 			context);
1216 		auto message = ApiWrap::MessageToSend(_history);
1217 		message.textWithTags = { text };
1218 		message.action.options = options;
1219 		session().api().sendMessage(std::move(message));
1220 	};
1221 	controller()->show(
1222 		PrepareScheduleBox(this, sendMenuType(), callback),
1223 		Ui::LayerOption::KeepOther);
1224 }
1225 
listHandleViaClick(not_null<UserData * > bot)1226 void ScheduledWidget::listHandleViaClick(not_null<UserData*> bot) {
1227 	_composeControls->setText({ '@' + bot->username + ' ' });
1228 }
1229 
listChatTheme()1230 not_null<Ui::ChatTheme*> ScheduledWidget::listChatTheme() {
1231 	return _theme.get();
1232 }
1233 
confirmSendNowSelected()1234 void ScheduledWidget::confirmSendNowSelected() {
1235 	ConfirmSendNowSelectedItems(_inner);
1236 }
1237 
confirmDeleteSelected()1238 void ScheduledWidget::confirmDeleteSelected() {
1239 	ConfirmDeleteSelectedItems(_inner);
1240 }
1241 
clearSelected()1242 void ScheduledWidget::clearSelected() {
1243 	_inner->cancelSelection();
1244 }
1245 
setupDragArea()1246 void ScheduledWidget::setupDragArea() {
1247 	const auto areas = DragArea::SetupDragAreaToContainer(
1248 		this,
1249 		[=](auto d) { return _history && !_composeControls->isRecording(); },
1250 		nullptr,
1251 		[=] { updateControlsGeometry(); });
1252 
1253 	const auto droppedCallback = [=](bool overrideSendImagesAsPhotos) {
1254 		return [=](const QMimeData *data) {
1255 			confirmSendingFiles(data, overrideSendImagesAsPhotos);
1256 			Window::ActivateWindow(controller());
1257 		};
1258 	};
1259 	areas.document->setDroppedCallback(droppedCallback(false));
1260 	areas.photo->setDroppedCallback(droppedCallback(true));
1261 }
1262 
1263 } // namespace HistoryView
1264