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_replies_section.h"
9
10 #include "history/view/controls/history_view_compose_controls.h"
11 #include "history/view/history_view_top_bar_widget.h"
12 #include "history/view/history_view_list_widget.h"
13 #include "history/view/history_view_schedule_box.h"
14 #include "history/view/history_view_pinned_bar.h"
15 #include "history/history.h"
16 #include "history/history_drag_area.h"
17 #include "history/history_item_components.h"
18 #include "history/history_item.h"
19 #include "chat_helpers/send_context_menu.h" // SendMenu::Type.
20 #include "ui/chat/pinned_bar.h"
21 #include "ui/chat/chat_style.h"
22 #include "ui/widgets/scroll_area.h"
23 #include "ui/widgets/shadow.h"
24 #include "ui/wrap/slide_wrap.h"
25 #include "ui/layers/generic_box.h"
26 #include "ui/item_text_options.h"
27 #include "ui/toast/toast.h"
28 #include "ui/text/format_values.h"
29 #include "ui/text/text_utilities.h"
30 #include "ui/chat/attach/attach_prepare.h"
31 #include "ui/chat/attach/attach_send_files_way.h"
32 #include "ui/special_buttons.h"
33 #include "ui/ui_utility.h"
34 #include "ui/toasts/common_toasts.h"
35 #include "base/timer_rpl.h"
36 #include "api/api_common.h"
37 #include "api/api_editing.h"
38 #include "api/api_sending.h"
39 #include "apiwrap.h"
40 #include "ui/boxes/confirm_box.h"
41 #include "boxes/delete_messages_box.h"
42 #include "boxes/edit_caption_box.h"
43 #include "boxes/send_files_box.h"
44 #include "window/window_adaptive.h"
45 #include "window/window_session_controller.h"
46 #include "window/window_peer_menu.h"
47 #include "base/event_filter.h"
48 #include "base/call_delayed.h"
49 #include "core/file_utilities.h"
50 #include "main/main_session.h"
51 #include "data/data_session.h"
52 #include "data/data_user.h"
53 #include "data/data_chat.h"
54 #include "data/data_channel.h"
55 #include "data/data_replies_list.h"
56 #include "data/data_changes.h"
57 #include "data/data_send_action.h"
58 #include "storage/storage_media_prepare.h"
59 #include "storage/storage_account.h"
60 #include "inline_bots/inline_bot_result.h"
61 #include "lang/lang_keys.h"
62 #include "facades.h"
63 #include "styles/style_chat.h"
64 #include "styles/style_window.h"
65 #include "styles/style_info.h"
66 #include "styles/style_boxes.h"
67
68 #include <QtCore/QMimeData>
69 #include <QtGui/QGuiApplication>
70
71 namespace HistoryView {
72 namespace {
73
74 constexpr auto kReadRequestTimeout = 3 * crl::time(1000);
75 constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);
76
CanSendFiles(not_null<const QMimeData * > data)77 bool CanSendFiles(not_null<const QMimeData*> data) {
78 if (data->hasImage()) {
79 return true;
80 } else if (const auto urls = data->urls(); !urls.empty()) {
81 if (ranges::all_of(urls, &QUrl::isLocalFile)) {
82 return true;
83 }
84 }
85 return false;
86 }
87
RootViewContent(not_null<History * > history,MsgId rootId)88 rpl::producer<Ui::MessageBarContent> RootViewContent(
89 not_null<History*> history,
90 MsgId rootId) {
91 return MessageBarContentByItemId(
92 &history->session(),
93 FullMsgId{ history->channelId(), rootId }
94 ) | rpl::map([=](Ui::MessageBarContent &&content) {
95 const auto item = history->owner().message(
96 history->channelId(),
97 rootId);
98 if (!item) {
99 content.text = Ui::Text::Link(tr::lng_deleted_message(tr::now));
100 }
101 const auto sender = (item && item->discussionPostOriginalSender())
102 ? item->discussionPostOriginalSender()
103 : history->peer.get();
104 content.title = sender->name.isEmpty() ? "Message" : sender->name;
105 return std::move(content);
106 });
107 }
108
109 } // namespace
110
RepliesMemento(not_null<HistoryItem * > commentsItem,MsgId commentId)111 RepliesMemento::RepliesMemento(
112 not_null<HistoryItem*> commentsItem,
113 MsgId commentId)
114 : RepliesMemento(commentsItem->history(), commentsItem->id, commentId) {
115 if (commentId) {
116 _list.setAroundPosition({
117 .fullId = FullMsgId(
118 commentsItem->history()->channelId(),
119 commentId),
120 .date = TimeId(0),
121 });
122 } else if (commentsItem->computeRepliesInboxReadTillFull() == MsgId(1)) {
123 _list.setAroundPosition(Data::MinMessagePosition);
124 _list.setScrollTopState(ListMemento::ScrollTopState{
125 Data::MinMessagePosition
126 });
127 }
128 }
129
createWidget(QWidget * parent,not_null<Window::SessionController * > controller,Window::Column column,const QRect & geometry)130 object_ptr<Window::SectionWidget> RepliesMemento::createWidget(
131 QWidget *parent,
132 not_null<Window::SessionController*> controller,
133 Window::Column column,
134 const QRect &geometry) {
135 if (column == Window::Column::Third) {
136 return nullptr;
137 }
138 auto result = object_ptr<RepliesWidget>(
139 parent,
140 controller,
141 _history,
142 _rootId);
143 result->setInternalState(geometry, this);
144 return result;
145 }
146
RepliesWidget(QWidget * parent,not_null<Window::SessionController * > controller,not_null<History * > history,MsgId rootId)147 RepliesWidget::RepliesWidget(
148 QWidget *parent,
149 not_null<Window::SessionController*> controller,
150 not_null<History*> history,
151 MsgId rootId)
152 : Window::SectionWidget(parent, controller, history->peer)
153 , _history(history)
154 , _rootId(rootId)
155 , _root(lookupRoot())
156 , _areComments(computeAreComments())
157 , _sendAction(history->owner().sendActionManager().repliesPainter(
158 history,
159 rootId))
160 , _topBar(this, controller)
161 , _topBarShadow(this)
162 , _composeControls(std::make_unique<ComposeControls>(
163 this,
164 controller,
165 ComposeControls::Mode::Normal,
166 SendMenu::Type::SilentOnly))
167 , _scroll(std::make_unique<Ui::ScrollArea>(
168 this,
169 controller->chatStyle()->value(lifetime(), st::historyScroll),
170 false))
171 , _scrollDown(
172 _scroll.get(),
173 controller->chatStyle()->value(lifetime(), st::historyToDown))
174 , _readRequestTimer([=] { sendReadTillRequest(); }) {
175 controller->chatStyle()->paletteChanged(
__anon0edbbaec0402null176 ) | rpl::start_with_next([=] {
177 _scroll->updateBars();
178 }, _scroll->lifetime());
179
180 Window::ChatThemeValueFromPeer(
181 controller,
182 history->peer
__anon0edbbaec0502(std::shared_ptr<Ui::ChatTheme> &&theme) 183 ) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
184 _theme = std::move(theme);
185 controller->setChatStyleTheme(_theme);
186 }, lifetime());
187
188 setupRoot();
189 setupRootView();
190
191 session().api().requestFullPeer(_history->peer);
192
193 refreshTopBarActiveChat();
194
195 _topBar->move(0, 0);
196 _topBar->resizeToWidth(width());
197 _topBar->show();
198
199 _rootView->move(0, _topBar->height());
200
201 _topBar->deleteSelectionRequest(
__anon0edbbaec0602null202 ) | rpl::start_with_next([=] {
203 confirmDeleteSelected();
204 }, _topBar->lifetime());
205 _topBar->forwardSelectionRequest(
__anon0edbbaec0702null206 ) | rpl::start_with_next([=] {
207 confirmForwardSelected();
208 }, _topBar->lifetime());
209 _topBar->clearSelectionRequest(
__anon0edbbaec0802null210 ) | rpl::start_with_next([=] {
211 clearSelected();
212 }, _topBar->lifetime());
213
214 _rootView->raise();
215 _topBarShadow->raise();
216
217 controller->adaptive().value(
__anon0edbbaec0902null218 ) | rpl::start_with_next([=] {
219 updateAdaptiveLayout();
220 }, lifetime());
221
222 _inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(
223 this,
224 controller,
225 static_cast<ListDelegate*>(this)));
226 _scroll->move(0, _topBar->height());
227 _scroll->show();
228 _scroll->scrolls(
__anon0edbbaec0a02null229 ) | rpl::start_with_next([=] {
230 onScroll();
231 }, lifetime());
232
233 _inner->editMessageRequested(
__anon0edbbaec0b02(auto fullId) 234 ) | rpl::start_with_next([=](auto fullId) {
235 if (const auto item = session().data().message(fullId)) {
236 const auto media = item->media();
237 if (media && !media->webpage()) {
238 if (media->allowsEditCaption()) {
239 controller->show(Box<EditCaptionBox>(controller, item));
240 }
241 } else {
242 _composeControls->editMessage(fullId);
243 }
244 }
245 }, _inner->lifetime());
246
247 _inner->replyToMessageRequested(
__anon0edbbaec0c02(auto fullId) 248 ) | rpl::start_with_next([=](auto fullId) {
249 replyToMessage(fullId);
250 }, _inner->lifetime());
251
252 _inner->showMessageRequested(
__anon0edbbaec0d02(auto fullId) 253 ) | rpl::start_with_next([=](auto fullId) {
254 if (const auto item = session().data().message(fullId)) {
255 showAtPosition(item->position());
256 }
257 }, _inner->lifetime());
258
259 _composeControls->sendActionUpdates(
__anon0edbbaec0e02(ComposeControls::SendActionUpdate &&data) 260 ) | rpl::start_with_next([=](ComposeControls::SendActionUpdate &&data) {
261 if (!data.cancel) {
262 session().sendProgressManager().update(
263 _history,
264 _rootId,
265 data.type,
266 data.progress);
267 } else {
268 session().sendProgressManager().cancel(
269 _history,
270 _rootId,
271 data.type);
272 }
273 }, lifetime());
274
275 using MessageUpdateFlag = Data::MessageUpdate::Flag;
276 _history->session().changes().messageUpdates(
277 MessageUpdateFlag::Destroyed
278 | MessageUpdateFlag::RepliesUnreadCount
__anon0edbbaec0f02(const Data::MessageUpdate &update) 279 ) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
280 if (update.flags & MessageUpdateFlag::Destroyed) {
281 if (update.item == _root) {
282 _root = nullptr;
283 updatePinnedVisibility();
284 controller->showBackFromStack();
285 }
286 while (update.item == _replyReturn) {
287 calculateNextReplyReturn();
288 }
289 return;
290 } else if ((update.item == _root)
291 && (update.flags & MessageUpdateFlag::RepliesUnreadCount)) {
292 refreshUnreadCountBadge();
293 }
294 }, lifetime());
295
296 _history->session().changes().historyUpdates(
297 _history,
298 Data::HistoryUpdate::Flag::OutboxRead
__anon0edbbaec1002null299 ) | rpl::start_with_next([=] {
300 _inner->update();
301 }, lifetime());
302
303 _history->session().data().unreadRepliesCountRequests(
304 ) | rpl::filter([=](
__anon0edbbaec1102( const Data::Session::UnreadRepliesCountRequest &request) 305 const Data::Session::UnreadRepliesCountRequest &request) {
306 return (request.root.get() == _root);
307 }) | rpl::start_with_next([=](
__anon0edbbaec1202( const Data::Session::UnreadRepliesCountRequest &request) 308 const Data::Session::UnreadRepliesCountRequest &request) {
309 if (const auto result = computeUnreadCountLocally(request.afterId)) {
310 *request.result = result;
311 }
312 }, lifetime());
313
314 setupScrollDownButton();
315 setupComposeControls();
316 orderWidgets();
317 }
318
~RepliesWidget()319 RepliesWidget::~RepliesWidget() {
320 if (_readRequestTimer.isActive()) {
321 sendReadTillRequest();
322 }
323 base::take(_sendAction);
324 _history->owner().sendActionManager().repliesPainterRemoved(
325 _history,
326 _rootId);
327 }
328
orderWidgets()329 void RepliesWidget::orderWidgets() {
330 if (_topBar) {
331 _topBar->raise();
332 }
333 if (_rootView) {
334 _rootView->raise();
335 }
336 _topBarShadow->raise();
337 _composeControls->raisePanels();
338 }
339
sendReadTillRequest()340 void RepliesWidget::sendReadTillRequest() {
341 if (!_root) {
342 _readRequestPending = true;
343 return;
344 }
345 if (_readRequestTimer.isActive()) {
346 _readRequestTimer.cancel();
347 }
348 _readRequestPending = false;
349 const auto api = &_history->session().api();
350 api->request(base::take(_readRequestId)).cancel();
351
352 _readRequestId = api->request(MTPmessages_ReadDiscussion(
353 _root->history()->peer->input,
354 MTP_int(_root->id),
355 MTP_int(_root->computeRepliesInboxReadTillFull())
356 )).done(crl::guard(this, [=](const MTPBool &) {
357 _readRequestId = 0;
358 reloadUnreadCountIfNeeded();
359 })).send();
360 }
361
setupRoot()362 void RepliesWidget::setupRoot() {
363 if (!_root) {
364 const auto channel = _history->peer->asChannel();
365 const auto done = crl::guard(this, [=](ChannelData*, MsgId) {
366 _root = lookupRoot();
367 if (_root) {
368 _areComments = computeAreComments();
369 refreshUnreadCountBadge();
370 if (_readRequestPending) {
371 sendReadTillRequest();
372 }
373 _inner->update();
374 }
375 updatePinnedVisibility();
376 });
377 _history->session().api().requestMessageData(channel, _rootId, done);
378 }
379 }
380
setupRootView()381 void RepliesWidget::setupRootView() {
382 auto content = rpl::combine(
383 RootViewContent(_history, _rootId),
384 _rootVisible.value()
385 ) | rpl::map([=](Ui::MessageBarContent &&content, bool shown) {
386 return shown ? std::move(content) : Ui::MessageBarContent();
387 });
388 _rootView = std::make_unique<Ui::PinnedBar>(this, std::move(content));
389
390 controller()->adaptive().oneColumnValue(
391 ) | rpl::start_with_next([=](bool one) {
392 _rootView->setShadowGeometryPostprocess([=](QRect geometry) {
393 if (!one) {
394 geometry.setLeft(geometry.left() + st::lineWidth);
395 }
396 return geometry;
397 });
398 }, _rootView->lifetime());
399
400 _rootView->barClicks(
401 ) | rpl::start_with_next([=] {
402 showAtStart();
403 }, lifetime());
404
405 _rootViewHeight = 0;
406 _rootView->heightValue(
407 ) | rpl::start_with_next([=](int height) {
408 if (const auto delta = height - _rootViewHeight) {
409 _rootViewHeight = height;
410 setGeometryWithTopMoved(geometry(), delta);
411 }
412 }, _rootView->lifetime());
413 }
414
lookupRoot() const415 HistoryItem *RepliesWidget::lookupRoot() const {
416 return _history->owner().message(_history->channelId(), _rootId);
417 }
418
computeAreComments() const419 bool RepliesWidget::computeAreComments() const {
420 return _root && _root->isDiscussionPost();
421 }
422
computeUnreadCount() const423 std::optional<int> RepliesWidget::computeUnreadCount() const {
424 if (!_root) {
425 return std::nullopt;
426 }
427 const auto views = _root->Get<HistoryMessageViews>();
428 if (!views) {
429 return std::nullopt;
430 }
431 return (views->repliesUnreadCount >= 0)
432 ? std::make_optional(views->repliesUnreadCount)
433 : std::nullopt;
434 }
435
setupComposeControls()436 void RepliesWidget::setupComposeControls() {
437 auto slowmodeSecondsLeft = session().changes().peerFlagsValue(
438 _history->peer,
439 Data::PeerUpdate::Flag::Slowmode
440 ) | rpl::map([=] {
441 return _history->peer->slowmodeSecondsLeft();
442 }) | rpl::map([=](int delay) -> rpl::producer<int> {
443 auto start = rpl::single(delay);
444 if (!delay) {
445 return start;
446 }
447 return std::move(
448 start
449 ) | rpl::then(base::timer_each(
450 kRefreshSlowmodeLabelTimeout
451 ) | rpl::map([=] {
452 return _history->peer->slowmodeSecondsLeft();
453 }) | rpl::take_while([=](int delay) {
454 return delay > 0;
455 })) | rpl::then(rpl::single(0));
456 }) | rpl::flatten_latest();
457
458 const auto channel = _history->peer->asChannel();
459 Assert(channel != nullptr);
460
461 auto hasSendingMessage = session().changes().historyFlagsValue(
462 _history,
463 Data::HistoryUpdate::Flag::ClientSideMessages
464 ) | rpl::map([=] {
465 return _history->latestSendingMessage() != nullptr;
466 }) | rpl::distinct_until_changed();
467
468 using namespace rpl::mappers;
469 auto sendDisabledBySlowmode = (!channel || channel->amCreator())
470 ? (rpl::single(false) | rpl::type_erased())
471 : rpl::combine(
472 channel->slowmodeAppliedValue(),
473 std::move(hasSendingMessage),
474 _1 && _2);
475
476 auto writeRestriction = session().changes().peerFlagsValue(
477 _history->peer,
478 Data::PeerUpdate::Flag::Rights
479 ) | rpl::map([=] {
480 return Data::RestrictionError(
481 _history->peer,
482 ChatRestriction::SendMessages);
483 });
484
485 _composeControls->setHistory({
486 .history = _history.get(),
487 .showSlowmodeError = [=] { return showSlowmodeError(); },
488 .slowmodeSecondsLeft = std::move(slowmodeSecondsLeft),
489 .sendDisabledBySlowmode = std::move(sendDisabledBySlowmode),
490 .writeRestriction = std::move(writeRestriction),
491 });
492
493 _composeControls->height(
494 ) | rpl::start_with_next([=] {
495 const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());
496 updateControlsGeometry();
497 if (wasMax) {
498 listScrollTo(_scroll->scrollTopMax());
499 }
500 }, lifetime());
501
502 _composeControls->cancelRequests(
503 ) | rpl::start_with_next([=] {
504 listCancelRequest();
505 }, lifetime());
506
507 _composeControls->sendRequests(
508 ) | rpl::start_with_next([=](Api::SendOptions options) {
509 send(options);
510 }, lifetime());
511
512 _composeControls->sendVoiceRequests(
513 ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
514 sendVoice(std::move(data));
515 }, lifetime());
516
517 _composeControls->sendCommandRequests(
518 ) | rpl::start_with_next([=](const QString &command) {
519 if (showSlowmodeError()) {
520 return;
521 }
522 listSendBotCommand(command, FullMsgId());
523 }, lifetime());
524
525 const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
526 _composeControls->editRequests(
527 ) | rpl::start_with_next([=](auto data) {
528 if (const auto item = session().data().message(data.fullId)) {
529 edit(item, data.options, saveEditMsgRequestId);
530 }
531 }, lifetime());
532
533 _composeControls->attachRequests(
534 ) | rpl::filter([=] {
535 return !_choosingAttach;
536 }) | rpl::start_with_next([=] {
537 _choosingAttach = true;
538 base::call_delayed(
539 st::historyAttach.ripple.hideDuration,
540 this,
541 [=] { _choosingAttach = false; chooseAttach(); });
542 }, lifetime());
543
544 using Selector = ChatHelpers::TabbedSelector;
545
546 _composeControls->fileChosen(
547 ) | rpl::start_with_next([=](Selector::FileChosen chosen) {
548 sendExistingDocument(chosen.document, chosen.options);
549 }, lifetime());
550
551 _composeControls->photoChosen(
552 ) | rpl::start_with_next([=](Selector::PhotoChosen chosen) {
553 sendExistingPhoto(chosen.photo, chosen.options);
554 }, lifetime());
555
556 _composeControls->inlineResultChosen(
557 ) | rpl::start_with_next([=](Selector::InlineChosen chosen) {
558 sendInlineResult(chosen.result, chosen.bot, chosen.options);
559 }, lifetime());
560
561 _composeControls->scrollRequests(
562 ) | rpl::start_with_next([=](Data::MessagePosition pos) {
563 showAtPosition(pos);
564 }, lifetime());
565
566 _composeControls->scrollKeyEvents(
567 ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
568 _scroll->keyPressEvent(e);
569 }, lifetime());
570
571 _composeControls->editLastMessageRequests(
572 ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
573 if (!_inner->lastMessageEditRequestNotify()) {
574 _scroll->keyPressEvent(e);
575 }
576 }, lifetime());
577
578 _composeControls->replyNextRequests(
579 ) | rpl::start_with_next([=](ComposeControls::ReplyNextRequest &&data) {
580 using Direction = ComposeControls::ReplyNextRequest::Direction;
581 _inner->replyNextMessage(
582 data.replyId,
583 data.direction == Direction::Next);
584 }, lifetime());
585
586 _composeControls->setMimeDataHook([=](
587 not_null<const QMimeData*> data,
588 Ui::InputField::MimeAction action) {
589 if (action == Ui::InputField::MimeAction::Check) {
590 return CanSendFiles(data);
591 } else if (action == Ui::InputField::MimeAction::Insert) {
592 return confirmSendingFiles(data, std::nullopt, data->text());
593 }
594 Unexpected("action in MimeData hook.");
595 });
596
597 _composeControls->lockShowStarts(
598 ) | rpl::start_with_next([=] {
599 updateScrollDownVisibility();
600 }, lifetime());
601
602 _composeControls->viewportEvents(
603 ) | rpl::start_with_next([=](not_null<QEvent*> e) {
604 _scroll->viewportEvent(e);
605 }, lifetime());
606
607 _composeControls->finishAnimating();
608 }
609
chooseAttach()610 void RepliesWidget::chooseAttach() {
611 if (const auto error = Data::RestrictionError(
612 _history->peer,
613 ChatRestriction::SendMedia)) {
614 Ui::ShowMultilineToast({
615 .text = { *error },
616 });
617 return;
618 } else if (showSlowmodeError()) {
619 return;
620 }
621
622 const auto filter = FileDialog::AllOrImagesFilter();
623 FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](
624 FileDialog::OpenResult &&result) {
625 if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
626 return;
627 }
628
629 if (!result.remoteContent.isEmpty()) {
630 auto read = Images::Read({
631 .content = result.remoteContent,
632 });
633 if (!read.image.isNull() && !read.animated) {
634 confirmSendingFiles(
635 std::move(read.image),
636 std::move(result.remoteContent));
637 } else {
638 uploadFile(result.remoteContent, SendMediaType::File);
639 }
640 } else {
641 auto list = Storage::PrepareMediaList(
642 result.paths,
643 st::sendMediaPreviewSize);
644 confirmSendingFiles(std::move(list));
645 }
646 }), nullptr);
647 }
648
confirmSendingFiles(not_null<const QMimeData * > data,std::optional<bool> overrideSendImagesAsPhotos,const QString & insertTextOnCancel)649 bool RepliesWidget::confirmSendingFiles(
650 not_null<const QMimeData*> data,
651 std::optional<bool> overrideSendImagesAsPhotos,
652 const QString &insertTextOnCancel) {
653 const auto hasImage = data->hasImage();
654
655 if (const auto urls = data->urls(); !urls.empty()) {
656 auto list = Storage::PrepareMediaList(
657 urls,
658 st::sendMediaPreviewSize);
659 if (list.error != Ui::PreparedList::Error::NonLocalUrl) {
660 if (list.error == Ui::PreparedList::Error::None
661 || !hasImage) {
662 const auto emptyTextOnCancel = QString();
663 list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
664 confirmSendingFiles(std::move(list), emptyTextOnCancel);
665 return true;
666 }
667 }
668 }
669
670 if (hasImage) {
671 auto image = qvariant_cast<QImage>(data->imageData());
672 if (!image.isNull()) {
673 confirmSendingFiles(
674 std::move(image),
675 QByteArray(),
676 overrideSendImagesAsPhotos,
677 insertTextOnCancel);
678 return true;
679 }
680 }
681 return false;
682 }
683
confirmSendingFiles(Ui::PreparedList && list,const QString & insertTextOnCancel)684 bool RepliesWidget::confirmSendingFiles(
685 Ui::PreparedList &&list,
686 const QString &insertTextOnCancel) {
687 if (showSendingFilesError(list)) {
688 return false;
689 }
690
691 using SendLimit = SendFilesBox::SendLimit;
692 auto box = Box<SendFilesBox>(
693 controller(),
694 std::move(list),
695 _composeControls->getTextWithAppliedMarkdown(),
696 _history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many,
697 Api::SendType::Normal,
698 SendMenu::Type::SilentOnly); // #TODO replies schedule
699
700 box->setConfirmedCallback(crl::guard(this, [=](
701 Ui::PreparedList &&list,
702 Ui::SendFilesWay way,
703 TextWithTags &&caption,
704 Api::SendOptions options,
705 bool ctrlShiftEnter) {
706 sendingFilesConfirmed(
707 std::move(list),
708 way,
709 std::move(caption),
710 options,
711 ctrlShiftEnter);
712 }));
713 box->setCancelledCallback(_composeControls->restoreTextCallback(
714 insertTextOnCancel));
715
716 //ActivateWindow(controller());
717 const auto shown = controller()->show(std::move(box));
718 shown->setCloseByOutsideClick(false);
719
720 return true;
721 }
722
sendingFilesConfirmed(Ui::PreparedList && list,Ui::SendFilesWay way,TextWithTags && caption,Api::SendOptions options,bool ctrlShiftEnter)723 void RepliesWidget::sendingFilesConfirmed(
724 Ui::PreparedList &&list,
725 Ui::SendFilesWay way,
726 TextWithTags &&caption,
727 Api::SendOptions options,
728 bool ctrlShiftEnter) {
729 Expects(list.filesToProcess.empty());
730
731 if (showSendingFilesError(list)) {
732 return;
733 }
734 auto groups = DivideByGroups(
735 std::move(list),
736 way,
737 _history->peer->slowmodeApplied());
738 const auto replyTo = replyToId();
739 const auto type = way.sendImagesAsPhotos()
740 ? SendMediaType::Photo
741 : SendMediaType::File;
742 auto action = Api::SendAction(_history);
743 action.replyTo = replyTo ? replyTo : _rootId;
744 action.options = options;
745 action.clearDraft = false;
746 if ((groups.size() != 1 || !groups.front().sentWithCaption())
747 && !caption.text.isEmpty()) {
748 auto message = Api::MessageToSend(_history);
749 message.textWithTags = base::take(caption);
750 message.action = action;
751 session().api().sendMessage(std::move(message));
752 }
753 for (auto &group : groups) {
754 const auto album = (group.type != Ui::AlbumType::None)
755 ? std::make_shared<SendingAlbum>()
756 : nullptr;
757 session().api().sendFiles(
758 std::move(group.list),
759 type,
760 base::take(caption),
761 album,
762 action);
763 }
764 if (_composeControls->replyingToMessage().msg == replyTo) {
765 _composeControls->cancelReplyMessage();
766 refreshTopBarActiveChat();
767 }
768 }
769
confirmSendingFiles(QImage && image,QByteArray && content,std::optional<bool> overrideSendImagesAsPhotos,const QString & insertTextOnCancel)770 bool RepliesWidget::confirmSendingFiles(
771 QImage &&image,
772 QByteArray &&content,
773 std::optional<bool> overrideSendImagesAsPhotos,
774 const QString &insertTextOnCancel) {
775 if (image.isNull()) {
776 return false;
777 }
778
779 auto list = Storage::PrepareMediaFromImage(
780 std::move(image),
781 std::move(content),
782 st::sendMediaPreviewSize);
783 list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
784 return confirmSendingFiles(std::move(list), insertTextOnCancel);
785 }
786
showSlowmodeError()787 bool RepliesWidget::showSlowmodeError() {
788 const auto text = [&] {
789 if (const auto left = _history->peer->slowmodeSecondsLeft()) {
790 return tr::lng_slowmode_enabled(
791 tr::now,
792 lt_left,
793 Ui::FormatDurationWords(left));
794 } else if (_history->peer->slowmodeApplied()) {
795 if (const auto item = _history->latestSendingMessage()) {
796 showAtPositionNow(item->position(), nullptr);
797 return tr::lng_slowmode_no_many(tr::now);
798 }
799 }
800 return QString();
801 }();
802 if (text.isEmpty()) {
803 return false;
804 }
805 Ui::ShowMultilineToast({
806 .text = { text },
807 });
808 return true;
809 }
810
writeRestriction() const811 std::optional<QString> RepliesWidget::writeRestriction() const {
812 return Data::RestrictionError(
813 _history->peer,
814 ChatRestriction::SendMessages);
815 }
816
pushReplyReturn(not_null<HistoryItem * > item)817 void RepliesWidget::pushReplyReturn(not_null<HistoryItem*> item) {
818 if (item->history() == _history && item->replyToTop() == _rootId) {
819 _replyReturns.push_back(item->id);
820 } else {
821 return;
822 }
823 _replyReturn = item;
824 updateScrollDownVisibility();
825 }
826
restoreReplyReturns(const std::vector<MsgId> & list)827 void RepliesWidget::restoreReplyReturns(const std::vector<MsgId> &list) {
828 _replyReturns = list;
829 computeCurrentReplyReturn();
830 if (!_replyReturn) {
831 calculateNextReplyReturn();
832 }
833 }
834
computeCurrentReplyReturn()835 void RepliesWidget::computeCurrentReplyReturn() {
836 _replyReturn = _replyReturns.empty()
837 ? nullptr
838 : _history->owner().message(
839 _history->channelId(),
840 _replyReturns.back());
841 }
842
calculateNextReplyReturn()843 void RepliesWidget::calculateNextReplyReturn() {
844 _replyReturn = nullptr;
845 while (!_replyReturns.empty() && !_replyReturn) {
846 _replyReturns.pop_back();
847 computeCurrentReplyReturn();
848 }
849 if (!_replyReturn) {
850 updateScrollDownVisibility();
851 }
852 }
853
checkReplyReturns()854 void RepliesWidget::checkReplyReturns() {
855 const auto currentTop = _scroll->scrollTop();
856 for (; _replyReturn != nullptr; calculateNextReplyReturn()) {
857 const auto position = _replyReturn->position();
858 const auto scrollTop = _inner->scrollTopForPosition(position);
859 const auto scrolledBelow = scrollTop
860 ? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax()))
861 : _inner->isBelowPosition(position);
862 if (!scrolledBelow) {
863 break;
864 }
865 }
866 }
867
uploadFile(const QByteArray & fileContent,SendMediaType type)868 void RepliesWidget::uploadFile(
869 const QByteArray &fileContent,
870 SendMediaType type) {
871 // #TODO replies schedule
872 auto action = Api::SendAction(_history);
873 action.replyTo = replyToId();
874 session().api().sendFile(fileContent, type, action);
875 }
876
showSendingFilesError(const Ui::PreparedList & list) const877 bool RepliesWidget::showSendingFilesError(
878 const Ui::PreparedList &list) const {
879 const auto text = [&] {
880 const auto peer = _history->peer;
881 const auto error = Data::RestrictionError(
882 peer,
883 ChatRestriction::SendMedia);
884 if (error) {
885 return *error;
886 }
887 if (peer->slowmodeApplied() && !list.canBeSentInSlowmode()) {
888 return tr::lng_slowmode_no_many(tr::now);
889 } else if (const auto left = _history->peer->slowmodeSecondsLeft()) {
890 return tr::lng_slowmode_enabled(
891 tr::now,
892 lt_left,
893 Ui::FormatDurationWords(left));
894 }
895 using Error = Ui::PreparedList::Error;
896 switch (list.error) {
897 case Error::None: return QString();
898 case Error::EmptyFile:
899 case Error::Directory:
900 case Error::NonLocalUrl: return tr::lng_send_image_empty(
901 tr::now,
902 lt_name,
903 list.errorData);
904 case Error::TooLargeFile: return tr::lng_send_image_too_large(
905 tr::now,
906 lt_name,
907 list.errorData);
908 }
909 return tr::lng_forward_send_files_cant(tr::now);
910 }();
911 if (text.isEmpty()) {
912 return false;
913 }
914
915 Ui::ShowMultilineToast({
916 .text = { text },
917 });
918 return true;
919 }
920
send()921 void RepliesWidget::send() {
922 if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) {
923 return;
924 }
925 send(Api::SendOptions());
926 // #TODO replies schedule
927 //const auto callback = [=](Api::SendOptions options) { send(options); };
928 //Ui::show(
929 // PrepareScheduleBox(this, sendMenuType(), callback),
930 // Ui::LayerOption::KeepOther);
931 }
932
sendVoice(ComposeControls::VoiceToSend && data)933 void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
934 auto action = Api::SendAction(_history);
935 action.replyTo = replyToId();
936 action.options = data.options;
937 session().api().sendVoiceMessage(
938 data.bytes,
939 data.waveform,
940 data.duration,
941 std::move(action));
942
943 _composeControls->cancelReplyMessage();
944 _composeControls->clearListenState();
945 finishSending();
946 }
947
send(Api::SendOptions options)948 void RepliesWidget::send(Api::SendOptions options) {
949 if (!options.scheduled && showSlowmodeError()) {
950 return;
951 }
952
953 const auto webPageId = _composeControls->webPageId();
954
955 auto message = ApiWrap::MessageToSend(_history);
956 message.textWithTags = _composeControls->getTextWithAppliedMarkdown();
957 message.action.options = options;
958 message.action.replyTo = replyToId();
959 message.webPageId = webPageId;
960
961 //const auto error = GetErrorTextForSending(
962 // _peer,
963 // _toForward,
964 // message.textWithTags);
965 //if (!error.isEmpty()) {
966 // Ui::ShowMultilineToast({
967 // .text = { error },
968 // });
969 // return;
970 //}
971
972 session().api().sendMessage(std::move(message));
973
974 _composeControls->clear();
975 session().sendProgressManager().update(
976 _history,
977 _rootId,
978 Api::SendProgressType::Typing,
979 -1);
980
981 //_saveDraftText = true;
982 //_saveDraftStart = crl::now();
983 //onDraftSave();
984
985 finishSending();
986 }
987
edit(not_null<HistoryItem * > item,Api::SendOptions options,mtpRequestId * const saveEditMsgRequestId)988 void RepliesWidget::edit(
989 not_null<HistoryItem*> item,
990 Api::SendOptions options,
991 mtpRequestId *const saveEditMsgRequestId) {
992 if (*saveEditMsgRequestId) {
993 return;
994 }
995 const auto textWithTags = _composeControls->getTextWithAppliedMarkdown();
996 const auto prepareFlags = Ui::ItemTextOptions(
997 _history,
998 session().user()).flags;
999 auto sending = TextWithEntities();
1000 auto left = TextWithEntities {
1001 textWithTags.text,
1002 TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };
1003 TextUtilities::PrepareForSending(left, prepareFlags);
1004
1005 if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) {
1006 if (item) {
1007 controller()->show(Box<DeleteMessagesBox>(item, false));
1008 } else {
1009 doSetInnerFocus();
1010 }
1011 return;
1012 } else if (!left.text.isEmpty()) {
1013 controller()->show(Box<Ui::InformBox>(
1014 tr::lng_edit_too_long(tr::now)));
1015 return;
1016 }
1017
1018 lifetime().add([=] {
1019 if (!*saveEditMsgRequestId) {
1020 return;
1021 }
1022 session().api().request(base::take(*saveEditMsgRequestId)).cancel();
1023 });
1024
1025 const auto done = [=](const MTPUpdates &result, mtpRequestId requestId) {
1026 if (requestId == *saveEditMsgRequestId) {
1027 *saveEditMsgRequestId = 0;
1028 _composeControls->cancelEditMessage();
1029 }
1030 };
1031
1032 const auto fail = [=](const MTP::Error &error, mtpRequestId requestId) {
1033 if (requestId == *saveEditMsgRequestId) {
1034 *saveEditMsgRequestId = 0;
1035 }
1036
1037 const auto &err = error.type();
1038 if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) {
1039 controller()->show(Box<Ui::InformBox>(
1040 tr::lng_edit_error(tr::now)));
1041 } else if (err == u"MESSAGE_NOT_MODIFIED"_q) {
1042 _composeControls->cancelEditMessage();
1043 } else if (err == u"MESSAGE_EMPTY"_q) {
1044 doSetInnerFocus();
1045 } else {
1046 controller()->show(Box<Ui::InformBox>(
1047 tr::lng_edit_error(tr::now)));
1048 }
1049 update();
1050 return true;
1051 };
1052
1053 *saveEditMsgRequestId = Api::EditTextMessage(
1054 item,
1055 sending,
1056 options,
1057 crl::guard(this, done),
1058 crl::guard(this, fail));
1059
1060 _composeControls->hidePanelsAnimated();
1061 doSetInnerFocus();
1062 }
1063
sendExistingDocument(not_null<DocumentData * > document)1064 void RepliesWidget::sendExistingDocument(
1065 not_null<DocumentData*> document) {
1066 sendExistingDocument(document, Api::SendOptions());
1067 // #TODO replies schedule
1068 //const auto callback = [=](Api::SendOptions options) {
1069 // sendExistingDocument(document, options);
1070 //};
1071 //Ui::show(
1072 // PrepareScheduleBox(this, sendMenuType(), callback),
1073 // Ui::LayerOption::KeepOther);
1074 }
1075
sendExistingDocument(not_null<DocumentData * > document,Api::SendOptions options)1076 bool RepliesWidget::sendExistingDocument(
1077 not_null<DocumentData*> document,
1078 Api::SendOptions options) {
1079 const auto error = Data::RestrictionError(
1080 _history->peer,
1081 ChatRestriction::SendStickers);
1082 if (error) {
1083 controller()->show(
1084 Box<Ui::InformBox>(*error),
1085 Ui::LayerOption::KeepOther);
1086 return false;
1087 } else if (showSlowmodeError()) {
1088 return false;
1089 }
1090
1091 auto message = Api::MessageToSend(_history);
1092 message.action.replyTo = replyToId();
1093 message.action.options = options;
1094 Api::SendExistingDocument(std::move(message), document);
1095
1096 _composeControls->cancelReplyMessage();
1097 finishSending();
1098 return true;
1099 }
1100
sendExistingPhoto(not_null<PhotoData * > photo)1101 void RepliesWidget::sendExistingPhoto(not_null<PhotoData*> photo) {
1102 sendExistingPhoto(photo, Api::SendOptions());
1103 // #TODO replies schedule
1104 //const auto callback = [=](Api::SendOptions options) {
1105 // sendExistingPhoto(photo, options);
1106 //};
1107 //Ui::show(
1108 // PrepareScheduleBox(this, sendMenuType(), callback),
1109 // Ui::LayerOption::KeepOther);
1110 }
1111
sendExistingPhoto(not_null<PhotoData * > photo,Api::SendOptions options)1112 bool RepliesWidget::sendExistingPhoto(
1113 not_null<PhotoData*> photo,
1114 Api::SendOptions options) {
1115 const auto error = Data::RestrictionError(
1116 _history->peer,
1117 ChatRestriction::SendMedia);
1118 if (error) {
1119 controller()->show(
1120 Box<Ui::InformBox>(*error),
1121 Ui::LayerOption::KeepOther);
1122 return false;
1123 } else if (showSlowmodeError()) {
1124 return false;
1125 }
1126
1127 auto message = Api::MessageToSend(_history);
1128 message.action.replyTo = replyToId();
1129 message.action.options = options;
1130 Api::SendExistingPhoto(std::move(message), photo);
1131
1132 _composeControls->cancelReplyMessage();
1133 finishSending();
1134 return true;
1135 }
1136
sendInlineResult(not_null<InlineBots::Result * > result,not_null<UserData * > bot)1137 void RepliesWidget::sendInlineResult(
1138 not_null<InlineBots::Result*> result,
1139 not_null<UserData*> bot) {
1140 const auto errorText = result->getErrorOnSend(_history);
1141 if (!errorText.isEmpty()) {
1142 controller()->show(Box<Ui::InformBox>(errorText));
1143 return;
1144 }
1145 sendInlineResult(result, bot, Api::SendOptions());
1146 //const auto callback = [=](Api::SendOptions options) {
1147 // sendInlineResult(result, bot, options);
1148 //};
1149 //Ui::show(
1150 // PrepareScheduleBox(this, sendMenuType(), callback),
1151 // Ui::LayerOption::KeepOther);
1152 }
1153
sendInlineResult(not_null<InlineBots::Result * > result,not_null<UserData * > bot,Api::SendOptions options)1154 void RepliesWidget::sendInlineResult(
1155 not_null<InlineBots::Result*> result,
1156 not_null<UserData*> bot,
1157 Api::SendOptions options) {
1158 auto action = Api::SendAction(_history);
1159 action.replyTo = replyToId();
1160 action.options = options;
1161 action.generateLocal = true;
1162 session().api().sendInlineResult(bot, result, action);
1163
1164 _composeControls->clear();
1165 //_saveDraftText = true;
1166 //_saveDraftStart = crl::now();
1167 //onDraftSave();
1168
1169 auto &bots = cRefRecentInlineBots();
1170 const auto index = bots.indexOf(bot);
1171 if (index) {
1172 if (index > 0) {
1173 bots.removeAt(index);
1174 } else if (bots.size() >= RecentInlineBotsLimit) {
1175 bots.resize(RecentInlineBotsLimit - 1);
1176 }
1177 bots.push_front(bot);
1178 bot->session().local().writeRecentHashtagsAndBots();
1179 }
1180 finishSending();
1181 }
1182
sendMenuType() const1183 SendMenu::Type RepliesWidget::sendMenuType() const {
1184 // #TODO replies schedule
1185 return _history->peer->isSelf()
1186 ? SendMenu::Type::Reminder
1187 : HistoryView::CanScheduleUntilOnline(_history->peer)
1188 ? SendMenu::Type::ScheduledToUser
1189 : SendMenu::Type::Scheduled;
1190 }
1191
refreshTopBarActiveChat()1192 void RepliesWidget::refreshTopBarActiveChat() {
1193 const auto state = Dialogs::EntryState{
1194 .key = _history,
1195 .section = Dialogs::EntryState::Section::Replies,
1196 .rootId = _rootId,
1197 .currentReplyToId = _composeControls->replyingToMessage().msg,
1198 };
1199 _topBar->setActiveChat(state, _sendAction.get());
1200 _composeControls->setCurrentDialogsEntryState(state);
1201 }
1202
replyToId() const1203 MsgId RepliesWidget::replyToId() const {
1204 const auto custom = _composeControls->replyingToMessage().msg;
1205 return custom ? custom : _rootId;
1206 }
1207
setupScrollDownButton()1208 void RepliesWidget::setupScrollDownButton() {
1209 _scrollDown->setClickedCallback([=] {
1210 scrollDownClicked();
1211 });
1212 refreshUnreadCountBadge();
1213 base::install_event_filter(_scrollDown, [=](not_null<QEvent*> event) {
1214 if (event->type() != QEvent::Wheel) {
1215 return base::EventFilterResult::Continue;
1216 }
1217 return _scroll->viewportEvent(event)
1218 ? base::EventFilterResult::Cancel
1219 : base::EventFilterResult::Continue;
1220 });
1221 updateScrollDownVisibility();
1222 }
1223
refreshUnreadCountBadge()1224 void RepliesWidget::refreshUnreadCountBadge() {
1225 if (!_root) {
1226 return;
1227 } else if (const auto count = computeUnreadCount()) {
1228 _scrollDown->setUnreadCount(*count);
1229 } else if (!_readRequestPending
1230 && !_readRequestTimer.isActive()
1231 && !_readRequestId) {
1232 reloadUnreadCountIfNeeded();
1233 }
1234 }
1235
reloadUnreadCountIfNeeded()1236 void RepliesWidget::reloadUnreadCountIfNeeded() {
1237 const auto views = _root ? _root->Get<HistoryMessageViews>() : nullptr;
1238 if (!views || views->repliesUnreadCount >= 0) {
1239 return;
1240 } else if (views->repliesInboxReadTillId
1241 < _root->computeRepliesInboxReadTillFull()) {
1242 _readRequestTimer.callOnce(0);
1243 } else if (!_reloadUnreadCountRequestId) {
1244 const auto session = &_history->session();
1245 const auto fullId = _root->fullId();
1246 const auto apply = [session, fullId](int readTill, int unreadCount) {
1247 if (const auto root = session->data().message(fullId)) {
1248 root->setRepliesInboxReadTill(readTill, unreadCount);
1249 if (const auto post = root->lookupDiscussionPostOriginal()) {
1250 post->setRepliesInboxReadTill(readTill, unreadCount);
1251 }
1252 }
1253 };
1254 const auto weak = Ui::MakeWeak(this);
1255 _reloadUnreadCountRequestId = session->api().request(
1256 MTPmessages_GetDiscussionMessage(
1257 _history->peer->input,
1258 MTP_int(_rootId))
1259 ).done([=](const MTPmessages_DiscussionMessage &result) {
1260 if (weak) {
1261 _reloadUnreadCountRequestId = 0;
1262 }
1263 result.match([&](const MTPDmessages_discussionMessage &data) {
1264 session->data().processUsers(data.vusers());
1265 session->data().processChats(data.vchats());
1266 apply(
1267 data.vread_inbox_max_id().value_or_empty(),
1268 data.vunread_count().v);
1269 });
1270 }).send();
1271 }
1272 }
1273
scrollDownClicked()1274 void RepliesWidget::scrollDownClicked() {
1275 if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) {
1276 showAtEnd();
1277 } else if (_replyReturn) {
1278 showAtPosition(_replyReturn->position());
1279 } else {
1280 showAtEnd();
1281 }
1282 }
1283
showAtStart()1284 void RepliesWidget::showAtStart() {
1285 showAtPosition(Data::MinMessagePosition);
1286 }
1287
showAtEnd()1288 void RepliesWidget::showAtEnd() {
1289 showAtPosition(Data::MaxMessagePosition);
1290 }
1291
finishSending()1292 void RepliesWidget::finishSending() {
1293 _composeControls->hidePanelsAnimated();
1294 //if (_previewData && _previewData->pendingTill) previewCancel();
1295 doSetInnerFocus();
1296 showAtEnd();
1297 refreshTopBarActiveChat();
1298 }
1299
showAtPosition(Data::MessagePosition position,HistoryItem * originItem)1300 void RepliesWidget::showAtPosition(
1301 Data::MessagePosition position,
1302 HistoryItem *originItem) {
1303 if (!showAtPositionNow(position, originItem)) {
1304 _inner->showAroundPosition(position, [=] {
1305 return showAtPositionNow(position, originItem);
1306 });
1307 }
1308 }
1309
showAtPositionNow(Data::MessagePosition position,HistoryItem * originItem,anim::type animated)1310 bool RepliesWidget::showAtPositionNow(
1311 Data::MessagePosition position,
1312 HistoryItem *originItem,
1313 anim::type animated) {
1314 using AnimatedScroll = HistoryView::ListWidget::AnimatedScroll;
1315 const auto item = position.fullId
1316 ? _history->owner().message(position.fullId)
1317 : nullptr;
1318 const auto use = item ? item->position() : position;
1319 if (const auto scrollTop = _inner->scrollTopForPosition(use)) {
1320 while (_replyReturn && use.fullId.msg == _replyReturn->id) {
1321 calculateNextReplyReturn();
1322 }
1323 const auto currentScrollTop = _scroll->scrollTop();
1324 const auto wanted = std::clamp(
1325 *scrollTop,
1326 0,
1327 _scroll->scrollTopMax());
1328 const auto fullDelta = (wanted - currentScrollTop);
1329 const auto limit = _scroll->height();
1330 const auto scrollDelta = std::clamp(fullDelta, -limit, limit);
1331 const auto type = (animated == anim::type::instant)
1332 ? AnimatedScroll::None
1333 : (std::abs(fullDelta) > limit)
1334 ? AnimatedScroll::Part
1335 : AnimatedScroll::Full;
1336 _inner->scrollTo(
1337 wanted,
1338 use,
1339 scrollDelta,
1340 type);
1341 if (use != Data::MaxMessagePosition
1342 && use != Data::UnreadMessagePosition) {
1343 _inner->highlightMessage(use.fullId);
1344 }
1345 if (originItem) {
1346 pushReplyReturn(originItem);
1347 }
1348 return true;
1349 }
1350 return false;
1351 }
1352
updateScrollDownVisibility()1353 void RepliesWidget::updateScrollDownVisibility() {
1354 if (animating()) {
1355 return;
1356 }
1357
1358 const auto scrollDownIsVisible = [&]() -> std::optional<bool> {
1359 if (_composeControls->isLockPresent()) {
1360 return false;
1361 }
1362 const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
1363 if (top < _scroll->scrollTopMax() || _replyReturn) {
1364 return true;
1365 } else if (_inner->loadedAtBottomKnown()) {
1366 return !_inner->loadedAtBottom();
1367 }
1368 return std::nullopt;
1369 };
1370 const auto scrollDownIsShown = scrollDownIsVisible();
1371 if (!scrollDownIsShown) {
1372 return;
1373 }
1374 if (_scrollDownIsShown != *scrollDownIsShown) {
1375 _scrollDownIsShown = *scrollDownIsShown;
1376 _scrollDownShown.start(
1377 [=] { updateScrollDownPosition(); },
1378 _scrollDownIsShown ? 0. : 1.,
1379 _scrollDownIsShown ? 1. : 0.,
1380 st::historyToDownDuration);
1381 }
1382 }
1383
updateScrollDownPosition()1384 void RepliesWidget::updateScrollDownPosition() {
1385 // _scrollDown is a child widget of _scroll, not me.
1386 auto top = anim::interpolate(
1387 0,
1388 _scrollDown->height() + st::historyToDownPosition.y(),
1389 _scrollDownShown.value(_scrollDownIsShown ? 1. : 0.));
1390 _scrollDown->moveToRight(
1391 st::historyToDownPosition.x(),
1392 _scroll->height() - top);
1393 auto shouldBeHidden = !_scrollDownIsShown && !_scrollDownShown.animating();
1394 if (shouldBeHidden != _scrollDown->isHidden()) {
1395 _scrollDown->setVisible(!shouldBeHidden);
1396 }
1397 }
1398
scrollDownAnimationFinish()1399 void RepliesWidget::scrollDownAnimationFinish() {
1400 _scrollDownShown.stop();
1401 updateScrollDownPosition();
1402 }
1403
updateAdaptiveLayout()1404 void RepliesWidget::updateAdaptiveLayout() {
1405 _topBarShadow->moveToLeft(
1406 controller()->adaptive().isOneColumn() ? 0 : st::lineWidth,
1407 _topBar->height());
1408 }
1409
history() const1410 not_null<History*> RepliesWidget::history() const {
1411 return _history;
1412 }
1413
activeChat() const1414 Dialogs::RowDescriptor RepliesWidget::activeChat() const {
1415 return {
1416 _history,
1417 FullMsgId(_history->channelId(), ShowAtUnreadMsgId)
1418 };
1419 }
1420
preventsClose(Fn<void ()> && continueCallback) const1421 bool RepliesWidget::preventsClose(Fn<void()> &&continueCallback) const {
1422 return _composeControls->preventsClose(std::move(continueCallback));
1423 }
1424
grabForShowAnimation(const Window::SectionSlideParams & params)1425 QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) {
1426 _topBar->updateControlsVisibility();
1427 if (params.withTopBarShadow) _topBarShadow->hide();
1428 _composeControls->showForGrab();
1429 auto result = Ui::GrabWidget(this);
1430 if (params.withTopBarShadow) _topBarShadow->show();
1431 _rootView->hide();
1432 return result;
1433 }
1434
doSetInnerFocus()1435 void RepliesWidget::doSetInnerFocus() {
1436 if (!_inner->getSelectedText().rich.text.isEmpty()
1437 || !_inner->getSelectedItems().empty()
1438 || !_composeControls->focus()) {
1439 _inner->setFocus();
1440 }
1441 }
1442
showInternal(not_null<Window::SectionMemento * > memento,const Window::SectionShow & params)1443 bool RepliesWidget::showInternal(
1444 not_null<Window::SectionMemento*> memento,
1445 const Window::SectionShow ¶ms) {
1446 if (auto logMemento = dynamic_cast<RepliesMemento*>(memento.get())) {
1447 if (logMemento->getHistory() == history()
1448 && logMemento->getRootId() == _rootId) {
1449 restoreState(logMemento);
1450 return true;
1451 }
1452 }
1453 return false;
1454 }
1455
setInternalState(const QRect & geometry,not_null<RepliesMemento * > memento)1456 void RepliesWidget::setInternalState(
1457 const QRect &geometry,
1458 not_null<RepliesMemento*> memento) {
1459 setGeometry(geometry);
1460 Ui::SendPendingMoveResizeEvents(this);
1461 restoreState(memento);
1462 }
1463
pushTabbedSelectorToThirdSection(not_null<PeerData * > peer,const Window::SectionShow & params)1464 bool RepliesWidget::pushTabbedSelectorToThirdSection(
1465 not_null<PeerData*> peer,
1466 const Window::SectionShow ¶ms) {
1467 return _composeControls->pushTabbedSelectorToThirdSection(peer, params);
1468 }
1469
returnTabbedSelector()1470 bool RepliesWidget::returnTabbedSelector() {
1471 return _composeControls->returnTabbedSelector();
1472 }
1473
createMemento()1474 std::shared_ptr<Window::SectionMemento> RepliesWidget::createMemento() {
1475 auto result = std::make_shared<RepliesMemento>(history(), _rootId);
1476 saveState(result.get());
1477 return result;
1478 }
1479
showMessage(PeerId peerId,const Window::SectionShow & params,MsgId messageId)1480 bool RepliesWidget::showMessage(
1481 PeerId peerId,
1482 const Window::SectionShow ¶ms,
1483 MsgId messageId) {
1484 if (peerId != _history->peer->id) {
1485 return false;
1486 }
1487 const auto id = FullMsgId{
1488 _history->channelId(),
1489 messageId
1490 };
1491 const auto message = _history->owner().message(id);
1492 if (!message || message->replyToTop() != _rootId) {
1493 return false;
1494 }
1495
1496 const auto originItem = [&]() -> HistoryItem* {
1497 using OriginMessage = Window::SectionShow::OriginMessage;
1498 if (const auto origin = std::get_if<OriginMessage>(¶ms.origin)) {
1499 if (const auto returnTo = session().data().message(origin->id)) {
1500 if (returnTo->history() == _history
1501 && returnTo->replyToTop() == _rootId
1502 && _replyReturn != returnTo) {
1503 return returnTo;
1504 }
1505 }
1506 }
1507 return nullptr;
1508 }();
1509 showAtPosition(
1510 Data::MessagePosition{ .fullId = id, .date = message->date() },
1511 originItem);
1512 return true;
1513 }
1514
sendBotCommand(Bot::SendCommandRequest request)1515 Window::SectionActionResult RepliesWidget::sendBotCommand(
1516 Bot::SendCommandRequest request) {
1517 if (request.peer != _history->peer) {
1518 return Window::SectionActionResult::Ignore;
1519 }
1520 listSendBotCommand(request.command, request.context);
1521 return Window::SectionActionResult::Handle;
1522 }
1523
replyToMessage(FullMsgId itemId)1524 void RepliesWidget::replyToMessage(FullMsgId itemId) {
1525 // if (item->history() != _history || item->replyToTop() != _rootId) {
1526 _composeControls->replyToMessage(itemId);
1527 refreshTopBarActiveChat();
1528 }
1529
saveState(not_null<RepliesMemento * > memento)1530 void RepliesWidget::saveState(not_null<RepliesMemento*> memento) {
1531 memento->setReplies(_replies);
1532 memento->setReplyReturns(_replyReturns);
1533 _inner->saveState(memento->list());
1534 }
1535
restoreState(not_null<RepliesMemento * > memento)1536 void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) {
1537 const auto setReplies = [&](std::shared_ptr<Data::RepliesList> replies) {
1538 _replies = std::move(replies);
1539
1540 rpl::combine(
1541 rpl::single(0) | rpl::then(_replies->fullCount()),
1542 _areComments.value()
1543 ) | rpl::map([=](int count, bool areComments) {
1544 return count
1545 ? (areComments
1546 ? tr::lng_comments_header
1547 : tr::lng_replies_header)(
1548 lt_count_decimal,
1549 rpl::single(count) | tr::to_count())
1550 : (areComments
1551 ? tr::lng_comments_header_none
1552 : tr::lng_replies_header_none)();
1553 }) | rpl::flatten_latest(
1554 ) | rpl::start_with_next([=](const QString &text) {
1555 _topBar->setCustomTitle(text);
1556 }, lifetime());
1557 };
1558 if (auto replies = memento->getReplies()) {
1559 setReplies(std::move(replies));
1560 } else if (!_replies) {
1561 setReplies(std::make_shared<Data::RepliesList>(_history, _rootId));
1562 }
1563 restoreReplyReturns(memento->replyReturns());
1564 _inner->restoreState(memento->list());
1565 if (const auto highlight = memento->getHighlightId()) {
1566 const auto position = Data::MessagePosition{
1567 .fullId = FullMsgId(_history->channelId(), highlight),
1568 .date = TimeId(0),
1569 };
1570 _inner->showAroundPosition(position, [=] {
1571 return showAtPositionNow(position, nullptr);
1572 });
1573 }
1574 }
1575
resizeEvent(QResizeEvent * e)1576 void RepliesWidget::resizeEvent(QResizeEvent *e) {
1577 if (!width() || !height()) {
1578 return;
1579 }
1580 _composeControls->resizeToWidth(width());
1581 recountChatWidth();
1582 updateControlsGeometry();
1583 }
1584
recountChatWidth()1585 void RepliesWidget::recountChatWidth() {
1586 auto layout = (width() < st::adaptiveChatWideWidth)
1587 ? Window::Adaptive::ChatLayout::Normal
1588 : Window::Adaptive::ChatLayout::Wide;
1589 controller()->adaptive().setChatLayout(layout);
1590 }
1591
updateControlsGeometry()1592 void RepliesWidget::updateControlsGeometry() {
1593 const auto contentWidth = width();
1594
1595 const auto newScrollTop = _scroll->isHidden()
1596 ? std::nullopt
1597 : base::make_optional(_scroll->scrollTop() + topDelta());
1598 _topBar->resizeToWidth(contentWidth);
1599 _topBarShadow->resize(contentWidth, st::lineWidth);
1600 if (_rootView) {
1601 _rootView->resizeToWidth(contentWidth);
1602 }
1603 _rootView->resizeToWidth(contentWidth);
1604
1605 const auto bottom = height();
1606 const auto controlsHeight = _composeControls->heightCurrent();
1607 const auto scrollY = _topBar->height() + _rootViewHeight;
1608 const auto scrollHeight = bottom - scrollY - controlsHeight;
1609 const auto scrollSize = QSize(contentWidth, scrollHeight);
1610 if (_scroll->size() != scrollSize) {
1611 _skipScrollEvent = true;
1612 _scroll->resize(scrollSize);
1613 _inner->resizeToWidth(scrollSize.width(), _scroll->height());
1614 _skipScrollEvent = false;
1615 }
1616 _scroll->move(0, scrollY);
1617 if (!_scroll->isHidden()) {
1618 if (newScrollTop) {
1619 _scroll->scrollToY(*newScrollTop);
1620 }
1621 updateInnerVisibleArea();
1622 }
1623 _composeControls->move(0, bottom - controlsHeight);
1624 _composeControls->setAutocompleteBoundingRect(_scroll->geometry());
1625
1626 updateScrollDownPosition();
1627 }
1628
paintEvent(QPaintEvent * e)1629 void RepliesWidget::paintEvent(QPaintEvent *e) {
1630 if (animating()) {
1631 SectionWidget::paintEvent(e);
1632 return;
1633 } else if (Ui::skipPaintEvent(this, e)) {
1634 return;
1635 }
1636
1637 const auto aboveHeight = _topBar->height();
1638 const auto bg = e->rect().intersected(
1639 QRect(0, aboveHeight, width(), height() - aboveHeight));
1640 SectionWidget::PaintBackground(controller(), _theme.get(), this, bg);
1641 }
1642
onScroll()1643 void RepliesWidget::onScroll() {
1644 if (_skipScrollEvent) {
1645 return;
1646 }
1647 updateInnerVisibleArea();
1648 }
1649
updateInnerVisibleArea()1650 void RepliesWidget::updateInnerVisibleArea() {
1651 if (!_inner->animatedScrolling()) {
1652 checkReplyReturns();
1653 }
1654 const auto scrollTop = _scroll->scrollTop();
1655 _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
1656 updatePinnedVisibility();
1657 updateScrollDownVisibility();
1658 }
1659
updatePinnedVisibility()1660 void RepliesWidget::updatePinnedVisibility() {
1661 if (!_loaded) {
1662 return;
1663 } else if (!_root) {
1664 setPinnedVisibility(true);
1665 return;
1666 }
1667 const auto item = [&] {
1668 if (const auto group = _history->owner().groups().find(_root)) {
1669 return group->items.front().get();
1670 }
1671 return _root;
1672 }();
1673 const auto view = _inner->viewByPosition(item->position());
1674 const auto visible = !view
1675 || (view->y() + view->height() <= _scroll->scrollTop());
1676 setPinnedVisibility(visible);
1677 }
1678
setPinnedVisibility(bool shown)1679 void RepliesWidget::setPinnedVisibility(bool shown) {
1680 if (!animating()) {
1681 if (!_rootViewInited) {
1682 const auto height = shown ? st::historyReplyHeight : 0;
1683 if (const auto delta = height - _rootViewHeight) {
1684 _rootViewHeight = height;
1685 if (_scroll->scrollTop() == _scroll->scrollTopMax()) {
1686 setGeometryWithTopMoved(geometry(), delta);
1687 } else {
1688 updateControlsGeometry();
1689 }
1690 }
1691 if (shown) {
1692 _rootView->show();
1693 } else {
1694 _rootView->hide();
1695 }
1696 _rootVisible = shown;
1697 _rootView->finishAnimating();
1698 _rootViewInited = true;
1699 } else {
1700 _rootVisible = shown;
1701 }
1702 }
1703 }
1704
showAnimatedHook(const Window::SectionSlideParams & params)1705 void RepliesWidget::showAnimatedHook(
1706 const Window::SectionSlideParams ¶ms) {
1707 _topBar->setAnimatingMode(true);
1708 if (params.withTopBarShadow) {
1709 _topBarShadow->show();
1710 }
1711 _composeControls->showStarted();
1712 }
1713
showFinishedHook()1714 void RepliesWidget::showFinishedHook() {
1715 _topBar->setAnimatingMode(false);
1716 _composeControls->showFinished();
1717 _rootView->show();
1718
1719 // We should setup the drag area only after
1720 // the section animation is finished,
1721 // because after that the method showChildren() is called.
1722 setupDragArea();
1723 updatePinnedVisibility();
1724 }
1725
floatPlayerHandleWheelEvent(QEvent * e)1726 bool RepliesWidget::floatPlayerHandleWheelEvent(QEvent *e) {
1727 return _scroll->viewportEvent(e);
1728 }
1729
floatPlayerAvailableRect()1730 QRect RepliesWidget::floatPlayerAvailableRect() {
1731 return mapToGlobal(_scroll->geometry());
1732 }
1733
listContext()1734 Context RepliesWidget::listContext() {
1735 return Context::Replies;
1736 }
1737
listScrollTo(int top)1738 void RepliesWidget::listScrollTo(int top) {
1739 if (_scroll->scrollTop() != top) {
1740 _scroll->scrollToY(top);
1741 } else {
1742 updateInnerVisibleArea();
1743 }
1744 }
1745
listCancelRequest()1746 void RepliesWidget::listCancelRequest() {
1747 if (_inner && !_inner->getSelectedItems().empty()) {
1748 clearSelected();
1749 return;
1750 } else if (_composeControls->handleCancelRequest()) {
1751 refreshTopBarActiveChat();
1752 return;
1753 }
1754 controller()->showBackFromStack();
1755 }
1756
listDeleteRequest()1757 void RepliesWidget::listDeleteRequest() {
1758 confirmDeleteSelected();
1759 }
1760
listSource(Data::MessagePosition aroundId,int limitBefore,int limitAfter)1761 rpl::producer<Data::MessagesSlice> RepliesWidget::listSource(
1762 Data::MessagePosition aroundId,
1763 int limitBefore,
1764 int limitAfter) {
1765 return _replies->source(
1766 aroundId,
1767 limitBefore,
1768 limitAfter
1769 ) | rpl::before_next([=] { // after_next makes a copy of value.
1770 if (!_loaded) {
1771 _loaded = true;
1772 crl::on_main(this, [=] {
1773 updatePinnedVisibility();
1774 });
1775 }
1776 });
1777 }
1778
listAllowsMultiSelect()1779 bool RepliesWidget::listAllowsMultiSelect() {
1780 return true;
1781 }
1782
listIsItemGoodForSelection(not_null<HistoryItem * > item)1783 bool RepliesWidget::listIsItemGoodForSelection(
1784 not_null<HistoryItem*> item) {
1785 return item->isRegular();
1786 }
1787
listIsLessInOrder(not_null<HistoryItem * > first,not_null<HistoryItem * > second)1788 bool RepliesWidget::listIsLessInOrder(
1789 not_null<HistoryItem*> first,
1790 not_null<HistoryItem*> second) {
1791 return first->position() < second->position();
1792 }
1793
listSelectionChanged(SelectedItems && items)1794 void RepliesWidget::listSelectionChanged(SelectedItems &&items) {
1795 HistoryView::TopBarWidget::SelectedState state;
1796 state.count = items.size();
1797 for (const auto &item : items) {
1798 if (item.canDelete) {
1799 ++state.canDeleteCount;
1800 }
1801 if (item.canForward) {
1802 ++state.canForwardCount;
1803 }
1804 }
1805 _topBar->showSelected(state);
1806 }
1807
computeUnreadCountLocally(MsgId afterId) const1808 std::optional<int> RepliesWidget::computeUnreadCountLocally(
1809 MsgId afterId) const {
1810 const auto views = _root ? _root->Get<HistoryMessageViews>() : nullptr;
1811 if (!views) {
1812 return std::nullopt;
1813 }
1814 const auto wasReadTillId = views->repliesInboxReadTillId;
1815 const auto wasUnreadCount = views->repliesUnreadCount;
1816 return _replies->fullUnreadCountAfter(
1817 afterId,
1818 wasReadTillId,
1819 wasUnreadCount);
1820 }
1821
readTill(not_null<HistoryItem * > item)1822 void RepliesWidget::readTill(not_null<HistoryItem*> item) {
1823 if (!_root) {
1824 return;
1825 }
1826 const auto was = _root->computeRepliesInboxReadTillFull();
1827 const auto now = item->id;
1828 if (now < was) {
1829 return;
1830 }
1831 const auto unreadCount = computeUnreadCountLocally(now);
1832 const auto fast = item->out() || !unreadCount.has_value();
1833 if (was < now || (fast && now == was)) {
1834 _root->setRepliesInboxReadTill(now, unreadCount);
1835 if (const auto post = _root->lookupDiscussionPostOriginal()) {
1836 post->setRepliesInboxReadTill(now, unreadCount);
1837 }
1838 if (!_readRequestTimer.isActive()) {
1839 _readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout);
1840 } else if (fast && _readRequestTimer.remainingTime() > 0) {
1841 _readRequestTimer.callOnce(0);
1842 }
1843 }
1844 }
1845
listVisibleItemsChanged(HistoryItemsList && items)1846 void RepliesWidget::listVisibleItemsChanged(HistoryItemsList &&items) {
1847 const auto reversed = ranges::views::reverse(items);
1848 const auto good = ranges::find_if(reversed, &HistoryItem::isRegular);
1849 if (good != end(reversed)) {
1850 readTill(*good);
1851 }
1852 }
1853
listMessagesBar(const std::vector<not_null<Element * >> & elements)1854 MessagesBarData RepliesWidget::listMessagesBar(
1855 const std::vector<not_null<Element*>> &elements) {
1856 if (!_root || elements.empty()) {
1857 return {};
1858 }
1859 const auto till = _root->computeRepliesInboxReadTillFull();
1860 const auto hidden = (till < 2);
1861 for (auto i = 0, count = int(elements.size()); i != count; ++i) {
1862 const auto item = elements[i]->data();
1863 if (item->isRegular() && item->id > till) {
1864 if (item->out() || !item->replyToId()) {
1865 readTill(item);
1866 } else {
1867 return {
1868 .bar = {
1869 .element = elements[i],
1870 .hidden = hidden,
1871 .focus = true,
1872 },
1873 .text = tr::lng_unread_bar_some(),
1874 };
1875 }
1876 }
1877 }
1878 return {};
1879 }
1880
listContentRefreshed()1881 void RepliesWidget::listContentRefreshed() {
1882 }
1883
listDateLink(not_null<Element * > view)1884 ClickHandlerPtr RepliesWidget::listDateLink(not_null<Element*> view) {
1885 return nullptr;
1886 }
1887
listElementHideReply(not_null<const Element * > view)1888 bool RepliesWidget::listElementHideReply(not_null<const Element*> view) {
1889 return (view->data()->replyToId() == _rootId);
1890 }
1891
listElementShownUnread(not_null<const Element * > view)1892 bool RepliesWidget::listElementShownUnread(not_null<const Element*> view) {
1893 if (!_root) {
1894 return false;
1895 }
1896 const auto item = view->data();
1897 const auto till = item->out()
1898 ? _root->computeRepliesOutboxReadTillFull()
1899 : _root->computeRepliesInboxReadTillFull();
1900 return (item->id > till);
1901 }
1902
listIsGoodForAroundPosition(not_null<const Element * > view)1903 bool RepliesWidget::listIsGoodForAroundPosition(
1904 not_null<const Element*> view) {
1905 return view->data()->isRegular();
1906 }
1907
listSendBotCommand(const QString & command,const FullMsgId & context)1908 void RepliesWidget::listSendBotCommand(
1909 const QString &command,
1910 const FullMsgId &context) {
1911 const auto text = Bot::WrapCommandInChat(
1912 _history->peer,
1913 command,
1914 context);
1915 auto message = ApiWrap::MessageToSend(_history);
1916 message.textWithTags = { text };
1917 message.action.replyTo = replyToId();
1918 session().api().sendMessage(std::move(message));
1919 finishSending();
1920 }
1921
listHandleViaClick(not_null<UserData * > bot)1922 void RepliesWidget::listHandleViaClick(not_null<UserData*> bot) {
1923 _composeControls->setText({ '@' + bot->username + ' ' });
1924 }
1925
listChatTheme()1926 not_null<Ui::ChatTheme*> RepliesWidget::listChatTheme() {
1927 return _theme.get();
1928 }
1929
confirmDeleteSelected()1930 void RepliesWidget::confirmDeleteSelected() {
1931 ConfirmDeleteSelectedItems(_inner);
1932 }
1933
confirmForwardSelected()1934 void RepliesWidget::confirmForwardSelected() {
1935 ConfirmForwardSelectedItems(_inner);
1936 }
1937
clearSelected()1938 void RepliesWidget::clearSelected() {
1939 _inner->cancelSelection();
1940 }
1941
setupDragArea()1942 void RepliesWidget::setupDragArea() {
1943 const auto areas = DragArea::SetupDragAreaToContainer(
1944 this,
1945 [=](auto d) { return _history && !_composeControls->isRecording(); },
1946 nullptr,
1947 [=] { updateControlsGeometry(); });
1948
1949 const auto droppedCallback = [=](bool overrideSendImagesAsPhotos) {
1950 return [=](const QMimeData *data) {
1951 confirmSendingFiles(data, overrideSendImagesAsPhotos);
1952 Window::ActivateWindow(controller());
1953 };
1954 };
1955 areas.document->setDroppedCallback(droppedCallback(false));
1956 areas.photo->setDroppedCallback(droppedCallback(true));
1957 }
1958
1959 } // namespace HistoryView
1960