/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_inner_widget.h" #include "core/file_utilities.h" #include "core/crash_reports.h" #include "core/click_handler_types.h" #include "history/history.h" #include "history/admin_log/history_admin_log_item.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/history_view_message.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_context_menu.h" #include "history/view/history_view_quick_action.h" #include "history/view/history_view_emoji_interactions.h" #include "history/view/history_view_pinned_bar.h" #include "history/history_item_components.h" #include "history/history_item_text.h" #include "ui/chat/chat_style.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "ui/image/image.h" #include "ui/toasts/common_toasts.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_options.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" #include "ui/controls/who_reacted_context_action.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "ui/inactive_press.h" #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "window/window_peer_menu.h" #include "window/window_controller.h" #include "window/notifications_manager.h" #include "boxes/about_sponsored_box.h" #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" #include "boxes/sticker_set_box.h" #include "boxes/premium_preview_box.h" #include "boxes/translate_box.h" #include "chat_helpers/message_field.h" #include "chat_helpers/emoji_interactions.h" #include "history/history_widget.h" #include "history/view/history_view_translate_tracker.h" #include "base/platform/base_platform_info.h" #include "base/qt/qt_common_adapters.h" #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "base/call_delayed.h" #include "mainwindow.h" #include "layout/layout_selection.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_item_download_files.h" #include "core/application.h" #include "apiwrap.h" #include "api/api_attached_stickers.h" #include "api/api_toggling_media.h" #include "api/api_who_reacted.h" #include "api/api_views.h" #include "lang/lang_keys.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_message_reactions.h" #include "data/data_document.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" #include "data/data_poll.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_peer_values.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "data/stickers/data_stickers.h" #include "data/data_sponsored_messages.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "settings/settings_premium.h" #include "styles/style_chat.h" #include "styles/style_window.h" // st::windowMinWidth #include "styles/style_menu_icons.h" #include #include #include namespace { constexpr auto kScrollDateHideTimeout = 1000; constexpr auto kUnloadHeavyPartsPages = 2; constexpr auto kClearUserpicsAfter = 50; // Helper binary search for an item in a list that is not completely // above the given top of the visible area or below the given bottom of the visible area // is applied once for blocks list in a history and once for items list in the found block. template int BinarySearchBlocksOrItems(const T &list, int edge) { // static_cast to work around GCC bug #78693 auto start = 0, end = static_cast(list.size()); while (end - start > 1) { auto middle = (start + end) / 2; auto top = list[middle]->y(); auto chooseLeft = (TopToBottom ? (top <= edge) : (top < edge)); if (chooseLeft) { start = middle; } else { end = middle; } } return start; } void FillSponsoredMessagesMenu( not_null controller, FullMsgId itemId, not_null menu) { const auto &data = controller->session().data().sponsoredMessages(); const auto info = data.lookupDetails(itemId).info; const auto toastParent = Window::Show(controller).toastParent(); if (!info.empty()) { auto fillSubmenu = [&](not_null menu) { const auto allText = ranges::accumulate( info, TextWithEntities(), [](TextWithEntities a, TextWithEntities b) { return a.text.isEmpty() ? b : a.append('\n').append(b); }).text; const auto callback = [=] { QGuiApplication::clipboard()->setText(allText); Ui::ShowMultilineToast({ .parentOverride = toastParent, .text = { tr::lng_text_copied(tr::now) }, }); }; for (const auto &i : info) { auto item = base::make_unique_q( menu, st::defaultMenu, st::historySponsorInfoItem, st::historyHasCustomEmojiPosition, base::duplicate(i)); item->clicks( ) | rpl::start_with_next(callback, menu->lifetime()); menu->addAction(std::move(item)); if (i != info.back()) { menu->addSeparator(); } } }; using namespace Ui::Menu; CreateAddActionCallback(menu)(MenuCallback::Args{ .text = tr::lng_sponsored_info_menu(tr::now), .handler = nullptr, .icon = &st::menuIconChannel, .fillSubmenu = std::move(fillSubmenu), }); menu->addSeparator(&st::expandedMenuSeparator); } menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { Settings::ShowPremium(controller, "no_ads"); }, &st::menuIconCancel); } } // namespace // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html HistoryMainElementDelegateMixin::HistoryMainElementDelegateMixin() = default; HistoryMainElementDelegateMixin::~HistoryMainElementDelegateMixin() = default; class HistoryMainElementDelegate final : public HistoryView::ElementDelegate , public HistoryMainElementDelegateMixin { public: using Element = HistoryView::Element; HistoryView::Context elementContext() override { return HistoryView::Context::History; } bool elementUnderCursor( not_null view) override { return (Element::Moused() == view); } [[nodiscard]] float64 elementHighlightOpacity( not_null item) const override { return _widget ? _widget->elementHighlightOpacity(item) : 0.; } bool elementInSelectionMode() override { return _widget ? _widget->inSelectionMode() : false; } bool elementIntersectsRange( not_null view, int from, int till) override { return _widget ? _widget->elementIntersectsRange(view, from, till) : false; } void elementStartStickerLoop( not_null view) override { if (_widget) { _widget->elementStartStickerLoop(view); } } void elementShowPollResults( not_null poll, FullMsgId context) override { if (_widget) { _widget->elementShowPollResults(poll, context); } } void elementOpenPhoto( not_null photo, FullMsgId context) override { if (_widget) { _widget->elementOpenPhoto(photo, context); } } void elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView = false) override { if (_widget) { _widget->elementOpenDocument( document, context, showInMediaView); } } void elementCancelUpload(const FullMsgId &context) override { if (_widget) { _widget->elementCancelUpload(context); } } void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) override { if (_widget) { _widget->elementShowTooltip(text, hiddenCallback); } } bool elementAnimationsPaused() override { return _widget ? _widget->elementAnimationsPaused() : false; } bool elementHideReply(not_null view) override { return view->isTopicRootReply(); } bool elementShownUnread(not_null view) override { return view->data()->unread(view->data()->history()); } void elementSendBotCommand( const QString &command, const FullMsgId &context) override { if (_widget) { _widget->elementSendBotCommand(command, context); } } void elementHandleViaClick(not_null bot) override { if (_widget) { _widget->elementHandleViaClick(bot); } } bool elementIsChatWide() override { return _widget ? _widget->elementIsChatWide() : false; } not_null elementPathShiftGradient() override { Expects(_widget != nullptr); return _widget->elementPathShiftGradient(); } void elementReplyTo(const FullMsgId &to) override { if (_widget) { _widget->elementReplyTo(to); } } void elementStartInteraction(not_null view) override { if (_widget) { _widget->elementStartInteraction(view); } } void elementStartPremium( not_null view, Element *replacing) override { if (_widget) { _widget->elementStartPremium(view, replacing); } } void elementCancelPremium(not_null view) override { if (_widget) { _widget->elementCancelPremium(view); } } QString elementAuthorRank(not_null view) override { return {}; } not_null delegate() override { return this; } }; class HistoryInner::BotAbout final : public ClickHandlerHost { public: BotAbout( not_null history, not_null delegate); [[nodiscard]] not_null history() const; [[nodiscard]] HistoryView::Element *view() const; [[nodiscard]] HistoryItem *item() const; bool refresh(); int top = 0; int height = 0; private: const not_null _history; const not_null _delegate; AdminLog::OwnedItem _item; int _version = 0; }; HistoryInner::BotAbout::BotAbout( not_null history, not_null delegate) : _history(history) , _delegate(delegate) { } not_null HistoryInner::BotAbout::history() const { return _history; } HistoryView::Element *HistoryInner::BotAbout::view() const { return _item.get(); } HistoryItem *HistoryInner::BotAbout::item() const { if (const auto element = view()) { return element->data(); } return nullptr; } bool HistoryInner::BotAbout::refresh() { const auto bot = _history->peer->asUser(); const auto info = bot ? bot->botInfo.get() : nullptr; if (!info) { if (_item) { _item = {}; return true; } _version = 0; return false; } const auto version = info->descriptionVersion; if (_version == version) { return false; } _version = version; const auto flags = MessageFlag::FakeBotAbout | MessageFlag::FakeHistoryItem | MessageFlag::Local; const auto postAuthor = QString(); const auto date = TimeId(0); const auto replyTo = MsgId(0); const auto viaBotId = UserId(0); const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( info->description, Ui::ItemTextBotNoMonoOptions().flags); const auto make = [&](auto &&a, auto &&b, auto &&...other) { return _history->makeMessage( _history->nextNonHistoryEntryId(), flags, replyTo, viaBotId, date, bot->id, postAuthor, std::forward(a), std::forward(b), HistoryMessageMarkupData(), std::forward(other)...); }; const auto item = info->document ? make(info->document, textWithEntities) : info->photo ? make(info->photo, textWithEntities) : make(textWithEntities, MTP_messageMediaEmpty(), groupedId); _item = AdminLog::OwnedItem(_delegate, item); return true; } HistoryInner::HistoryInner( not_null historyWidget, not_null scroll, not_null controller, not_null history) : RpWidget(nullptr) , _widget(historyWidget) , _scroll(scroll) , _controller(controller) , _peer(history->peer) , _history(history) , _elementDelegate(_history->delegateMixin()->delegate()) , _emojiInteractions(std::make_unique( &controller->session(), [=](not_null view) { return itemTop(view); })) , _migrated(history->migrateFrom()) , _translateTracker(std::make_unique(history)) , _pathGradient( HistoryView::MakePathShiftGradient( controller->chatStyle(), [=] { update(); })) , _reactionsManager( std::make_unique( this, [=](QRect updated) { update(updated); }, controller->cachedReactionIconFactory().createMethod())) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) { _history->delegateMixin()->setCurrent(this); if (_migrated) { _migrated->delegateMixin()->setCurrent(this); _migrated->translateTo(_history->translatedTo()); } Window::ChatThemeValueFromPeer( controller, _peer ) | rpl::start_with_next([=](std::shared_ptr &&theme) { _theme = std::move(theme); controller->setChatStyleTheme(_theme); }, lifetime()); setAttribute(Qt::WA_AcceptTouchEvents); notifyIsBotChanged(); setMouseTracking(true); _controller->gifPauseLevelChanged( ) | rpl::start_with_next([=] { if (!elementAnimationsPaused()) { update(); } }, lifetime()); using PlayRequest = ChatHelpers::EmojiInteractionPlayRequest; _controller->emojiInteractions().playRequests( ) | rpl::filter([=](const PlayRequest &request) { return (request.item->history() == _history) && _controller->widget()->isActive(); }) | rpl::start_with_next([=](PlayRequest &&request) { if (const auto view = viewByItem(request.item)) { _emojiInteractions->play(std::move(request), view); } }, lifetime()); _emojiInteractions->updateRequests( ) | rpl::start_with_next([=](QRect rect) { update(rect); }, lifetime()); _emojiInteractions->playStarted( ) | rpl::start_with_next([=](QString &&emoji) { _controller->emojiInteractions().playStarted(_peer, std::move(emoji)); }, lifetime()); _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { _reactionsManager->updateButton({}); reactionChosen(reaction); }, lifetime()); _reactionsManager->premiumPromoChosen( ) | rpl::start_with_next([=](FullMsgId context) { _reactionsManager->updateButton({}); ShowPremiumPreviewBox( _controller, PremiumPreview::InfiniteReactions); }, lifetime()); session().data().itemRemoved( ) | rpl::start_with_next( [this](auto item) { itemRemoved(item); }, lifetime()); session().data().viewRemoved( ) | rpl::start_with_next( [this](auto view) { viewRemoved(view); }, lifetime()); rpl::merge( session().data().historyUnloaded(), session().data().historyCleared() ) | rpl::filter([this](not_null history) { return (_history == history); }) | rpl::start_with_next([this] { mouseActionCancel(); }, lifetime()); session().data().viewRepaintRequest( ) | rpl::start_with_next([this](not_null view) { repaintItem(view); }, lifetime()); session().data().viewLayoutChanged( ) | rpl::filter([=](not_null view) { return (view == viewByItem(view->data())) && view->isUnderCursor(); }) | rpl::start_with_next([=](not_null view) { mouseActionUpdate(); }, lifetime()); session().data().itemDataChanges( ) | rpl::start_with_next([=](not_null item) { if (const auto view = viewByItem(item)) { view->itemDataChanged(); } }, lifetime()); session().changes().historyUpdates( _history, (Data::HistoryUpdate::Flag::OutboxRead | Data::HistoryUpdate::Flag::TranslatedTo) ) | rpl::start_with_next([=] { update(); }, lifetime()); HistoryView::Reactions::SetupManagerList( _reactionsManager.get(), _reactionsItem.value()); Core::App().settings().cornerReactionValue( ) | rpl::start_with_next([=](bool value) { _useCornerReaction = value; if (!value) { _reactionsManager->updateButton({}); } }, lifetime()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { _isChatWide = wide; }, lifetime()); _selectScroll.scrolls( ) | rpl::start_with_next([=](int d) { _scroll->scrollToY(_scroll->scrollTop() + d); }, _scroll->lifetime()); setupSharingDisallowed(); } void HistoryInner::reactionChosen(const ChosenReaction &reaction) { const auto item = session().data().message(reaction.context); if (!item) { return; } else if (Window::ShowReactPremiumError( _controller, item, reaction.id)) { if (_menu) { _menu->hideMenu(); } return; } item->toggleReaction(reaction.id, HistoryItem::ReactionSource::Selector); if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = viewByItem(item)) { if (const auto top = itemTop(view); top >= 0) { const auto geometry = reaction.localGeometry.isEmpty() ? mapFromGlobal(reaction.globalGeometry) : reaction.localGeometry; view->animateReaction({ .id = reaction.id, .flyIcon = reaction.icon, .flyFrom = geometry.translated(0, -top), }); } } } Main::Session &HistoryInner::session() const { return _controller->session(); } void HistoryInner::setupSharingDisallowed() { Expects(_peer != nullptr); if (_peer->isUser()) { _sharingDisallowed = false; return; } const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); _sharingDisallowed = chat ? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards) : Data::PeerFlagValue( channel, ChannelDataFlag::NoForwards ) | rpl::type_erased(); auto rights = chat ? chat->adminRightsValue() : channel->adminRightsValue(); auto canDelete = std::move( rights ) | rpl::map([=] { return chat ? chat->canDeleteMessages() : channel->canDeleteMessages(); }); rpl::combine( _sharingDisallowed.value(), std::move(canDelete) ) | rpl::filter([=](bool disallowed, bool canDelete) { return hasSelectRestriction() && !getSelectedItems().empty(); }) | rpl::start_with_next([=] { _widget->clearSelected(); if (_mouseAction == MouseAction::PrepareSelect) { mouseActionCancel(); } }, lifetime()); } bool HistoryInner::hasSelectRestriction() const { if (!_sharingDisallowed.current()) { return false; } else if (const auto chat = _peer->asChat()) { return !chat->canDeleteMessages(); } else if (const auto channel = _peer->asChannel()) { return !channel->canDeleteMessages(); } return true; } void HistoryInner::messagesReceived( not_null peer, const QVector &messages) { if (_history->peer == peer) { _history->addOlderSlice(messages); if (!messages.isEmpty()) { _translateTracker->addBunchFromBlocks(); } } else if (_migrated && _migrated->peer == peer) { const auto newLoaded = _migrated && _migrated->isEmpty() && !_history->isEmpty(); _migrated->addOlderSlice(messages); if (newLoaded) { _migrated->addNewerSlice(QVector()); } } } void HistoryInner::messagesReceivedDown( not_null peer, const QVector &messages) { if (_history->peer == peer) { const auto oldLoaded = _migrated && _history->isEmpty() && !_migrated->isEmpty(); _history->addNewerSlice(messages); if (oldLoaded) { _history->addOlderSlice(QVector()); } } else if (_migrated && _migrated->peer == peer) { _migrated->addNewerSlice(messages); } } void HistoryInner::repaintItem(const HistoryItem *item) { if (const auto view = viewByItem(item)) { repaintItem(view); } } void HistoryInner::repaintItem(const Element *view) { if (_widget->skipItemRepaint()) { return; } const auto top = itemTop(view); if (top >= 0) { const auto range = view->verticalRepaintRange(); update(0, top + range.top, width(), range.height); const auto id = view->data()->fullId(); if (const auto area = _reactionsManager->lookupEffectArea(id)) { update(*area); } } } template void HistoryInner::enumerateItemsInHistory(History *history, int historytop, Method method) { // No displayed messages in this history. if (historytop < 0 || history->isEmpty()) { return; } if (_visibleAreaBottom <= historytop || historytop + history->height() <= _visibleAreaTop) { return; } auto searchEdge = TopToBottom ? _visibleAreaTop : _visibleAreaBottom; // Binary search for blockIndex of the first block that is not completely below the visible area. auto blockIndex = BinarySearchBlocksOrItems(history->blocks, searchEdge - historytop); // Binary search for itemIndex of the first item that is not completely below the visible area. auto block = history->blocks[blockIndex].get(); auto blocktop = historytop + block->y(); auto blockbottom = blocktop + block->height(); auto itemIndex = BinarySearchBlocksOrItems(block->messages, searchEdge - blocktop); while (true) { while (true) { auto view = block->messages[itemIndex].get(); auto itemtop = blocktop + view->y(); auto itembottom = itemtop + view->height(); // Binary search should've skipped all the items that are above / below the visible area. if (TopToBottom) { Assert(itembottom > _visibleAreaTop); } else { Assert(itemtop < _visibleAreaBottom); } if (!method(view, itemtop, itembottom)) { return; } // Skip all the items that are below / above the visible area. if (TopToBottom) { if (itembottom >= _visibleAreaBottom) { return; } } else { if (itemtop <= _visibleAreaTop) { return; } } if (TopToBottom) { if (++itemIndex >= block->messages.size()) { break; } } else { if (--itemIndex < 0) { break; } } } // Skip all the rest blocks that are below / above the visible area. if (TopToBottom) { if (blockbottom >= _visibleAreaBottom) { return; } } else { if (blocktop <= _visibleAreaTop) { return; } } if (TopToBottom) { if (++blockIndex >= history->blocks.size()) { return; } } else { if (--blockIndex < 0) { return; } } block = history->blocks[blockIndex].get(); blocktop = historytop + block->y(); blockbottom = blocktop + block->height(); if (TopToBottom) { itemIndex = 0; } else { itemIndex = block->messages.size() - 1; } } } bool HistoryInner::canHaveFromUserpics() const { if (_peer->isUser() && !_peer->isSelf() && !_peer->isRepliesChat() && !_isChatWide) { return false; } else if (_canHaveFromUserpicsSponsored) { return true; } else if (_peer->isChannel() && !_peer->isMegagroup()) { return false; } return true; } template void HistoryInner::enumerateUserpics(Method method) { if (!canHaveFromUserpics()) { return; } // Find and remember the top of an attached messages pack // -1 means we didn't find an attached to next message yet. int lowestAttachedItemTop = -1; auto userpicCallback = [&](not_null view, int itemtop, int itembottom) { // Skip all service messages. const auto item = view->data(); if (view->isHidden() || item->isService()) { return true; } if (lowestAttachedItemTop < 0 && view->isAttachedToNext()) { lowestAttachedItemTop = itemtop + view->marginTop(); } // Call method on a userpic for all messages that have it and for those who are not showing it // because of their attachment to the next message if they are bottom-most visible. if (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleAreaBottom)) { if (lowestAttachedItemTop < 0) { lowestAttachedItemTop = itemtop + view->marginTop(); } // Attach userpic to the bottom of the visible area with the same margin as the last message. auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom(); auto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleAreaBottom - userpicMinBottomSkip); // Do not let the userpic go above the attached messages pack top line. userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize); // Call the template callback function that was passed // and return if it finished everything it needed. if (!method(view, userpicBottom - st::msgPhotoSize)) { return false; } } // Forget the found top of the pack, search for the next one from scratch. if (!view->isAttachedToNext()) { lowestAttachedItemTop = -1; } return true; }; enumerateItems(userpicCallback); } template void HistoryInner::enumerateDates(Method method) { auto drawtop = historyDrawTop(); // Find and remember the bottom of an single-day messages pack // -1 means we didn't find a same-day with previous message yet. auto lowestInOneDayItemBottom = -1; auto dateCallback = [&](not_null view, int itemtop, int itembottom) { const auto item = view->data(); if (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) { lowestInOneDayItemBottom = itembottom - view->marginBottom(); } // Call method on a date for all messages that have it and for those who are not showing it // because they are in a one day together with the previous message if they are top-most visible. if (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) { // skip the date of history migrate item if it will be in migrated if (itemtop < drawtop && item->history() == _history) { if (itemtop > _visibleAreaTop) { // Previous item (from the _migrated history) is drawing date now. return false; } } if (lowestInOneDayItemBottom < 0) { lowestInOneDayItemBottom = itembottom - view->marginBottom(); } // Attach date to the top of the visible area with the same margin as it has in service message. int dateTop = qMax(itemtop, _visibleAreaTop) + st::msgServiceMargin.top(); // Do not let the date go below the single-day messages pack bottom line. int dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight); // Call the template callback function that was passed // and return if it finished everything it needed. if (!method(view, itemtop, dateTop)) { return false; } } // Forget the found bottom of the pack, search for the next one from scratch. if (!view->isInOneDayWithPrevious()) { lowestInOneDayItemBottom = -1; } return true; }; enumerateItems(dateCallback); } TextSelection HistoryInner::computeRenderSelection( not_null selected, not_null view) const { if (view->isHiddenByGroup()) { return TextSelection(); } const auto item = view->data(); const auto itemSelection = [&](not_null item) { auto i = selected->find(item); if (i != selected->end()) { return i->second; } return TextSelection(); }; const auto result = itemSelection(item); if (result != TextSelection() && result != FullSelection) { return result; } if (const auto group = session().data().groups().find(item)) { auto parts = TextSelection(); auto allFullSelected = true; const auto count = int(group->items.size()); for (auto i = 0; i != count; ++i) { const auto part = group->items[i]; const auto selection = itemSelection(part); if (part == item && selection != FullSelection && selection != TextSelection()) { return selection; } else if (selection == FullSelection) { parts = AddGroupItemSelection(parts, i); } else { allFullSelected = false; } } return allFullSelected ? FullSelection : parts; } return itemSelection(item); } TextSelection HistoryInner::itemRenderSelection( not_null view, int selfromy, int seltoy) const { const auto item = view->data(); const auto y = view->block()->y() + view->y(); if (y >= selfromy && y < seltoy) { if (_dragSelecting && !item->isService() && item->isRegular()) { return FullSelection; } } else if (!_selected.empty()) { return computeRenderSelection(&_selected, view); } return TextSelection(); } void HistoryInner::paintEmpty( Painter &p, not_null st, int width, int height) { if (!_emptyPainter) { _emptyPainter = std::make_unique( _history); } _emptyPainter->paint(p, st, width, height); } Ui::ChatPaintContext HistoryInner::preparePaintContext( const QRect &clip) const { const auto visibleAreaTopGlobal = mapToGlobal( QPoint(0, _visibleAreaTop)).y(); return _controller->preparePaintContext({ .theme = _theme.get(), .visibleAreaTop = _visibleAreaTop, .visibleAreaTopGlobal = visibleAreaTopGlobal, .visibleAreaWidth = width(), .clip = clip, }); } void HistoryInner::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e) || hasPendingResizedItems()) { return; } else if (_recountedAfterPendingResizedItems) { _recountedAfterPendingResizedItems = false; mouseActionUpdate(); } Painter p(this); auto clip = e->rect(); auto context = preparePaintContext(clip); _pathGradient->startFrame( 0, width(), std::min(st::msgMaxWidth / 2, width() / 2)); const auto historyDisplayedEmpty = _history->isDisplayedEmpty() && (!_migrated || _migrated->isDisplayedEmpty()); if (const auto view = _botAbout ? _botAbout->view() : nullptr) { if (clip.y() < _botAbout->top + _botAbout->height && clip.y() + clip.height() > _botAbout->top) { const auto top = _botAbout->top; context.translate(0, -top); context.selection = computeRenderSelection(&_selected, view); p.translate(0, top); view->draw(p, context); context.translate(0, top); p.translate(0, -top); } } else if (historyDisplayedEmpty) { paintEmpty(p, context.st, width(), height()); } else { _emptyPainter = nullptr; } const auto mtop = migratedTop(); const auto htop = historyTop(); if (historyDisplayedEmpty || (mtop < 0 && htop < 0)) { return; } _translateTracker->startBunch(); auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); const auto guard = gsl::finally([&] { if (_pinnedItem) { _translateTracker->add(_pinnedItem); } _translateTracker->finishBunch(); if (readTill && _widget->markingMessagesRead()) { session().data().histories().readInboxTill(readTill); } if (!readContents.empty() && _widget->markingContentsRead()) { session().api().markContentsRead(readContents); } _userpicsCache.clear(); }); const auto processPainted = [&]( not_null view, int top, int height) { _translateTracker->add(view); const auto item = view->data(); const auto isSponsored = item->isSponsored(); const auto isUnread = !item->out() && item->unread(_history) && (item->history() == _history); const auto withReaction = item->hasUnreadReaction(); const auto yShown = [&](int y) { return (_visibleAreaBottom >= y && _visibleAreaTop <= y); }; const auto markShown = isSponsored ? view->markSponsoredViewed(_visibleAreaBottom - top) : withReaction ? yShown(top + context.reactionInfo->position.y()) : isUnread ? yShown(top + height) : yShown(top + height / 2); if (markShown) { if (isSponsored) { session().data().sponsoredMessages().view(item->fullId()); } else if (isUnread) { readTill = item; } if (item->hasViews()) { session().api().views().scheduleIncrement(item); } if (withReaction) { readContents.insert(item); } else if (item->isUnreadMention() && !item->isUnreadMedia()) { readContents.insert(item); _widget->enqueueMessageHighlight(view); } } session().data().reactions().poll(item, context.now); if (item->hasExtendedMediaPreview()) { session().api().views().pollExtendedMedia(item); } _reactionsManager->recordCurrentReactionEffect( item->fullId(), QPoint(0, top)); }; adjustCurrent(clip.top()); const auto drawToY = clip.y() + clip.height(); auto selfromy = itemTop(_dragSelFrom); auto seltoy = itemTop(_dragSelTo); if (selfromy < 0 || seltoy < 0) { selfromy = seltoy = -1; } else { seltoy += _dragSelTo->height(); } const auto hdrawtop = historyDrawTop(); if (mtop >= 0) { auto iBlock = (_curHistory == _migrated ? _curBlock : (_migrated->blocks.size() - 1)); auto block = _migrated->blocks[iBlock].get(); auto iItem = (_curHistory == _migrated ? _curItem : (block->messages.size() - 1)); auto view = block->messages[iItem].get(); auto top = mtop + block->y() + view->y(); context.translate(0, -top); p.translate(0, top); if (context.clip.y() < view->height()) while (top < drawToY) { const auto height = view->height(); context.reactionInfo = _reactionsManager->currentReactionPaintInfo(); context.outbg = view->hasOutLayout(); context.selection = itemRenderSelection( view, selfromy - mtop, seltoy - mtop); view->draw(p, context); processPainted(view, top, height); top += height; context.translate(0, -height); p.translate(0, height); ++iItem; if (iItem == block->messages.size()) { iItem = 0; ++iBlock; if (iBlock == _migrated->blocks.size()) { break; } block = _migrated->blocks[iBlock].get(); } view = block->messages[iItem].get(); } context.translate(0, top); p.translate(0, -top); } if (htop >= 0) { auto iBlock = (_curHistory == _history ? _curBlock : 0); auto block = _history->blocks[iBlock].get(); auto iItem = (_curHistory == _history ? _curItem : 0); auto view = block->messages[iItem].get(); auto top = htop + block->y() + view->y(); context.clip = clip.intersected( QRect(0, hdrawtop, width(), clip.top() + clip.height())); context.translate(0, -top); p.translate(0, top); const auto &sendingAnimation = _controller->sendingAnimation(); while (top < drawToY) { const auto height = view->height(); if ((context.clip.y() < height) && (hdrawtop < top + height) && !sendingAnimation.hasAnimatedMessage(view->data())) { context.reactionInfo = _reactionsManager->currentReactionPaintInfo(); context.outbg = view->hasOutLayout(); context.selection = itemRenderSelection( view, selfromy - htop, seltoy - htop); view->draw(p, context); processPainted(view, top, height); } top += height; context.translate(0, -height); p.translate(0, height); ++iItem; if (iItem == block->messages.size()) { iItem = 0; ++iBlock; if (iBlock == _history->blocks.size()) { break; } block = _history->blocks[iBlock].get(); } view = block->messages[iItem].get(); } context.translate(0, top); p.translate(0, -top); } enumerateUserpics([&](not_null view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= clip.top() + clip.height()) { return false; } // paint the userpic if it intersects the painted rect if (userpicTop + st::msgPhotoSize > clip.top()) { const auto item = view->data(); if (const auto from = item->displayFrom()) { Dialogs::Ui::PaintUserpic( p, from, validateVideoUserpic(from), _userpics[from], st::historyPhotoLeft, userpicTop, width(), st::msgPhotoSize, context.paused); } else if (const auto info = item->hiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, st::historyPhotoLeft, userpicTop, width(), st::msgPhotoSize); } else { auto &userpic = _hiddenSenderUserpics[item->id]; const auto valid = info->paintCustomUserpic( p, userpic, st::historyPhotoLeft, userpicTop, width(), st::msgPhotoSize); if (!valid) { info->customUserpic.load(&session(), item->fullId()); } } } else { Unexpected("Corrupt forwarded information in message."); } } return true; }); const auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); //QDate lastDate; //if (!_history->isEmpty()) { // lastDate = _history->blocks.back()->messages.back()->data()->date.date(); //} //// if item top is before this value always show date as a floating date //int showFloatingBefore = height() - 2 * (_visibleAreaBottom - _visibleAreaTop) - dateHeight; auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); enumerateDates([&](not_null view, int itemtop, int dateTop) { // stop the enumeration if the date is above the painted rect if (dateTop + dateHeight <= clip.top()) { return false; } const auto displayDate = view->displayDate(); auto dateInPlace = displayDate; if (dateInPlace) { const auto correctDateTop = itemtop + st::msgServiceMargin.top(); dateInPlace = (dateTop < correctDateTop + dateHeight); } //bool noFloatingDate = (item->date.date() == lastDate && displayDate); //if (noFloatingDate) { // if (itemtop < showFloatingBefore) { // noFloatingDate = false; // } //} // paint the date if it intersects the painted rect if (dateTop < clip.top() + clip.height()) { auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity; if (opacity > 0.) { p.setOpacity(opacity); const auto dateY = false // noFloatingDate ? itemtop : (dateTop - st::msgServiceMargin.top()); if (const auto date = view->Get()) { date->paint(p, context.st, dateY, _contentWidth, _isChatWide); } else { HistoryView::ServiceMessagePainter::PaintDate( p, context.st, view->dateTime(), dateY, _contentWidth, _isChatWide); } } } return true; }); p.setOpacity(1.); _reactionsManager->paint(p, context); _emojiInteractions->paint(p); } bool HistoryInner::eventHook(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { QTouchEvent *ev = static_cast(e); if (ev->device()->type() == base::TouchDevice::TouchScreen) { touchEvent(ev); return true; } } return RpWidget::eventHook(e); } HistoryInner::VideoUserpic *HistoryInner::validateVideoUserpic( not_null peer) { if (!peer->isPremium() || peer->userpicPhotoUnknown() || !peer->userpicHasVideo()) { _videoUserpics.remove(peer); return nullptr; } const auto i = _videoUserpics.find(peer); if (i != end(_videoUserpics)) { return i->second.get(); } const auto repaint = [=] { if (hasPendingResizedItems()) { return; } enumerateUserpics([&](not_null view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= _visibleAreaBottom) { return false; } // repaint the userpic if it intersects the painted rect if (userpicTop + st::msgPhotoSize > _visibleAreaTop) { if (const auto from = view->data()->displayFrom()) { if (from == peer) { rtlupdate( st::historyPhotoLeft, userpicTop, st::msgPhotoSize, st::msgPhotoSize); } } } return true; }); }; return _videoUserpics.emplace(peer, std::make_unique( peer, repaint )).first->second.get(); } void HistoryInner::onTouchScrollTimer() { auto nowTime = crl::now(); if (_touchScrollState == Ui::TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) { _touchScrollState = Ui::TouchScrollState::Manual; touchResetSpeed(); } else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) { int32 elapsed = int32(nowTime - _touchTime); QPoint delta = _touchSpeed * elapsed / 1000; bool hasScrolled = _widget->touchScroll(delta); if (_touchSpeed.isNull() || !hasScrolled) { _touchScrollState = Ui::TouchScrollState::Manual; _touchScroll = false; _touchScrollTimer.cancel(); } else { _touchTime = nowTime; } touchDeaccelerate(elapsed); } } void HistoryInner::touchUpdateSpeed() { const auto nowTime = crl::now(); if (_touchPrevPosValid) { const int elapsed = nowTime - _touchSpeedTime; if (elapsed) { const QPoint newPixelDiff = (_touchPos - _touchPrevPos); const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed); // fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because // of a small horizontal offset when scrolling vertically const int newSpeedY = (qAbs(pixelsPerSecond.y()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0; const int newSpeedX = (qAbs(pixelsPerSecond.x()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0; if (_touchScrollState == Ui::TouchScrollState::Auto) { const int oldSpeedY = _touchSpeed.y(); const int oldSpeedX = _touchSpeed.x(); if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0) && (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) { _touchSpeed.setY(std::clamp( (oldSpeedY + (newSpeedY / 4)), -Ui::kMaxScrollAccelerated, +Ui::kMaxScrollAccelerated)); _touchSpeed.setX(std::clamp( (oldSpeedX + (newSpeedX / 4)), -Ui::kMaxScrollAccelerated, +Ui::kMaxScrollAccelerated)); } else { _touchSpeed = QPoint(); } } else { // we average the speed to avoid strange effects with the last delta if (!_touchSpeed.isNull()) { _touchSpeed.setX(std::clamp( (_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), -Ui::kMaxScrollFlick, +Ui::kMaxScrollFlick)); _touchSpeed.setY(std::clamp( (_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), -Ui::kMaxScrollFlick, +Ui::kMaxScrollFlick)); } else { _touchSpeed = QPoint(newSpeedX, newSpeedY); } } } } else { _touchPrevPosValid = true; } _touchSpeedTime = nowTime; _touchPrevPos = _touchPos; } void HistoryInner::touchResetSpeed() { _touchSpeed = QPoint(); _touchPrevPosValid = false; } void HistoryInner::touchDeaccelerate(int32 elapsed) { int32 x = _touchSpeed.x(); int32 y = _touchSpeed.y(); _touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed)); _touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed)); } void HistoryInner::touchEvent(QTouchEvent *e) { if (e->type() == QEvent::TouchCancel) { // cancel if (!_touchInProgress) return; _touchInProgress = false; _touchSelectTimer.cancel(); _touchScroll = _touchSelect = false; _touchScrollState = Ui::TouchScrollState::Manual; mouseActionCancel(); return; } if (!e->touchPoints().isEmpty()) { _touchPrevPos = _touchPos; _touchPos = e->touchPoints().cbegin()->screenPos().toPoint(); } switch (e->type()) { case QEvent::TouchBegin: { if (_menu) { e->accept(); return; // ignore mouse press, that was hiding context menu } if (_touchInProgress) return; if (e->touchPoints().isEmpty()) return; _touchInProgress = true; if (_touchScrollState == Ui::TouchScrollState::Auto) { _touchScrollState = Ui::TouchScrollState::Acceleration; _touchWaitingAcceleration = true; _touchAccelerationTime = crl::now(); touchUpdateSpeed(); _touchStart = _touchPos; } else { _touchScroll = false; _touchSelectTimer.callOnce(QApplication::startDragTime()); } _touchSelect = false; _touchStart = _touchPrevPos = _touchPos; } break; case QEvent::TouchUpdate: { if (!_touchInProgress) return; if (_touchSelect) { mouseActionUpdate(_touchPos); } else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { _touchSelectTimer.cancel(); _touchScroll = true; touchUpdateSpeed(); } if (_touchScroll) { if (_touchScrollState == Ui::TouchScrollState::Manual) { touchScrollUpdated(_touchPos); } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { touchUpdateSpeed(); _touchAccelerationTime = crl::now(); if (_touchSpeed.isNull()) { _touchScrollState = Ui::TouchScrollState::Manual; } } } } break; case QEvent::TouchEnd: { if (!_touchInProgress) return; _touchInProgress = false; auto weak = Ui::MakeWeak(this); if (_touchSelect) { mouseActionFinish(_touchPos, Qt::RightButton); QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos); showContextMenu(&contextMenu, true); _touchScroll = false; } else if (_touchScroll) { if (_touchScrollState == Ui::TouchScrollState::Manual) { _touchScrollState = Ui::TouchScrollState::Auto; _touchPrevPosValid = false; _touchScrollTimer.callEach(15); _touchTime = crl::now(); } else if (_touchScrollState == Ui::TouchScrollState::Auto) { _touchScrollState = Ui::TouchScrollState::Manual; _touchScroll = false; touchResetSpeed(); } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { _touchScrollState = Ui::TouchScrollState::Auto; _touchWaitingAcceleration = false; _touchPrevPosValid = false; } } else { // One short tap is like left mouse click. mouseActionStart(_touchPos, Qt::LeftButton); mouseActionFinish(_touchPos, Qt::LeftButton); } if (weak) { _touchSelectTimer.cancel(); _touchSelect = false; } } break; } } void HistoryInner::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); } if (reallyMoved) { lastGlobalPosition = e->globalPos(); if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) { keepScrollDateForNow(); } } mouseActionUpdate(e->globalPos()); } void HistoryInner::mouseActionUpdate(const QPoint &screenPos) { _mousePosition = screenPos; mouseActionUpdate(); } void HistoryInner::touchScrollUpdated(const QPoint &screenPos) { _touchPos = screenPos; _widget->touchScroll(_touchPos - _touchPrevPos); touchUpdateSpeed(); } QPoint HistoryInner::mapPointToItem(QPoint p, const Element *view) const { if (view) { const auto top = itemTop(view); p.setY(p.y() - top); return p; } return QPoint(); } QPoint HistoryInner::mapPointToItem( QPoint p, const HistoryItem *item) const { if (const auto view = viewByItem(item)) { return mapPointToItem(p, view); } return QPoint(); } void HistoryInner::mousePressEvent(QMouseEvent *e) { if (_menu) { e->accept(); return; // ignore mouse press, that was hiding context menu } mouseActionStart(e->globalPos(), e->button()); } void HistoryInner::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) { mouseActionUpdate(screenPos); if (button != Qt::LeftButton) return; ClickHandler::pressed(); if (Element::Pressed() != Element::Hovered()) { repaintItem(Element::Pressed()); Element::Pressed(Element::Hovered()); repaintItem(Element::Pressed()); } const auto mouseActionView = Element::Moused(); _mouseAction = MouseAction::None; _mouseActionItem = mouseActionView ? mouseActionView->data().get() : nullptr; _dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), mouseActionView); _pressWasInactive = Ui::WasInactivePress(_controller->widget()); if (_pressWasInactive) { Ui::MarkInactivePress(_controller->widget(), false); } if (ClickHandler::getPressed()) { _mouseAction = MouseAction::PrepareDrag; } else if (inSelectionMode()) { if (_dragStateItem && _selected.find(_dragStateItem) != _selected.cend() && Element::Hovered()) { _mouseAction = MouseAction::PrepareDrag; // start items drag } else if (!_pressWasInactive) { _mouseAction = MouseAction::PrepareSelect; // start items select } } if (_mouseAction == MouseAction::None && mouseActionView) { TextState dragState; if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) { StateRequest request; request.flags = Ui::Text::StateRequest::Flag::LookupSymbol; dragState = mouseActionView->textState(_dragStartPosition, request); if (dragState.cursor == CursorState::Text) { TextSelection selStatus = { dragState.symbol, dragState.symbol }; if (selStatus != FullSelection && (_selected.empty() || _selected.cbegin()->second != FullSelection)) { if (!_selected.empty()) { repaintItem(_selected.cbegin()->first); _selected.clear(); } _selected.emplace(_mouseActionItem, selStatus); _mouseTextSymbol = dragState.symbol; _mouseAction = MouseAction::Selecting; _mouseSelectType = TextSelectType::Paragraphs; mouseActionUpdate(_mousePosition); _trippleClickTimer.callOnce( QApplication::doubleClickInterval()); } } } else if (Element::Pressed()) { StateRequest request; request.flags = Ui::Text::StateRequest::Flag::LookupSymbol; dragState = mouseActionView->textState(_dragStartPosition, request); } if (_mouseSelectType != TextSelectType::Paragraphs) { if (Element::Pressed()) { _mouseTextSymbol = dragState.symbol; bool uponSelected = (dragState.cursor == CursorState::Text); if (uponSelected) { if (_selected.empty() || _selected.cbegin()->second == FullSelection || _selected.cbegin()->first != _mouseActionItem) { uponSelected = false; } else { uint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to; if (_mouseTextSymbol < selFrom || _mouseTextSymbol >= selTo) { uponSelected = false; } } } if (uponSelected) { _mouseAction = MouseAction::PrepareDrag; // start text drag } else if (!_pressWasInactive) { const auto media = Element::Pressed()->media(); if ((media && media->dragItem()) || _mouseCursorState == CursorState::Date) { _mouseAction = MouseAction::PrepareDrag; // start sticker drag or by-date drag } else { if (dragState.afterSymbol) ++_mouseTextSymbol; TextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol }; if (selStatus != FullSelection && (_selected.empty() || _selected.cbegin()->second != FullSelection)) { if (!_selected.empty()) { repaintItem(_selected.cbegin()->first); _selected.clear(); } _selected.emplace(_mouseActionItem, selStatus); _mouseAction = MouseAction::Selecting; repaintItem(_mouseActionItem); } else if (!hasSelectRestriction()) { _mouseAction = MouseAction::PrepareSelect; } } } } else if (!_pressWasInactive && !hasSelectRestriction()) { _mouseAction = MouseAction::PrepareSelect; // start items select } } } if (!_mouseActionItem) { _mouseAction = MouseAction::None; } else if (_mouseAction == MouseAction::None) { _mouseActionItem = nullptr; } } void HistoryInner::mouseActionCancel() { _mouseActionItem = nullptr; _dragStateItem = nullptr; _mouseAction = MouseAction::None; _dragStartPosition = QPoint(0, 0); _dragSelFrom = _dragSelTo = nullptr; _wasSelectedText = false; _selectScroll.cancel(); } std::unique_ptr HistoryInner::prepareDrag() { if (_mouseAction != MouseAction::Dragging) { return nullptr; } const auto pressedHandler = ClickHandler::getPressed(); if (dynamic_cast(pressedHandler.get()) || hasCopyRestriction()) { return nullptr; } const auto mouseActionView = viewByItem(_mouseActionItem); bool uponSelected = false; if (mouseActionView) { if (!_selected.empty() && _selected.cbegin()->second == FullSelection) { uponSelected = _dragStateItem && (_selected.find(_dragStateItem) != _selected.cend()); } else { StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto dragState = mouseActionView->textState(_dragStartPosition, request); uponSelected = (dragState.cursor == CursorState::Text); if (uponSelected) { if (_selected.empty() || _selected.cbegin()->second == FullSelection || _selected.cbegin()->first != _mouseActionItem) { uponSelected = false; } else { uint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to; if (dragState.symbol < selFrom || dragState.symbol >= selTo) { uponSelected = false; } } } } } auto urls = QList(); const auto selectedText = [&] { if (uponSelected) { return getSelectedText(); } else if (pressedHandler) { //if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') { // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o //} return TextForMimeData::Simple(pressedHandler->dragText()); } return TextForMimeData(); }(); if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) { updateDragSelection(nullptr, nullptr, false); _selectScroll.cancel(); if (!urls.isEmpty()) mimeData->setUrls(urls); if (uponSelected && !_controller->adaptive().isOneColumn()) { auto selectedState = getSelectionState(); if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { session().data().setMimeForwardIds(getSelectedItems()); mimeData->setData(u"application/x-td-forward"_q, "1"); } } return mimeData; } else if (_dragStateItem) { const auto view = viewByItem(_dragStateItem); if (!view) { return nullptr; } auto forwardIds = MessageIdsList(); if (_mouseCursorState == CursorState::Date) { forwardIds = session().data().itemOrItsGroup(_dragStateItem); } else if (view->isHiddenByGroup() && pressedHandler) { forwardIds = MessageIdsList(1, _dragStateItem->fullId()); } else if (const auto media = view->media()) { if (media->dragItemByHandler(pressedHandler) || media->dragItem()) { forwardIds = MessageIdsList(1, _dragStateItem->fullId()); } } if (forwardIds.empty()) { return nullptr; } session().data().setMimeForwardIds(std::move(forwardIds)); auto result = std::make_unique(); result->setData(u"application/x-td-forward"_q, "1"); if (const auto media = view->media()) { if (const auto document = media->getDocument()) { const auto filepath = document->filepath(true); if (!filepath.isEmpty()) { QList urls; urls.push_back(QUrl::fromLocalFile(filepath)); result->setUrls(urls); } } } return result; } return nullptr; } void HistoryInner::performDrag() { if (auto mimeData = prepareDrag()) { // This call enters event loop and can destroy any QObject. _reactionsManager->updateButton({}); _controller->widget()->launchDrag( std::move(mimeData), crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); })); } } void HistoryInner::itemRemoved(not_null item) { if (_history != item->history() && _migrated != item->history()) { return; } if (_pinnedItem == item) { _pinnedItem = nullptr; } if (_reactionsItem.current() == item) { _reactionsItem = nullptr; } _animatedStickersPlayed.remove(item); _reactionsManager->remove(item->fullId()); auto i = _selected.find(item); if (i != _selected.cend()) { _selected.erase(i); _widget->updateTopBarSelection(); } if (_mouseActionItem == item) { mouseActionCancel(); } if (_dragStateItem == item) { _dragStateItem = nullptr; } if ((_dragSelFrom && _dragSelFrom->data() == item) || (_dragSelTo && _dragSelTo->data() == item)) { _dragSelFrom = nullptr; _dragSelTo = nullptr; update(); } if (_scrollDateLastItem && _scrollDateLastItem->data() == item) { _scrollDateLastItem = nullptr; } mouseActionUpdate(); } void HistoryInner::viewRemoved(not_null view) { const auto refresh = [&](auto &saved) { if (saved == view) { const auto now = viewByItem(view->data()); saved = (now && now != view) ? now : nullptr; } }; refresh(_dragSelFrom); refresh(_dragSelTo); refresh(_scrollDateLastItem); } void HistoryInner::mouseActionFinish( const QPoint &screenPos, Qt::MouseButton button) { mouseActionUpdate(screenPos); auto activated = ClickHandler::unpressed(); if (_mouseAction == MouseAction::Dragging) { activated = nullptr; } else if (_mouseActionItem) { // if we are in selecting items mode perhaps we want to // toggle selection instead of activating the pressed link if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && inSelectionMode() && button != Qt::RightButton) { if (const auto view = viewByItem(_mouseActionItem)) { if (view->toggleSelectionByHandlerClick(activated)) { activated = nullptr; } } } } const auto pressedItemView = Element::Pressed(); if (pressedItemView) { repaintItem(pressedItemView); Element::Pressed(nullptr); } _wasSelectedText = false; if (activated) { const auto pressedItemId = pressedItemView ? pressedItemView->data()->fullId() : _mouseActionItem ? _mouseActionItem->fullId() : FullMsgId(); const auto weak = base::make_weak(_controller); mouseActionCancel(); ActivateClickHandler( window(), activated, prepareClickContext(button, pressedItemId)); return; } if ((_mouseAction == MouseAction::PrepareSelect) && !_pressWasInactive && inSelectionMode()) { changeSelectionAsGroup( &_selected, _mouseActionItem, SelectAction::Invert); repaintItem(_mouseActionItem); } else if ((_mouseAction == MouseAction::PrepareDrag) && !_pressWasInactive && _dragStateItem && (button != Qt::RightButton)) { auto i = _selected.find(_dragStateItem); if (i != _selected.cend() && i->second == FullSelection) { _selected.erase(i); repaintItem(_mouseActionItem); } else if ((i == _selected.cend()) && !_dragStateItem->isService() && _dragStateItem->isRegular() && inSelectionMode()) { if (_selected.size() < MaxSelectedItems) { _selected.emplace(_dragStateItem, FullSelection); repaintItem(_mouseActionItem); } } else { _selected.clear(); update(); } } else if (_mouseAction == MouseAction::Selecting) { if (_dragSelFrom && _dragSelTo) { applyDragSelection(); _dragSelFrom = _dragSelTo = nullptr; } else if (!_selected.empty() && !_pressWasInactive) { auto sel = _selected.cbegin()->second; if (sel != FullSelection && sel.from == sel.to) { _selected.clear(); _controller->widget()->setInnerFocus(); } } } _mouseAction = MouseAction::None; _mouseActionItem = nullptr; _mouseSelectType = TextSelectType::Letters; _selectScroll.cancel(); _widget->updateTopBarSelection(); if (QGuiApplication::clipboard()->supportsSelection() && !_selected.empty() && _selected.cbegin()->second != FullSelection && !hasCopyRestriction(_selected.cbegin()->first)) { const auto [item, selection] = *_selected.cbegin(); if (const auto view = viewByItem(item)) { TextUtilities::SetClipboardText( view->selectedText(selection), QClipboard::Selection); } } } void HistoryInner::mouseReleaseEvent(QMouseEvent *e) { mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); } } void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) { mouseActionStart(e->globalPos(), e->button()); const auto mouseActionView = viewByItem(_mouseActionItem); if (_mouseSelectType == TextSelectType::Letters && mouseActionView && ((_mouseAction == MouseAction::Selecting && !_selected.empty() && _selected.cbegin()->second != FullSelection) || (_mouseAction == MouseAction::None && (_selected.empty() || _selected.cbegin()->second != FullSelection)))) { StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto dragState = mouseActionView->textState(_dragStartPosition, request); if (dragState.cursor == CursorState::Text) { _mouseTextSymbol = dragState.symbol; _mouseSelectType = TextSelectType::Words; if (_mouseAction == MouseAction::None) { _mouseAction = MouseAction::Selecting; TextSelection selStatus = { dragState.symbol, dragState.symbol }; if (!_selected.empty()) { repaintItem(_selected.cbegin()->first); _selected.clear(); } _selected.emplace(_mouseActionItem, selStatus); } mouseMoveEvent(e); _trippleClickPoint = e->globalPos(); _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); } } if (!ClickHandler::getActive() && !ClickHandler::getPressed() && (_mouseCursorState == CursorState::None || _mouseCursorState == CursorState::Date) && !inSelectionMode() && !_emptyPainter) { if (const auto view = Element::Moused()) { mouseActionCancel(); switch (HistoryView::CurrentQuickAction()) { case HistoryView::DoubleClickQuickAction::Reply: { _widget->replyToMessage(view->data()); } break; case HistoryView::DoubleClickQuickAction::React: { toggleFavoriteReaction(view); } break; default: break; } } } } void HistoryInner::toggleFavoriteReaction(not_null view) const { const auto item = view->data(); const auto favorite = session().data().reactions().favoriteId(); if (!ranges::contains( Data::LookupPossibleReactions(item).recent, favorite, &Data::Reaction::id) || Window::ShowReactPremiumError(_controller, item, favorite)) { return; } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { view->animateReaction({ .id = favorite }); } } item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { showContextMenu(e); } void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (e->reason() == QContextMenuEvent::Mouse) { mouseActionUpdate(e->globalPos()); } const auto link = ClickHandler::getActive(); if (link && !link->property( kSendReactionEmojiProperty).value().empty() && _reactionsManager->showContextMenu( this, e, session().data().reactions().favoriteId())) { return; } auto selectedState = getSelectionState(); // -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items auto isUponSelected = 0; auto hasSelected = 0; if (!_selected.empty()) { isUponSelected = -1; if (_selected.cbegin()->second == FullSelection) { hasSelected = 2; if (_dragStateItem && _selected.find(_dragStateItem) != _selected.cend()) { isUponSelected = 2; } else { isUponSelected = -2; } } else if (Element::Moused() && Element::Moused() == Element::Hovered() && _selected.cbegin()->first == Element::Moused()->data()) { uint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to; hasSelected = (selTo > selFrom) ? 1 : 0; auto mousePos = mapPointToItem(mapFromGlobal(_mousePosition), Element::Moused()); StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto dragState = Element::Moused()->textState(mousePos, request); if (dragState.cursor == CursorState::Text && dragState.symbol >= selFrom && dragState.symbol < selTo) { isUponSelected = 1; } } } if (showFromTouch && hasSelected && isUponSelected < hasSelected) { isUponSelected = hasSelected; } const auto hasWhoReactedItem = _dragStateItem && Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::All); const auto clickedReaction = link ? link->property( kReactionsCountEmojiProperty).value() : Data::ReactionId(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() && _dragStateItem && Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::One)) { HistoryView::ShowWhoReactedMenu( &_menu, e->globalPos(), this, _dragStateItem, clickedReaction, _controller, _whoReactedMenuLifetime); e->accept(); return; } _menu = base::make_unique_q(this, st::popupMenuWithIcons); const auto session = &this->session(); const auto controller = _controller; const auto groupLeaderOrSelf = [](HistoryItem *item) -> HistoryItem* { if (!item) { return nullptr; } else if (const auto group = item->history()->owner().groups().find(item)) { return group->items.front(); } return item; }; const auto addItemActions = [&]( HistoryItem *item, HistoryItem *albumPartItem) { if (!item || !item->isRegular() || isUponSelected == 2 || isUponSelected == -2) { return; } const auto itemId = item->fullId(); const auto canReply = [&] { const auto peer = item->history()->peer; const auto topic = item->topic(); return topic ? Data::CanSendAnything(topic) : (Data::CanSendAnything(peer) && (!peer->isChannel() || peer->asChannel()->amIn())); }(); if (canReply) { _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { _widget->replyToMessage(itemId); }, &st::menuIconReply); } const auto repliesCount = item->repliesCount(); const auto withReplies = (repliesCount > 0); const auto topicRootId = item->history()->isForum() ? item->topicRootId() : 0; if (topicRootId || (withReplies && item->history()->peer->isMegagroup())) { const auto highlightId = topicRootId ? item->id : 0; const auto rootId = topicRootId ? topicRootId : repliesCount ? item->id : item->replyToTop(); const auto phrase = topicRootId ? tr::lng_replies_view_topic(tr::now) : (repliesCount > 0) ? tr::lng_replies_view( tr::now, lt_count, repliesCount) : tr::lng_replies_view_thread(tr::now); _menu->addAction(phrase, [=] { controller->showRepliesForMessage( _history, rootId, highlightId); }, &st::menuIconViewReplies); } const auto t = base::unixtime::now(); const auto editItem = (albumPartItem && albumPartItem->allowsEdit(t)) ? albumPartItem : item->allowsEdit(t) ? item : nullptr; if (editItem) { const auto editItemId = editItem->fullId(); _menu->addAction(tr::lng_context_edit_msg(tr::now), [=] { _widget->editMessage(editItemId); }, &st::menuIconEdit); } const auto pinItem = (item->canPin() && item->isPinned()) ? item : groupLeaderOrSelf(item); if (pinItem->canPin()) { const auto isPinned = pinItem->isPinned(); const auto pinItemId = pinItem->fullId(); const auto controller = _controller; _menu->addAction(isPinned ? tr::lng_context_unpin_msg(tr::now) : tr::lng_context_pin_msg(tr::now), crl::guard(controller, [=] { Window::ToggleMessagePinned(controller, pinItemId, !isPinned); }), isPinned ? &st::menuIconUnpin : &st::menuIconPin); } }; const auto addPhotoActions = [&](not_null photo, HistoryItem *item) { const auto media = photo->activeMediaView(); const auto itemId = item ? item->fullId() : FullMsgId(); if (!photo->isNull() && media && media->loaded() && !hasCopyMediaRestriction(item)) { _menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { savePhotoToFile(photo); }), &st::menuIconSaveImage); _menu->addAction(tr::lng_context_copy_image(tr::now), [=] { copyContextImage(photo, itemId); }, &st::menuIconCopy); } if (photo->hasAttachedStickers()) { _menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] { session->api().attachedStickers().requestAttachedStickerSets( controller, photo); }, &st::menuIconStickers); } }; const auto addDocumentActions = [&](not_null document, HistoryItem *item) { if (document->loading()) { _menu->addAction(tr::lng_context_cancel_download(tr::now), [=] { cancelContextDownload(document); }, &st::menuIconCancel); return; } const auto itemId = item ? item->fullId() : FullMsgId(); const auto lnkIsVideo = document->isVideoFile(); const auto lnkIsVoice = document->isVoiceMessage(); const auto lnkIsAudio = document->isAudioFile(); if (document->isGifv()) { const auto notAutoplayedGif = [&] { return item && !Data::AutoDownload::ShouldAutoPlay( session->settings().autoDownload(), item->history()->peer, document); }(); if (notAutoplayedGif) { _menu->addAction(tr::lng_context_open_gif(tr::now), [=] { openContextGif(itemId); }, &st::menuIconShowInChat); } if (!hasCopyMediaRestriction(item)) { _menu->addAction(tr::lng_context_save_gif(tr::now), [=] { saveContextGif(itemId); }, &st::menuIconGif); } } if (!document->filepath(true).isEmpty()) { _menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] { showContextInFolder(document); }, &st::menuIconShowInFolder); } if (item && !hasCopyMediaRestriction(item)) { HistoryView::AddSaveSoundForNotifications( _menu, item, document, controller); _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { saveDocumentToFile(itemId, document); }), &st::menuIconDownload); } if (document->hasAttachedStickers()) { _menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] { session->api().attachedStickers().requestAttachedStickerSets( controller, document); }, &st::menuIconStickers); } }; const auto addSelectMessageAction = [&]( not_null item, bool asGroup = true) { if (item->isRegular() && !item->isService() && !hasSelectRestriction()) { const auto itemId = item->fullId(); _menu->addAction(tr::lng_context_select_msg(tr::now), [=] { if (const auto item = session->data().message(itemId)) { if (const auto view = viewByItem(item)) { if (asGroup) { changeSelectionAsGroup(&_selected, item, SelectAction::Select); } else { changeSelection(&_selected, item, SelectAction::Select); } repaintItem(item); _widget->updateTopBarSelection(); } } }, &st::menuIconSelect); } }; const auto lnkPhoto = link ? reinterpret_cast( link->property(kPhotoLinkMediaProperty).toULongLong()) : nullptr; const auto lnkDocument = link ? reinterpret_cast( link->property(kDocumentLinkMediaProperty).toULongLong()) : nullptr; if (lnkPhoto || lnkDocument) { const auto item = _dragStateItem; const auto itemId = item ? item->fullId() : FullMsgId(); if (isUponSelected > 0) { if (!hasCopyRestrictionForSelected()) { _menu->addAction( (isUponSelected > 1 ? tr::lng_context_copy_selected_items(tr::now) : tr::lng_context_copy_selected(tr::now)), [=] { copySelectedText(); }, &st::menuIconCopy); } if (!Ui::SkipTranslate(getSelectedText().rich)) { _menu->addAction(tr::lng_context_translate_selected({}), [=] { _controller->show(Box( Ui::TranslateBox, item->history()->peer, MsgId(), getSelectedText().rich, hasCopyRestrictionForSelected())); }, &st::menuIconTranslate); } } addItemActions(item, item); if (!selectedState.count) { if (lnkPhoto) { addPhotoActions(lnkPhoto, item); } else { addDocumentActions(lnkDocument, item); } } if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) { _menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] { HistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History); }, &st::menuIconLink); } if (isUponSelected > 1) { if (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) { _menu->addAction(tr::lng_context_forward_selected(tr::now), [=] { _widget->forwardSelected(); }, &st::menuIconForward); } if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) { _menu->addAction(tr::lng_context_delete_selected(tr::now), [=] { _widget->confirmDeleteSelected(); }, &st::menuIconDelete); } if (selectedState.count > 0) { Menu::AddDownloadFilesAction(_menu, controller, _selected, this); } _menu->addAction(tr::lng_context_clear_selection(tr::now), [=] { _widget->clearSelected(); }, &st::menuIconSelect); } else if (item) { const auto itemId = item->fullId(); const auto blockSender = item->history()->peer->isRepliesChat(); if (isUponSelected != -2) { if (item->allowsForward()) { _menu->addAction(tr::lng_context_forward_msg(tr::now), [=] { forwardItem(itemId); }, &st::menuIconForward); } if (item->canDelete()) { _menu->addAction(Ui::DeleteMessageContextAction( _menu->menu(), [=] { deleteItem(itemId); }, item->ttlDestroyAt(), [=] { _menu = nullptr; })); } if (!blockSender && item->suggestReport()) { _menu->addAction(tr::lng_context_report_msg(tr::now), [=] { reportItem(itemId); }, &st::menuIconReport); } } addSelectMessageAction(item, false); if (isUponSelected != -2 && blockSender) { _menu->addAction(tr::lng_profile_block_user(tr::now), [=] { blockSenderItem(itemId); }, &st::menuIconBlock); } } } else { // maybe cursor on some text history item? const auto albumPartItem = _dragStateItem; const auto item = [&] { const auto result = Element::Hovered() ? Element::Hovered()->data().get() : Element::HoveredLink() ? Element::HoveredLink()->data().get() : nullptr; return result ? groupLeaderOrSelf(result) : nullptr; }(); const auto itemId = item ? item->fullId() : FullMsgId(); const auto canDelete = item && item->canDelete() && (item->isRegular() || !item->isService()); const auto canForward = item && item->allowsForward(); const auto canReport = item && item->suggestReport(); const auto canBlockSender = item && item->history()->peer->isRepliesChat(); const auto view = viewByItem(item); const auto actionText = link ? link->copyToClipboardContextItemText() : QString(); if (isUponSelected > 0) { if (!hasCopyRestrictionForSelected()) { _menu->addAction( ((isUponSelected > 1) ? tr::lng_context_copy_selected_items(tr::now) : tr::lng_context_copy_selected(tr::now)), [=] { copySelectedText(); }, &st::menuIconCopy); } if (!Ui::SkipTranslate(getSelectedText().rich)) { _menu->addAction(tr::lng_context_translate_selected({}), [=] { _controller->show(Box( Ui::TranslateBox, item->history()->peer, MsgId(), getSelectedText().rich, hasCopyRestrictionForSelected())); }, &st::menuIconTranslate); } addItemActions(item, item); } else { addItemActions(item, albumPartItem); if (item && !isUponSelected) { const auto media = (view ? view->media() : nullptr); const auto mediaHasTextForCopy = media && media->hasTextForCopy(); if (const auto document = media ? media->getDocument() : nullptr) { if (!view->isIsolatedEmoji() && document->sticker()) { if (document->sticker()->set) { _menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] { showStickerPackInfo(document); }, &st::menuIconStickers); const auto isFaved = session->data().stickers().isFaved(document); _menu->addAction(isFaved ? tr::lng_faved_stickers_remove(tr::now) : tr::lng_faved_stickers_add(tr::now), [=] { Api::ToggleFavedSticker(controller, document, itemId); }, isFaved ? &st::menuIconUnfave : &st::menuIconFave); } if (!hasCopyMediaRestriction(item)) { _menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { saveDocumentToFile(itemId, document); }), &st::menuIconDownload); } } } if (const auto media = item->media()) { if (const auto poll = media->poll()) { HistoryView::AddPollActions( _menu, poll, item, HistoryView::Context::History, _controller); } else if (const auto contact = media->sharedContact()) { const auto phone = contact->phoneNumber; _menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] { QGuiApplication::clipboard()->setText(phone); }, &st::menuIconCopy); } } if (item->isSponsored()) { const auto itemId = item->fullId(); FillSponsoredMessagesMenu(controller, itemId, _menu); } if (!item->isService() && view && actionText.isEmpty()) { if (!hasCopyRestriction(item) && (view->hasVisibleText() || mediaHasTextForCopy)) { _menu->addAction( tr::lng_context_copy_text(tr::now), [=] { copyContextText(itemId); }, &st::menuIconCopy); } if ((!item->translation() || !_history->translatedTo()) && (view->hasVisibleText() || mediaHasTextForCopy)) { const auto translate = mediaHasTextForCopy ? (HistoryView::TransribedText(item) .append('\n') .append(item->originalText())) : item->originalText(); if (!translate.text.isEmpty() && !Ui::SkipTranslate(translate)) { _menu->addAction(tr::lng_context_translate(tr::now), [=] { _controller->show(Box( Ui::TranslateBox, item->history()->peer, mediaHasTextForCopy ? MsgId() : item->fullId().msg, translate, hasCopyRestriction(item))); }, &st::menuIconTranslate); } } } } } if (!actionText.isEmpty() && !hasCopyRestriction(item)) { _menu->addAction( actionText, [text = link->copyToClipboardText()] { QGuiApplication::clipboard()->setText(text); }, &st::menuIconCopy); } else if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) { _menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] { HistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History); }, &st::menuIconLink); } if (item && item->isSponsored()) { _menu->addSeparator(&st::expandedMenuSeparator); auto item = base::make_unique_q( _menu, st::menuWithIcons, st::historyHasCustomEmoji, st::historySponsoredAboutMenuLabelPosition, TextWithEntities{ tr::lng_sponsored_title(tr::now) }, &st::menuIconInfo); item->clicks( ) | rpl::start_with_next([=] { controller->show(Box(Ui::AboutSponsoredBox)); }, item->lifetime()); _menu->addAction(std::move(item)); } if (isUponSelected > 1) { if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { _menu->addAction(tr::lng_context_forward_selected(tr::now), [=] { _widget->forwardSelected(); }, &st::menuIconForward); } if (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) { _menu->addAction(tr::lng_context_delete_selected(tr::now), [=] { _widget->confirmDeleteSelected(); }, &st::menuIconDelete); } if (selectedState.count > 0) { Menu::AddDownloadFilesAction(_menu, controller, _selected, this); } _menu->addAction(tr::lng_context_clear_selection(tr::now), [=] { _widget->clearSelected(); }, &st::menuIconSelect); } else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->isRegular())) { if (isUponSelected != -2) { if (canForward) { _menu->addAction(tr::lng_context_forward_msg(tr::now), [=] { forwardAsGroup(itemId); }, &st::menuIconForward); } if (canDelete) { const auto callback = [=] { deleteAsGroup(itemId); }; if (item->isUploading()) { _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( _menu->menu(), callback, item->ttlDestroyAt(), [=] { _menu = nullptr; })); } } if (!canBlockSender && canReport) { _menu->addAction(tr::lng_context_report_msg(tr::now), [=] { reportAsGroup(itemId); }, &st::menuIconReport); } } addSelectMessageAction(item); if (isUponSelected != -2 && canBlockSender) { _menu->addAction(tr::lng_profile_block_user(tr::now), [=] { blockSenderAsGroup(itemId); }, &st::menuIconBlock); } } else if (Element::Moused()) { addSelectMessageAction(Element::Moused()->data()); } } if (_dragStateItem) { HistoryView::AddEmojiPacksAction( _menu, _dragStateItem, HistoryView::EmojiPacksSource::Message, _controller); } if (hasWhoReactedItem) { HistoryView::AddWhoReactedAction( _menu, this, _dragStateItem, _controller); } if (_menu->empty()) { _menu = nullptr; return; } using namespace HistoryView::Reactions; const auto desiredPosition = e->globalPos(); const auto reactItem = Element::Hovered() ? Element::Hovered()->data().get() : nullptr; const auto attached = reactItem ? AttachSelectorToMenu( _menu.get(), controller, desiredPosition, reactItem, [=](ChosenReaction reaction) { reactionChosen(reaction); }, [=](FullMsgId context) { ShowPremiumPreviewBox( controller, PremiumPreview::InfiniteReactions); }, _controller->cachedReactionIconFactory().createMethod()) : AttachSelectorResult::Skipped; if (attached == AttachSelectorResult::Failed) { _menu = nullptr; return; } else if (attached == AttachSelectorResult::Attached) { _menu->popupPrepared(); } else { _menu->popup(desiredPosition); } e->accept(); } bool HistoryInner::hasCopyRestriction(HistoryItem *item) const { return !_peer->allowsForwarding() || (item && item->forbidsForward()); } bool HistoryInner::hasCopyMediaRestriction( not_null item) const { return hasCopyRestriction(item) || item->forbidsSaving(); } bool HistoryInner::showCopyRestriction(HistoryItem *item) { if (!hasCopyRestriction(item)) { return false; } Ui::ShowMultilineToast({ .parentOverride = Window::Show(_controller).toastParent(), .text = { _peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) : tr::lng_error_nocopy_group(tr::now) }, }); return true; } bool HistoryInner::showCopyMediaRestriction(not_null item) { if (!hasCopyMediaRestriction(item)) { return false; } Ui::ShowMultilineToast({ .parentOverride = Window::Show(_controller).toastParent(), .text = { _peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) : tr::lng_error_nocopy_group(tr::now) }, }); return true; } bool HistoryInner::hasCopyRestrictionForSelected() const { if (hasCopyRestriction()) { return true; } for (const auto &[item, selection] : _selected) { if (item && item->forbidsForward()) { return true; } } return false; } bool HistoryInner::showCopyRestrictionForSelected() { for (const auto &[item, selection] : _selected) { if (showCopyRestriction(item)) { return true; } } return false; } void HistoryInner::copySelectedText() { if (!showCopyRestrictionForSelected()) { TextUtilities::SetClipboardText(getSelectedText()); } } void HistoryInner::savePhotoToFile(not_null photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { return; } auto filter = u"JPEG Image (*.jpg);;"_q + FileDialog::AllFilesFilter(); FileDialog::GetWritePath( this, tr::lng_save_photo(tr::now), filter, filedialogDefaultName(u"photo"_q, u".jpg"_q), crl::guard(this, [=](const QString &result) { if (!result.isEmpty()) { media->saveToFile(result); } })); } void HistoryInner::copyContextImage( not_null photo, FullMsgId itemId) { const auto item = session().data().message(itemId); const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { return; } else if (!showCopyMediaRestriction(item)) { media->setToClipboard(); } } void HistoryInner::showStickerPackInfo(not_null document) { StickerSetBox::Show(_controller, document); } void HistoryInner::cancelContextDownload(not_null document) { document->cancel(); } void HistoryInner::showContextInFolder(not_null document) { const auto filepath = document->filepath(true); if (!filepath.isEmpty()) { File::ShowInFolder(filepath); } } void HistoryInner::saveDocumentToFile( FullMsgId contextId, not_null document) { DocumentSaveClickHandler::SaveAndTrack( contextId, document, DocumentSaveClickHandler::Mode::ToNewFile); } void HistoryInner::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { _controller->openDocument(document, itemId, MsgId(), true); } } } } void HistoryInner::saveContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (!hasCopyMediaRestriction(item)) { if (const auto media = item->media()) { if (const auto document = media->document()) { Api::ToggleSavedGif( _controller, document, item->fullId(), true); } } } } } void HistoryInner::copyContextText(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (!showCopyRestriction(item)) { if (const auto group = session().data().groups().find(item)) { TextUtilities::SetClipboardText(HistoryGroupText(group)); } else { TextUtilities::SetClipboardText(HistoryItemText(item)); } } } } void HistoryInner::resizeEvent(QResizeEvent *e) { mouseActionUpdate(); } TextForMimeData HistoryInner::getSelectedText() const { auto selected = _selected; if (_mouseAction == MouseAction::Selecting && _dragSelFrom && _dragSelTo) { applyDragSelection(&selected); } if (selected.empty()) { return TextForMimeData(); } if (selected.cbegin()->second != FullSelection) { const auto [item, selection] = *selected.cbegin(); if (const auto view = viewByItem(item)) { return view->selectedText(selection); } return TextForMimeData(); } struct Part { QString name; QString time; TextForMimeData unwrapped; }; auto groups = base::flat_set>(); auto fullSize = 0; auto texts = base::flat_map(); const auto wrapItem = [&]( not_null item, TextForMimeData &&unwrapped) { const auto i = texts.emplace(item->position(), Part{ .name = item->author()->name(), .time = QString(", [%1]\n").arg( QLocale().toString(ItemDateTime(item), QLocale::ShortFormat)), .unwrapped = std::move(unwrapped), }).first; fullSize += i->second.name.size() + i->second.time.size() + i->second.unwrapped.expanded.size(); }; const auto addItem = [&](not_null item) { wrapItem(item, HistoryItemText(item)); }; const auto addGroup = [&](not_null group) { Expects(!group->items.empty()); wrapItem(group->items.back(), HistoryGroupText(group)); }; for (const auto &[item, selection] : selected) { if (const auto group = session().data().groups().find(item)) { if (groups.contains(group)) { continue; } if (isSelectedGroup(&selected, group)) { groups.emplace(group); addGroup(group); } else { addItem(item); } } else { addItem(item); } } if (texts.size() == 1) { return texts.front().second.unwrapped; } auto result = TextForMimeData(); const auto sep = u"\n\n"_q; result.reserve(fullSize + (texts.size() - 1) * sep.size()); for (auto i = texts.begin(), e = texts.end(); i != e;) { result.append(i->second.name).append(i->second.time); result.append(std::move(i->second.unwrapped)); if (++i != e) { result.append(sep); } } return result; } void HistoryInner::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { _widget->escape(); } else if (e == QKeySequence::Copy && !_selected.empty()) { copySelectedText(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier) && !showCopyRestrictionForSelected()) { TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer); #endif // Q_OS_MAC } else if (e == QKeySequence::Delete) { auto selectedState = getSelectionState(); if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) { _widget->confirmDeleteSelected(); } } else { e->ignore(); } } void HistoryInner::checkActivation() { if (!_widget->markingMessagesRead()) { return; } adjustCurrent(_visibleAreaBottom); if (_history->loadedAtBottom() && _visibleAreaBottom >= height()) { // Clear possible message notifications. // Side-effect: Also clears all notifications from forum topics. Core::App().notifications().clearFromHistory(_history); } if (_curHistory != _history || _history->isEmpty()) { return; } auto block = _history->blocks[_curBlock].get(); auto view = block->messages[_curItem].get(); while (_curBlock > 0 || _curItem > 0) { const auto bottom = itemTop(view) + view->height(); if (_visibleAreaBottom >= bottom) { break; } if (_curItem > 0) { view = block->messages[--_curItem].get(); } else { while (_curBlock > 0) { block = _history->blocks[--_curBlock].get(); _curItem = block->messages.size(); if (_curItem > 0) { view = block->messages[--_curItem].get(); break; } } } } session().data().histories().readInboxTill(view->data()); } void HistoryInner::recountHistoryGeometry() { _contentWidth = _scroll->width(); if (_history->hasPendingResizedItems() || (_migrated && _migrated->hasPendingResizedItems())) { _recountedAfterPendingResizedItems = true; } const auto visibleHeight = _scroll->height(); auto oldHistoryPaddingTop = qMax( visibleHeight - historyHeight() - st::historyPaddingBottom, 0); if (_botAbout) { accumulate_max(oldHistoryPaddingTop, _botAbout->height); } _history->resizeToWidth(_contentWidth); if (_migrated) { _migrated->resizeToWidth(_contentWidth); } // With migrated history we perhaps do not need to display // the first _history message date (just skip it by height). _historySkipHeight = 0; if (_migrated && _migrated->loadedAtBottom() && _history->loadedAtTop()) { if (const auto first = _history->findFirstNonEmpty()) { if (const auto last = _migrated->findLastNonEmpty()) { if (first->dateTime().date() == last->dateTime().date()) { const auto dateHeight = first->displayedDateHeight(); if (_migrated->height() > dateHeight) { _historySkipHeight += dateHeight; } } } } } updateBotInfo(false); if (const auto view = _botAbout ? _botAbout->view() : nullptr) { _botAbout->height = view->resizeGetHeight(_contentWidth); _botAbout->top = qMin( _historyPaddingTop - _botAbout->height, qMax(0, (_scroll->height() - _botAbout->height) / 2)); } else if (_botAbout) { _botAbout->top = _botAbout->height = 0; } auto newHistoryPaddingTop = qMax( visibleHeight - historyHeight() - st::historyPaddingBottom, 0); if (_botAbout) { accumulate_max(newHistoryPaddingTop, _botAbout->height); } auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop); if (historyPaddingTopDelta != 0) { if (_history->scrollTopItem) { _history->scrollTopOffset += historyPaddingTopDelta; } else if (_migrated && _migrated->scrollTopItem) { _migrated->scrollTopOffset += historyPaddingTopDelta; } } } void HistoryInner::updateBotInfo(bool recount) { if (!_botAbout) { return; } else if (_botAbout->refresh() && recount && _contentWidth > 0) { const auto view = _botAbout->view(); const auto now = view ? view->resizeGetHeight(_contentWidth) : 0; if (_botAbout->height != now) { _botAbout->height = now; updateSize(); } } } bool HistoryInner::wasSelectedText() const { return _wasSelectedText; } void HistoryInner::visibleAreaUpdated(int top, int bottom) { auto scrolledUp = (top < _visibleAreaTop); _visibleAreaTop = top; _visibleAreaBottom = bottom; const auto visibleAreaHeight = bottom - top; // if history has pending resize events we should not update scrollTopItem if (hasPendingResizedItems()) { return; } if (bottom >= _historyPaddingTop + historyHeight() + st::historyPaddingBottom) { _history->forgetScrollState(); if (_migrated) { _migrated->forgetScrollState(); } } else { int htop = historyTop(), mtop = migratedTop(); if ((htop >= 0 && top >= htop) || mtop < 0) { _history->countScrollState(top - htop); if (_migrated) { _migrated->forgetScrollState(); } } else if (mtop >= 0 && top >= mtop) { _history->forgetScrollState(); _migrated->countScrollState(top - mtop); } else { _history->countScrollState(top - htop); if (_migrated) { _migrated->forgetScrollState(); } } } if (scrolledUp) { _scrollDateCheck.call(); } else { scrollDateHideByTimer(); } // Unload userpics. if (_userpics.size() > kClearUserpicsAfter) { _userpicsCache = std::move(_userpics); } // Unload lottie animations. const auto pages = kUnloadHeavyPartsPages; const auto from = _visibleAreaTop - pages * visibleAreaHeight; const auto till = _visibleAreaBottom + pages * visibleAreaHeight; session().data().unloadHeavyViewParts(_elementDelegate, from, till); if (_migratedElementDelegate) { session().data().unloadHeavyViewParts( _migratedElementDelegate, from, till); } checkActivation(); _emojiInteractions->visibleAreaUpdated( _visibleAreaTop, _visibleAreaBottom); } bool HistoryInner::displayScrollDate() const { return (_visibleAreaTop <= height() - 2 * (_visibleAreaBottom - _visibleAreaTop)); } void HistoryInner::scrollDateCheck() { auto newScrollDateItem = _history->scrollTopItem ? _history->scrollTopItem : (_migrated ? _migrated->scrollTopItem : nullptr); auto newScrollDateItemTop = _history->scrollTopItem ? _history->scrollTopOffset : (_migrated ? _migrated->scrollTopOffset : 0); //if (newScrollDateItem && !displayScrollDate()) { // if (!_history->isEmpty() && newScrollDateItem->date.date() == _history->blocks.back()->messages.back()->data()->date.date()) { // newScrollDateItem = nullptr; // } //} if (!newScrollDateItem) { _scrollDateLastItem = nullptr; _scrollDateLastItemTop = 0; scrollDateHide(); } else if (newScrollDateItem != _scrollDateLastItem || newScrollDateItemTop != _scrollDateLastItemTop) { // Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem). if (_scrollDateLastItem && !_scrollDateShown) { toggleScrollDateShown(); } _scrollDateLastItem = newScrollDateItem; _scrollDateLastItemTop = newScrollDateItemTop; _scrollDateHideTimer.callOnce(kScrollDateHideTimeout); } } void HistoryInner::scrollDateHideByTimer() { _scrollDateHideTimer.cancel(); if (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) { scrollDateHide(); } } void HistoryInner::scrollDateHide() { if (_scrollDateShown) { toggleScrollDateShown(); } } void HistoryInner::keepScrollDateForNow() { if (!_scrollDateShown && _scrollDateLastItem && _scrollDateOpacity.animating()) { toggleScrollDateShown(); } _scrollDateHideTimer.callOnce(kScrollDateHideTimeout); } void HistoryInner::toggleScrollDateShown() { _scrollDateShown = !_scrollDateShown; auto from = _scrollDateShown ? 0. : 1.; auto to = _scrollDateShown ? 1. : 0.; _scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration); } void HistoryInner::repaintScrollDateCallback() { int updateTop = _visibleAreaTop; int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); update(0, updateTop, width(), updateHeight); } void HistoryInner::setItemsRevealHeight(int revealHeight) { _revealHeight = revealHeight; } void HistoryInner::changeItemsRevealHeight(int revealHeight) { if (_revealHeight == revealHeight) { return; } _revealHeight = revealHeight; updateSize(); } void HistoryInner::updateSize() { const auto visibleHeight = _scroll->height(); const auto itemsHeight = historyHeight() - _revealHeight; auto newHistoryPaddingTop = qMax(visibleHeight - itemsHeight - st::historyPaddingBottom, 0); if (_botAbout) { accumulate_max(newHistoryPaddingTop, _botAbout->height); } if (_botAbout && _botAbout->height > 0) { _botAbout->top = qMin( newHistoryPaddingTop - _botAbout->height, qMax(0, (_scroll->height() - _botAbout->height) / 2)); } if (_historyPaddingTop != newHistoryPaddingTop) { _historyPaddingTop = newHistoryPaddingTop; } int newHeight = _historyPaddingTop + itemsHeight + st::historyPaddingBottom; if (width() != _scroll->width() || height() != newHeight) { resize(_scroll->width(), newHeight); if (!_revealHeight) { mouseActionUpdate(QCursor::pos()); } } else { update(); } } void HistoryInner::setShownPinned(HistoryItem *item) { _pinnedItem = item; } void HistoryInner::enterEventHook(QEnterEvent *e) { mouseActionUpdate(QCursor::pos()); return TWidget::enterEventHook(e); } void HistoryInner::leaveEventHook(QEvent *e) { _reactionsManager->updateButton({ .cursorLeft = true }); if (auto item = Element::Hovered()) { repaintItem(item); Element::Hovered(nullptr); } ClickHandler::clearActive(); Ui::Tooltip::Hide(); if (!ClickHandler::getPressed() && _cursor != style::cur_default) { _cursor = style::cur_default; setCursor(_cursor); } return TWidget::leaveEventHook(e); } HistoryInner::~HistoryInner() { _botAbout = nullptr; for (const auto &item : _animatedStickersPlayed) { if (const auto view = item->mainView()) { if (const auto media = view->media()) { media->stickerClearLoopPlayed(); } } } _history->delegateMixin()->setCurrent(nullptr); if (_migrated) { _migrated->delegateMixin()->setCurrent(nullptr); } delete _menu; _mouseAction = MouseAction::None; } bool HistoryInner::focusNextPrevChild(bool next) { if (_selected.empty()) { return TWidget::focusNextPrevChild(next); } else { clearSelected(); return true; } } void HistoryInner::adjustCurrent(int32 y) const { int32 htop = historyTop(), hdrawtop = historyDrawTop(), mtop = migratedTop(); _curHistory = nullptr; if (mtop >= 0) { adjustCurrent(y - mtop, _migrated); } if (htop >= 0 && hdrawtop >= 0 && (mtop < 0 || y >= hdrawtop)) { adjustCurrent(y - htop, _history); } } void HistoryInner::adjustCurrent(int32 y, History *history) const { Expects(!history->isEmpty()); _curHistory = history; if (_curBlock >= history->blocks.size()) { _curBlock = history->blocks.size() - 1; _curItem = 0; } while (history->blocks[_curBlock]->y() > y && _curBlock > 0) { --_curBlock; _curItem = 0; } while (history->blocks[_curBlock]->y() + history->blocks[_curBlock]->height() <= y && _curBlock + 1 < history->blocks.size()) { ++_curBlock; _curItem = 0; } auto block = history->blocks[_curBlock].get(); if (_curItem >= block->messages.size()) { _curItem = block->messages.size() - 1; } auto by = block->y(); while (block->messages[_curItem]->y() + by > y && _curItem > 0) { --_curItem; } while (block->messages[_curItem]->y() + block->messages[_curItem]->height() + by <= y && _curItem + 1 < block->messages.size()) { ++_curItem; } } auto HistoryInner::prevItem(Element *view) -> Element* { if (!view) { return nullptr; } else if (const auto result = view->previousDisplayedInBlocks()) { return result; } else if (view->history() == _history && _migrated && _history->loadedAtTop() && !_migrated->isEmpty() && _migrated->loadedAtBottom()) { return _migrated->findLastDisplayed(); } return nullptr; } auto HistoryInner::nextItem(Element *view) -> Element* { if (!view) { return nullptr; } else if (const auto result = view->nextDisplayedInBlocks()) { return result; } else if (view->history() == _migrated && _migrated->loadedAtBottom() && _history->loadedAtTop() && !_history->isEmpty()) { return _history->findFirstDisplayed(); } return nullptr; } bool HistoryInner::canCopySelected() const { return !_selected.empty(); } bool HistoryInner::canDeleteSelected() const { const auto selectedState = getSelectionState(); return (selectedState.count > 0) && (selectedState.count == selectedState.canDeleteCount); } bool HistoryInner::inSelectionMode() const { if (hasSelectedItems()) { return true; } else if (_mouseAction == MouseAction::Selecting && _dragSelFrom && _dragSelTo) { return true; } else if (_chooseForReportReason.has_value()) { return true; } return false; } bool HistoryInner::elementIntersectsRange( not_null view, int from, int till) const { const auto top = itemTop(view); if (top < 0) { return false; } const auto bottom = top + view->height(); return (top < till && bottom > from); } void HistoryInner::elementStartStickerLoop( not_null view) { _animatedStickersPlayed.emplace(view->data()); } float64 HistoryInner::elementHighlightOpacity( not_null item) const { return _widget->highlightOpacity(item); } void HistoryInner::elementShowPollResults( not_null poll, FullMsgId context) { _controller->showPollResults(poll, context); } void HistoryInner::elementOpenPhoto( not_null photo, FullMsgId context) { _controller->openPhoto(photo, context, MsgId(0)); } void HistoryInner::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { _controller->openDocument(document, context, MsgId(0), showInMediaView); } void HistoryInner::elementCancelUpload(const FullMsgId &context) { if (const auto item = session().data().message(context)) { _controller->cancelUploadLayer(item); } } void HistoryInner::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { _widget->showInfoTooltip(text, std::move(hiddenCallback)); } bool HistoryInner::elementAnimationsPaused() { return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any); } void HistoryInner::elementSendBotCommand( const QString &command, const FullMsgId &context) { _widget->sendBotCommand({ _history->peer, command, context }); } void HistoryInner::elementHandleViaClick(not_null bot) { _widget->insertBotCommand('@' + bot->username()); } bool HistoryInner::elementIsChatWide() { return _isChatWide; } not_null HistoryInner::elementPathShiftGradient() { return _pathGradient.get(); } void HistoryInner::elementReplyTo(const FullMsgId &to) { return _widget->replyToMessage(to); } void HistoryInner::elementStartInteraction(not_null view) { _controller->emojiInteractions().startOutgoing(view); } void HistoryInner::elementStartPremium( not_null view, Element *replacing) { const auto already = !_emojiInteractions->playPremiumEffect( view, replacing); _animatedStickersPlayed.emplace(view->data()); if (already) { _widget->showPremiumStickerTooltip(view); } } void HistoryInner::elementCancelPremium(not_null view) { _emojiInteractions->cancelPremiumEffect(view); } auto HistoryInner::getSelectionState() const -> HistoryView::TopBarWidget::SelectedState { auto result = HistoryView::TopBarWidget::SelectedState {}; for (auto &selected : _selected) { if (selected.second == FullSelection) { ++result.count; if (selected.first->canDelete()) { ++result.canDeleteCount; } if (selected.first->allowsForward()) { ++result.canForwardCount; } } else if (selected.second.from != selected.second.to) { result.textSelected = true; } } return result; } void HistoryInner::clearSelected(bool onlyTextSelection) { if (!_selected.empty() && (!onlyTextSelection || _selected.cbegin()->second != FullSelection)) { _selected.clear(); _widget->updateTopBarSelection(); _widget->update(); } } bool HistoryInner::hasSelectedItems() const { return !_selected.empty() && _selected.cbegin()->second == FullSelection; } MessageIdsList HistoryInner::getSelectedItems() const { using namespace ranges; if (!hasSelectedItems()) { return {}; } auto result = ranges::make_subrange( _selected.begin(), _selected.end() ) | views::filter([](const auto &selected) { const auto item = selected.first; return item && !item->isService() && item->isRegular(); }) | views::transform([](const auto &selected) { return selected.first->fullId(); }) | to_vector; result |= actions::sort(less{}, [](const FullMsgId &msgId) { return peerIsChannel(msgId.peer) ? msgId.msg : (msgId.msg - ServerMaxMsgId); }); return result; } void HistoryInner::onTouchSelect() { _touchSelect = true; mouseActionStart(_touchPos, Qt::LeftButton); } auto HistoryInner::reactionButtonParameters( not_null view, QPoint position, const HistoryView::TextState &reactionState) const -> HistoryView::Reactions::ButtonParameters { if (!_useCornerReaction) { return {}; } const auto top = itemTop(view); if (top < 0 || !view->data()->canReact() || _mouseAction == MouseAction::Dragging || _mouseAction == MouseAction::Selecting || inSelectionMode()) { return {}; } auto result = view->reactionButtonParameters( position, reactionState ).translated({ 0, itemTop(view) }); result.visibleTop = _visibleAreaTop; result.visibleBottom = _visibleAreaBottom; result.globalPointer = _mousePosition; return result; } void HistoryInner::mouseActionUpdate() { if (hasPendingResizedItems()) { return; } auto mousePos = mapFromGlobal(_mousePosition); auto point = _widget->clampMousePosition(mousePos); QPoint m; adjustCurrent(point.y()); const auto reactionState = _reactionsManager->buttonTextState(point); const auto reactionItem = session().data().message(reactionState.itemId); const auto reactionView = viewByItem(reactionItem); const auto view = reactionView ? reactionView : (_botAbout && _botAbout->view() && point.y() >= _botAbout->top && point.y() < _botAbout->top + _botAbout->view()->height()) ? _botAbout->view() : (_curHistory && !_curHistory->isEmpty()) ? _curHistory->blocks[_curBlock]->messages[_curItem].get() : nullptr; const auto item = view ? view->data().get() : nullptr; if (view) { const auto changed = (Element::Moused() != view); if (changed) { repaintItem(Element::Moused()); Element::Moused(view); repaintItem(Element::Moused()); } m = mapPointToItem(point, view); _reactionsManager->updateButton(reactionButtonParameters( view, m, reactionState)); if (changed) { _reactionsItem = item; } if (view->pointState(m) != PointState::Outside) { if (Element::Hovered() != view) { repaintItem(Element::Hovered()); Element::Hovered(view); repaintItem(Element::Hovered()); } } else if (Element::Hovered()) { repaintItem(Element::Hovered()); Element::Hovered(nullptr); } } else { if (Element::Moused()) { repaintItem(Element::Moused()); Element::Moused(nullptr); } _reactionsManager->updateButton({}); } if (_mouseActionItem && !viewByItem(_mouseActionItem)) { mouseActionCancel(); } TextState dragState; ClickHandlerHost *lnkhost = nullptr; auto selectingText = (item == _mouseActionItem) && (view == Element::Hovered()) && !_selected.empty() && (_selected.cbegin()->second != FullSelection); const auto overReaction = reactionView && reactionState.link; if (overReaction) { dragState = reactionState; lnkhost = reactionView; } else if (item) { if (item != _mouseActionItem || (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) { if (_mouseAction == MouseAction::PrepareDrag) { _mouseAction = MouseAction::Dragging; InvokeQueued(this, [=] { performDrag(); }); } else if (_mouseAction == MouseAction::PrepareSelect) { _mouseAction = MouseAction::Selecting; } } auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); enumerateDates([&](not_null view, int itemtop, int dateTop) { // stop enumeration if the date is above our point if (dateTop + dateHeight <= point.y()) { return false; } const auto displayDate = view->displayDate(); auto dateInPlace = displayDate; if (dateInPlace) { const auto correctDateTop = itemtop + st::msgServiceMargin.top(); dateInPlace = (dateTop < correctDateTop + dateHeight); } // stop enumeration if we've found a date under the cursor if (dateTop <= point.y()) { auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity; if (opacity > 0.) { const auto item = view->data(); auto dateWidth = 0; if (const auto date = view->Get()) { dateWidth = date->width; } else { dateWidth = st::msgServiceFont->width(langDayOfMonthFull(view->dateTime().date())); } dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right(); auto dateLeft = st::msgServiceMargin.left(); auto maxwidth = _contentWidth; if (_isChatWide) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); dateLeft += (widthForDate - dateWidth) / 2; if (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) { if (!_scrollDateLink) { _scrollDateLink = std::make_shared(item->history(), view->dateTime().date()); } else { static_cast(_scrollDateLink.get())->setDate(view->dateTime().date()); } dragState = TextState( nullptr, _scrollDateLink); _dragStateItem = session().data().message(dragState.itemId); lnkhost = view; } } return false; } return true; }); if (!dragState.link) { StateRequest request; if (_mouseAction == MouseAction::Selecting) { request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; } else { selectingText = false; } if (base::IsAltPressed()) { request.flags &= ~Ui::Text::StateRequest::Flag::LookupLink; } dragState = view->textState(m, request); _dragStateItem = session().data().message(dragState.itemId); lnkhost = view; if (!dragState.link && m.x() >= st::historyPhotoLeft && m.x() < st::historyPhotoLeft + st::msgPhotoSize) { if (!item->isService() && view->hasFromPhoto()) { enumerateUserpics([&](not_null view, int userpicTop) -> bool { // stop enumeration if the userpic is below our point if (userpicTop > point.y()) { return false; } // stop enumeration if we've found a userpic under the cursor if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) { dragState = TextState(nullptr, view->fromPhotoLink()); _dragStateItem = nullptr; lnkhost = view; return false; } return true; }); } } } } auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } if (dragState.link || dragState.cursor == CursorState::Date || dragState.cursor == CursorState::Forwarded || dragState.customTooltip) { Ui::Tooltip::Show(1000, this); } Qt::CursorShape cur = style::cur_default; if (_mouseAction == MouseAction::None) { _mouseCursorState = dragState.cursor; if (dragState.link) { cur = style::cur_pointer; } else if (_mouseCursorState == CursorState::Text && (_selected.empty() || _selected.cbegin()->second != FullSelection)) { cur = style::cur_text; } else if (_mouseCursorState == CursorState::Date) { //cur = style::cur_cross; } } else if (item) { if (_mouseAction == MouseAction::Selecting) { if (selectingText) { uint16 second = dragState.symbol; if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) { ++second; } auto selState = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) }; if (_mouseSelectType != TextSelectType::Letters) { if (const auto view = viewByItem(_mouseActionItem)) { selState = view->adjustSelection(selState, _mouseSelectType); } } if (_selected[_mouseActionItem] != selState) { _selected[_mouseActionItem] = selState; repaintItem(_mouseActionItem); } if (!_wasSelectedText && (selState == FullSelection || selState.from != selState.to)) { _wasSelectedText = true; setFocus(); } updateDragSelection(nullptr, nullptr, false); } else { auto selectingDown = (itemTop(_mouseActionItem) < itemTop(item)) || (_mouseActionItem == item && _dragStartPosition.y() < m.y()); auto dragSelFrom = viewByItem(_mouseActionItem); auto dragSelTo = view; // Maybe exclude dragSelFrom. if (dragSelFrom->pointState(_dragStartPosition) == PointState::Outside) { if (selectingDown) { if (_dragStartPosition.y() >= dragSelFrom->height() - dragSelFrom->marginBottom() || ((view == dragSelFrom) && (m.y() < _dragStartPosition.y() + QApplication::startDragDistance() || m.y() < dragSelFrom->marginTop()))) { dragSelFrom = (dragSelFrom != dragSelTo) ? nextItem(dragSelFrom) : nullptr; } } else { if (_dragStartPosition.y() < dragSelFrom->marginTop() || ((view == dragSelFrom) && (m.y() >= _dragStartPosition.y() - QApplication::startDragDistance() || m.y() >= dragSelFrom->height() - dragSelFrom->marginBottom()))) { dragSelFrom = (dragSelFrom != dragSelTo) ? prevItem(dragSelFrom) : nullptr; } } } if (_mouseActionItem != item) { // maybe exclude dragSelTo if (selectingDown) { if (m.y() < dragSelTo->marginTop()) { dragSelTo = (dragSelFrom != dragSelTo) ? prevItem(dragSelTo) : nullptr; } } else { if (m.y() >= dragSelTo->height() - dragSelTo->marginBottom()) { dragSelTo = (dragSelFrom != dragSelTo) ? nextItem(dragSelTo) : nullptr; } } } auto dragSelecting = false; auto dragFirstAffected = dragSelFrom; while (dragFirstAffected && (!dragFirstAffected->data()->isRegular() || dragFirstAffected->data()->isService())) { dragFirstAffected = (dragFirstAffected != dragSelTo) ? (selectingDown ? nextItem(dragFirstAffected) : prevItem(dragFirstAffected)) : nullptr; } if (dragFirstAffected) { auto i = _selected.find(dragFirstAffected->data()); dragSelecting = (i == _selected.cend() || i->second != FullSelection); } updateDragSelection(dragSelFrom, dragSelTo, dragSelecting); } } else if (_mouseAction == MouseAction::Dragging) { } if (ClickHandler::getPressed()) { cur = style::cur_pointer; } else if ((_mouseAction == MouseAction::Selecting) && !_selected.empty() && (_selected.cbegin()->second != FullSelection)) { if (!_dragSelFrom || !_dragSelTo) { cur = style::cur_text; } } } // Voice message seek support. if (const auto pressedItem = _dragStateItem) { if (const auto pressedView = viewByItem(pressedItem)) { if (pressedItem->history() == _history || pressedItem->history() == _migrated) { auto adjustedPoint = mapPointToItem(point, pressedView); pressedView->updatePressed(adjustedPoint); } } } if (_mouseAction == MouseAction::Selecting) { _selectScroll.checkDeltaScroll( mousePos, _scroll->scrollTop(), _scroll->scrollTop() + _scroll->height()); } else { updateDragSelection(nullptr, nullptr, false); _selectScroll.cancel(); } if (_mouseAction == MouseAction::None && (lnkChanged || cur != _cursor)) { setCursor(_cursor = cur); } } void HistoryInner::updateDragSelection(Element *dragSelFrom, Element *dragSelTo, bool dragSelecting) { if (_dragSelFrom == dragSelFrom && _dragSelTo == dragSelTo && _dragSelecting == dragSelecting) { return; } else if (dragSelFrom && hasSelectRestriction()) { updateDragSelection(nullptr, nullptr, false); return; } _dragSelFrom = dragSelFrom; _dragSelTo = dragSelTo; int32 fromy = itemTop(_dragSelFrom), toy = itemTop(_dragSelTo); if (fromy >= 0 && toy >= 0 && fromy > toy) { std::swap(_dragSelFrom, _dragSelTo); } _dragSelecting = dragSelecting; if (!_wasSelectedText && _dragSelFrom && _dragSelTo && _dragSelecting) { _wasSelectedText = true; setFocus(); } update(); } int HistoryInner::historyHeight() const { int result = 0; if (_history->isEmpty()) { result += _migrated ? _migrated->height() : 0; } else { result += _history->height() - _historySkipHeight + (_migrated ? _migrated->height() : 0); } return result; } int HistoryInner::historyScrollTop() const { auto htop = historyTop(); auto mtop = migratedTop(); if (htop >= 0 && _history->scrollTopItem) { return htop + _history->scrollTopItem->block()->y() + _history->scrollTopItem->y() + _history->scrollTopOffset; } if (mtop >= 0 && _migrated->scrollTopItem) { return mtop + _migrated->scrollTopItem->block()->y() + _migrated->scrollTopItem->y() + _migrated->scrollTopOffset; } return ScrollMax; } int HistoryInner::migratedTop() const { return (_migrated && !_migrated->isEmpty()) ? _historyPaddingTop : -1; } int HistoryInner::historyTop() const { int mig = migratedTop(); return !_history->isEmpty() ? (mig >= 0 ? (mig + _migrated->height() - _historySkipHeight) : _historyPaddingTop) : -1; } int HistoryInner::historyDrawTop() const { auto top = historyTop(); return (top >= 0) ? (top + _historySkipHeight) : -1; } void HistoryInner::setChooseReportReason(Ui::ReportReason reason) { _chooseForReportReason = reason; } void HistoryInner::clearChooseReportReason() { _chooseForReportReason = std::nullopt; } void HistoryInner::setCanHaveFromUserpicsSponsored(bool value) { _canHaveFromUserpicsSponsored = value; } auto HistoryInner::viewByItem(const HistoryItem *item) const -> Element* { return !item ? nullptr : (_botAbout && _botAbout->item() == item) ? _botAbout->view() : item->mainView(); } // -1 if should not be visible, -2 if bad history() int HistoryInner::itemTop(const HistoryItem *item) const { return item ? itemTop(viewByItem(item)) : -2; } int HistoryInner::itemTop(const Element *view) const { if (!view) { return -1; } else if (_botAbout && view == _botAbout->view()) { return _botAbout->top; } else if (view->data()->mainView() != view) { return -1; } const auto top = (view->history() == _history) ? historyTop() : (view->history() == _migrated ? migratedTop() : -2); return (top < 0) ? top : (top + view->y() + view->block()->y()); } auto HistoryInner::findViewForPinnedTracking(int top) const -> std::pair { const auto normalTop = historyTop(); const auto oldTop = migratedTop(); const auto fromHistory = [&](not_null history, int historyTop) -> std::pair { auto [view, offset] = history->findItemAndOffset(top - historyTop); while (view && !view->data()->isRegular()) { offset -= view->height(); view = view->nextInBlocks(); } return { view, offset }; }; if (normalTop >= 0 && (oldTop < 0 || top >= normalTop)) { return fromHistory(_history, normalTop); } else if (oldTop >= 0) { auto [view, offset] = fromHistory(_migrated, oldTop); if (!view && normalTop >= 0) { return fromHistory(_history, normalTop); } return { view, offset }; } return { nullptr, 0 }; } void HistoryInner::notifyIsBotChanged() { if (const auto user = _peer->asUser()) { if (const auto info = user->botInfo.get()) { if (!_botAbout) { _botAbout = std::make_unique( _history, _history->delegateMixin()->delegate()); } if (!info->inited) { session().api().requestFullPeer(_peer); } } } } void HistoryInner::notifyMigrateUpdated() { const auto migrated = _history->migrateFrom(); if (_migrated != migrated) { if (_migrated) { _migrated->delegateMixin()->setCurrent(nullptr); } _migrated = migrated; if (_migrated) { _migrated->delegateMixin()->setCurrent(this); _migrated->translateTo(_history->translatedTo()); } } } void HistoryInner::applyDragSelection() { if (!hasSelectRestriction()) { applyDragSelection(&_selected); } } bool HistoryInner::isSelected( not_null toItems, not_null item) const { const auto i = toItems->find(item); return (i != toItems->cend()) && (i->second == FullSelection); } bool HistoryInner::isSelectedGroup( not_null toItems, not_null group) const { for (const auto &other : group->items) { if (!isSelected(toItems, other)) { return false; } } return true; } bool HistoryInner::isSelectedAsGroup( not_null toItems, not_null item) const { if (const auto group = session().data().groups().find(item)) { return isSelectedGroup(toItems, group); } return isSelected(toItems, item); } bool HistoryInner::goodForSelection( not_null toItems, not_null item, int &totalCount) const { if (!item->isRegular() || item->isService()) { return false; } else if (toItems->find(item) == toItems->end()) { ++totalCount; } return true; } void HistoryInner::addToSelection( not_null toItems, not_null item) const { const auto i = toItems->find(item); if (i == toItems->cend()) { if (toItems->size() == 1 && toItems->begin()->second != FullSelection) { toItems->clear(); } toItems->emplace(item, FullSelection); } else if (i->second != FullSelection) { i->second = FullSelection; } } void HistoryInner::removeFromSelection( not_null toItems, not_null item) const { const auto i = toItems->find(item); if (i != toItems->cend()) { toItems->erase(i); } } void HistoryInner::changeSelection( not_null toItems, not_null item, SelectAction action) const { if (action == SelectAction::Invert) { action = isSelected(toItems, item) ? SelectAction::Deselect : SelectAction::Select; } auto total = int(toItems->size()); const auto add = (action == SelectAction::Select); if (add && goodForSelection(toItems, item, total) && total <= MaxSelectedItems) { addToSelection(toItems, item); } else { removeFromSelection(toItems, item); } } void HistoryInner::changeSelectionAsGroup( not_null toItems, not_null item, SelectAction action) const { const auto group = session().data().groups().find(item); if (!group) { return changeSelection(toItems, item, action); } if (action == SelectAction::Invert) { action = isSelectedAsGroup(toItems, item) ? SelectAction::Deselect : SelectAction::Select; } auto total = int(toItems->size()); const auto canSelect = [&] { for (const auto &other : group->items) { if (!goodForSelection(toItems, other, total)) { return false; } } return (total <= MaxSelectedItems); }(); if (action == SelectAction::Select && canSelect) { for (const auto &other : group->items) { addToSelection(toItems, other); } } else { for (const auto &other : group->items) { removeFromSelection(toItems, other); } } } void HistoryInner::forwardItem(FullMsgId itemId) { Window::ShowForwardMessagesBox(_controller, { 1, itemId }); } void HistoryInner::forwardAsGroup(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { Window::ShowForwardMessagesBox( _controller, session().data().itemOrItsGroup(item)); } } void HistoryInner::deleteItem(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { deleteItem(item); } } void HistoryInner::deleteItem(not_null item) { if (item->isUploading()) { _controller->cancelUploadLayer(item); return; } const auto suggestModerateActions = true; _controller->show(Box(item, suggestModerateActions)); } bool HistoryInner::hasPendingResizedItems() const { return _history->hasPendingResizedItems() || (_migrated && _migrated->hasPendingResizedItems()); } void HistoryInner::deleteAsGroup(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { const auto group = session().data().groups().find(item); if (!group) { return deleteItem(item); } _controller->show(Box( &session(), session().data().itemsToIds(group->items))); } } void HistoryInner::reportItem(FullMsgId itemId) { _controller->show(ReportItemsBox(_peer, { 1, itemId })); } void HistoryInner::reportAsGroup(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { const auto group = session().data().groups().find(item); _controller->show(ReportItemsBox( _peer, (group ? session().data().itemsToIds(group->items) : MessageIdsList{ 1, itemId }))); } } void HistoryInner::blockSenderItem(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { _controller->show(Box( Window::BlockSenderFromRepliesBox, _controller, itemId)); } } void HistoryInner::blockSenderAsGroup(FullMsgId itemId) { blockSenderItem(itemId); } void HistoryInner::addSelectionRange( not_null toItems, not_null history, int fromblock, int fromitem, int toblock, int toitem) const { if (fromblock >= 0 && fromitem >= 0 && toblock >= 0 && toitem >= 0) { for (; fromblock <= toblock; ++fromblock) { auto block = history->blocks[fromblock].get(); for (int cnt = (fromblock < toblock) ? block->messages.size() : (toitem + 1); fromitem < cnt; ++fromitem) { auto item = block->messages[fromitem]->data(); changeSelectionAsGroup(toItems, item, SelectAction::Select); } if (toItems->size() >= MaxSelectedItems) break; fromitem = 0; } } } void HistoryInner::applyDragSelection( not_null toItems) const { const auto selfromy = itemTop(_dragSelFrom); const auto seltoy = [&] { auto result = itemTop(_dragSelTo); return (result < 0) ? result : (result + _dragSelTo->height()); }(); if (selfromy < 0 || seltoy < 0) { return; } if (!toItems->empty() && toItems->cbegin()->second != FullSelection) { toItems->clear(); } const auto botAboutView = _botAbout ? _botAbout->view() : nullptr; if (_dragSelecting) { auto fromblock = (_dragSelFrom != botAboutView) ? _dragSelFrom->block()->indexInHistory() : _history->blocks.empty() ? -1 : 0; auto fromitem = (_dragSelFrom != botAboutView) ? _dragSelFrom->indexInBlock() : (_history->blocks.empty() || _history->blocks[0]->messages.empty()) ? -1 : 0; auto toblock = (_dragSelTo != botAboutView) ? _dragSelTo->block()->indexInHistory() : _history->blocks.empty() ? -1 : 0; auto toitem = (_dragSelTo != botAboutView) ? _dragSelTo->indexInBlock() : (_history->blocks.empty() || _history->blocks[0]->messages.empty()) ? -1 : 0; if (_migrated) { if (_dragSelFrom->history() == _migrated) { if (_dragSelTo->history() == _migrated) { addSelectionRange(toItems, _migrated, fromblock, fromitem, toblock, toitem); toblock = -1; toitem = -1; } else { addSelectionRange(toItems, _migrated, fromblock, fromitem, _migrated->blocks.size() - 1, _migrated->blocks.back()->messages.size() - 1); } fromblock = 0; fromitem = 0; } else if (_dragSelTo->history() == _migrated) { // wtf toblock = -1; toitem = -1; } } addSelectionRange(toItems, _history, fromblock, fromitem, toblock, toitem); } else { auto toRemove = std::vector>(); for (const auto &item : *toItems) { auto iy = itemTop(item.first); if (iy < -1) { toRemove.emplace_back(item.first); } else if (iy >= 0 && iy >= selfromy && iy < seltoy) { toRemove.emplace_back(item.first); } } for (const auto item : toRemove) { changeSelectionAsGroup(toItems, item, SelectAction::Deselect); } } } QString HistoryInner::tooltipText() const { if (_mouseCursorState == CursorState::Date && _mouseAction == MouseAction::None) { if (const auto view = Element::Hovered()) { return HistoryView::DateTooltipText(view); } } else if (_mouseCursorState == CursorState::Forwarded && _mouseAction == MouseAction::None) { if (const auto view = Element::Moused()) { if (const auto forwarded = view->data()->Get()) { return forwarded->text.toString(); } } } else if (const auto lnk = ClickHandler::getActive()) { return lnk->tooltip(); } else if (const auto view = Element::Moused()) { StateRequest request; const auto local = mapFromGlobal(_mousePosition); const auto point = _widget->clampMousePosition(local); request.flags |= Ui::Text::StateRequest::Flag::LookupCustomTooltip; const auto state = view->textState( mapPointToItem(point, view), request); return state.customTooltipText; } return QString(); } QPoint HistoryInner::tooltipPos() const { return _mousePosition; } bool HistoryInner::tooltipWindowActive() const { return Ui::AppInFocus() && Ui::InFocusChain(window()); } void HistoryInner::onParentGeometryChanged() { auto mousePos = QCursor::pos(); auto mouseOver = _widget->rect().contains(_widget->mapFromGlobal(mousePos)); auto needToUpdate = (_mouseAction != MouseAction::None || _touchScroll || mouseOver); if (needToUpdate) { mouseActionUpdate(mousePos); } } Fn HistoryInner::elementDelegateFactory( FullMsgId itemId) const { const auto weak = base::make_weak(_controller); return [=]() -> HistoryView::ElementDelegate* { if (const auto strong = weak.get()) { auto &data = strong->session().data(); if (const auto item = data.message(itemId)) { const auto history = item->history(); return history->delegateMixin()->delegate(); } } return nullptr; }; } ClickHandlerContext HistoryInner::prepareClickHandlerContext( FullMsgId itemId) const { return ClickHandlerContext{ .itemId = itemId, .elementDelegate = elementDelegateFactory(itemId), .sessionWindow = base::make_weak(_controller), }; } ClickContext HistoryInner::prepareClickContext( Qt::MouseButton button, FullMsgId itemId) const { return { button, QVariant::fromValue(prepareClickHandlerContext(itemId)), }; } auto HistoryInner::DelegateMixin() -> std::unique_ptr { return std::make_unique(); }