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 ¶ms) {
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 ¶ms) {
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 ¶ms) {
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 ¶ms) {
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