tdesktop/Telegram/SourceFiles/history/history_inner_widget.cpp

4471 lines
133 KiB
C++
Raw Normal View History

/*
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/click_handler_types.h"
#include "history/history.h"
#include "history/admin_log/history_admin_log_item.h"
2022-12-14 13:15:46 +01:00
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/controls/history_view_forward_panel.h"
2023-10-25 19:23:54 +02:00
#include "history/view/controls/history_view_draft_options.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"
2018-01-09 18:08:31 +01:00
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_cursor_state.h"
2018-12-19 12:20:04 +01:00
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_quick_action.h"
2021-09-15 16:49:06 +02:00
#include "history/view/history_view_emoji_interactions.h"
2023-01-27 16:25:47 +01:00
#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/effects/path_shift_gradient.h"
#include "ui/effects/message_sending_animation_controller.h"
#include "ui/effects/reaction_fly_animation.h"
2020-10-10 11:15:37 +02:00
#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"
2022-09-16 22:23:27 +02:00
#include "ui/painter.h"
2019-09-13 14:22:54 +02:00
#include "ui/ui_utility.h"
2019-09-16 13:14:06 +02:00
#include "ui/inactive_press.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "window/notifications_manager.h"
#include "info/info_memento.h"
#include "info/statistics/info_statistics_widget.h"
2021-10-07 02:06:39 +02:00
#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"
2017-06-29 12:27:09 +02:00
#include "history/history_widget.h"
#include "history/view/history_view_translate_tracker.h"
2019-09-17 18:13:12 +02:00
#include "base/platform/base_platform_info.h"
2022-01-24 17:33:30 +01:00
#include "base/qt/qt_common_adapters.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/unixtime.h"
2022-11-30 13:55:51 +01:00
#include "base/call_delayed.h"
#include "mainwindow.h"
2021-07-26 05:59:25 +02:00
#include "layout/layout_selection.h"
2019-07-24 13:45:24 +02:00
#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"
2021-12-24 14:51:53 +01:00
#include "api/api_who_reacted.h"
#include "api/api_views.h"
2017-04-13 10:27:10 +02:00
#include "lang/lang_keys.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
2019-09-11 11:26:13 +02:00
#include "data/data_message_reactions.h"
#include "data/data_document.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
2018-12-19 12:20:04 +01:00
#include "data/data_poll.h"
#include "data/data_photo.h"
2020-05-25 16:16:04 +02:00
#include "data/data_photo_media.h"
2021-11-05 18:37:01 +01:00
#include "data/data_peer_values.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_file_click_handler.h"
2019-09-16 13:14:06 +02:00
#include "data/data_file_origin.h"
2020-02-19 16:35:26 +01:00
#include "data/data_histories.h"
2020-06-12 16:09:04 +02:00
#include "data/data_changes.h"
#include "data/data_sponsored_messages.h"
#include "dialogs/ui/dialogs_video_userpic.h"
2022-06-09 09:00:29 +02:00
#include "settings/settings_premium.h"
2020-10-10 11:15:37 +02:00
#include "styles/style_chat.h"
2021-12-09 18:56:24 +01:00
#include "styles/style_menu_icons.h"
2023-10-06 09:15:26 +02:00
#include "payments/payments_checkout_process.h"
#include "payments/payments_form.h"
#include "base/random.h"
2019-09-04 09:19:15 +02:00
#include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
#include <QtCore/QMimeData>
namespace {
constexpr auto kScrollDateHideTimeout = 1000;
constexpr auto kUnloadHeavyPartsPages = 2;
2020-05-28 16:32:10 +02:00
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 <bool TopToBottom, typename T>
int BinarySearchBlocksOrItems(const T &list, int edge) {
// static_cast to work around GCC bug #78693
auto start = 0, end = static_cast<int>(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<Window::SessionController*> controller,
FullMsgId itemId,
not_null<Ui::PopupMenu*> menu) {
const auto &data = controller->session().data().sponsoredMessages();
const auto info = data.lookupDetails(itemId).info;
const auto show = controller->uiShow();
if (!info.empty()) {
auto fillSubmenu = [&](not_null<Ui::PopupMenu*> 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);
show->showToast(tr::lng_text_copied(tr::now));
};
for (const auto &i : info) {
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
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<const Element*> view) override {
return (Element::Moused() == view);
}
bool elementInSelectionMode() override {
return _widget ? _widget->inSelectionMode() : false;
}
bool elementIntersectsRange(
not_null<const Element*> view,
int from,
int till) override {
return _widget
? _widget->elementIntersectsRange(view, from, till)
: false;
}
void elementStartStickerLoop(
not_null<const Element*> view) override {
if (_widget) {
_widget->elementStartStickerLoop(view);
}
}
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) override {
if (_widget) {
_widget->elementShowPollResults(poll, context);
}
}
void elementOpenPhoto(
not_null<PhotoData*> photo,
FullMsgId context) override {
if (_widget) {
_widget->elementOpenPhoto(photo, context);
}
}
void elementOpenDocument(
not_null<DocumentData*> 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<void()> hiddenCallback) override {
if (_widget) {
_widget->elementShowTooltip(text, hiddenCallback);
}
}
bool elementAnimationsPaused() override {
return _widget ? _widget->elementAnimationsPaused() : false;
}
bool elementHideReply(not_null<const Element*> view) override {
return view->isTopicRootReply();
}
bool elementShownUnread(not_null<const Element*> 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<UserData*> bot) override {
if (_widget) {
_widget->elementHandleViaClick(bot);
}
}
bool elementIsChatWide() override {
return _widget ? _widget->elementIsChatWide() : false;
}
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {
Expects(_widget != nullptr);
return _widget->elementPathShiftGradient();
}
2023-10-10 08:49:22 +02:00
void elementReplyTo(const FullReplyTo &to) override {
if (_widget) {
_widget->elementReplyTo(to);
}
}
void elementStartInteraction(not_null<const Element*> view) override {
if (_widget) {
_widget->elementStartInteraction(view);
}
}
void elementStartPremium(
not_null<const Element*> view,
Element *replacing) override {
if (_widget) {
_widget->elementStartPremium(view, replacing);
}
}
void elementCancelPremium(not_null<const Element*> view) override {
2022-04-22 18:23:47 +02:00
if (_widget) {
_widget->elementCancelPremium(view);
2022-04-22 18:23:47 +02:00
}
}
2022-10-28 10:54:06 +02:00
QString elementAuthorRank(not_null<const Element*> view) override {
return {};
}
not_null<HistoryView::ElementDelegate*> delegate() override {
return this;
}
};
2019-05-14 11:50:44 +02:00
class HistoryInner::BotAbout final : public ClickHandlerHost {
public:
BotAbout(
not_null<History*> history,
not_null<HistoryView::ElementDelegate*> delegate);
[[nodiscard]] not_null<History*> history() const;
[[nodiscard]] HistoryView::Element *view() const;
[[nodiscard]] HistoryItem *item() const;
bool refresh();
int top = 0;
int height = 0;
private:
const not_null<History*> _history;
const not_null<HistoryView::ElementDelegate*> _delegate;
AdminLog::OwnedItem _item;
int _version = 0;
};
HistoryInner::BotAbout::BotAbout(
not_null<History*> history,
not_null<HistoryView::ElementDelegate*> delegate)
: _history(history)
, _delegate(delegate) {
}
not_null<History*> 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 = FullReplyTo();
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<decltype(a)>(a),
std::forward<decltype(b)>(b),
HistoryMessageMarkupData(),
std::forward<decltype(other)>(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*> historyWidget,
2020-04-30 11:16:42 +02:00
not_null<Ui::ScrollArea*> scroll,
not_null<Window::SessionController*> controller,
not_null<History*> history)
: RpWidget(nullptr)
2020-04-30 11:16:42 +02:00
, _widget(historyWidget)
, _scroll(scroll)
, _controller(controller)
, _peer(history->peer)
, _history(history)
, _elementDelegate(_history->delegateMixin()->delegate())
2021-09-15 16:49:06 +02:00
, _emojiInteractions(std::make_unique<HistoryView::EmojiInteractions>(
2022-04-26 15:48:38 +02:00
&controller->session(),
[=](not_null<const Element*> view) { return itemTop(view); }))
, _migrated(history->migrateFrom())
, _translateTracker(std::make_unique<HistoryView::TranslateTracker>(history))
, _pathGradient(
HistoryView::MakePathShiftGradient(
controller->chatStyle(),
[=] { update(); }))
, _reactionsManager(
std::make_unique<HistoryView::Reactions::Manager>(
2021-12-27 23:21:05 +01:00
this,
[=](QRect updated) { update(updated); },
controller->cachedReactionIconFactory().createMethod()))
2021-09-29 02:00:11 +02:00
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); })
, _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) {
_history->delegateMixin()->setCurrent(this);
if (_migrated) {
_migrated->delegateMixin()->setCurrent(this);
_migrated->translateTo(_history->translatedTo());
}
2019-05-14 11:50:44 +02:00
2023-10-06 09:15:26 +02:00
#if 0
if (const auto channel = _history->peer->asBroadcast()) {
if (channel->amCreator()) {
const auto weak = base::make_weak(_controller);
channel->session().api().request(MTPpayments_GetPremiumGiftCodeOptions(
MTP_flags(MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer),
channel->input
)).done(crl::guard(weak, [=](const MTPVector<MTPPremiumGiftCodeOption> &result) {
if (result.v.isEmpty()) {
return;
}
const auto &data = result.v.front().data();
const auto randomId = base::RandomValue<uint64>();
Payments::CheckoutProcess::Start(
Payments::InvoicePremiumGiftCode{
.purpose = Payments::InvoicePremiumGiftCodeGiveaway{
.boostPeer = channel,
//.additionalChannels = ,
.untilDate = (base::unixtime::now() + 300),
.onlyNewSubscribers = true,
},
.randomId = randomId,
.currency = qs(data.vcurrency()),
.amount = data.vamount().v,
.storeProduct = qs(data.vstore_product().value_or_empty()),
.storeQuantity = data.vstore_quantity().value_or_empty(),
.users = data.vusers().v,
.months = data.vmonths().v,
},
crl::guard(weak, [=](auto) { weak->window().activate(); }));
})).fail(crl::guard(weak, [=](const MTP::Error &error) {
weak.get()->showToast(error.type());
})).send();
}
}
#endif
Window::ChatThemeValueFromPeer(
controller,
_peer
) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&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());
2021-09-15 16:49:06 +02:00
using PlayRequest = ChatHelpers::EmojiInteractionPlayRequest;
_controller->emojiInteractions().playRequests(
) | rpl::filter([=](const PlayRequest &request) {
return (request.item->history() == _history)
&& _controller->widget()->isActive();
2021-09-15 17:30:29 +02:00
}) | rpl::start_with_next([=](PlayRequest &&request) {
2023-03-08 12:32:10 +01:00
if (const auto view = viewByItem(request.item)) {
2021-09-15 17:30:29 +02:00
_emojiInteractions->play(std::move(request), view);
2021-09-15 16:49:06 +02:00
}
}, lifetime());
_emojiInteractions->updateRequests(
) | rpl::start_with_next([=](QRect rect) {
2022-04-26 15:48:38 +02:00
update(rect);
2021-09-15 16:49:06 +02:00
}, lifetime());
2021-09-16 18:30:41 +02:00
_emojiInteractions->playStarted(
) | rpl::start_with_next([=](QString &&emoji) {
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime());
2021-09-15 16:49:06 +02:00
_reactionsManager->chosen(
2021-12-13 15:03:47 +01:00
) | 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);
2021-12-13 15:03:47 +01:00
}, lifetime());
session().data().peerDecorationsUpdated(
) | rpl::start_with_next([=] {
update();
}, lifetime());
2019-07-24 13:13:51 +02:00
session().data().itemRemoved(
) | rpl::start_with_next(
[this](auto item) { itemRemoved(item); },
lifetime());
2019-07-24 13:13:51 +02:00
session().data().viewRemoved(
) | rpl::start_with_next(
[this](auto view) { viewRemoved(view); },
lifetime());
rpl::merge(
2019-07-24 13:13:51 +02:00
session().data().historyUnloaded(),
session().data().historyCleared()
) | rpl::filter([this](not_null<const History*> history) {
return (_history == history);
}) | rpl::start_with_next([this] {
mouseActionCancel();
}, lifetime());
2019-07-24 13:13:51 +02:00
session().data().viewRepaintRequest(
) | rpl::start_with_next([this](not_null<const Element*> view) {
repaintItem(view);
}, lifetime());
2019-07-24 13:13:51 +02:00
session().data().viewLayoutChanged(
2023-03-08 12:32:10 +01:00
) | rpl::filter([=](not_null<const Element*> view) {
return (view == viewByItem(view->data())) && view->isUnderCursor();
}) | rpl::start_with_next([=](not_null<const Element*> view) {
mouseActionUpdate();
}, lifetime());
session().data().itemDataChanges(
2023-03-08 12:32:10 +01:00
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
if (const auto view = viewByItem(item)) {
view->itemDataChanged();
}
}, lifetime());
2020-06-12 16:09:04 +02:00
session().changes().historyUpdates(
_history,
(Data::HistoryUpdate::Flag::OutboxRead
| Data::HistoryUpdate::Flag::TranslatedTo)
2020-06-12 16:09:04 +02:00
) | rpl::start_with_next([=] {
update();
}, lifetime());
2022-01-10 15:22:43 +01:00
HistoryView::Reactions::SetupManagerList(
_reactionsManager.get(),
_reactionsItem.value());
2021-12-13 15:03:47 +01:00
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());
2021-11-05 18:37:01 +01:00
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;
2023-03-08 12:32:10 +01:00
} 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),
});
}
}
}
2019-07-24 13:45:24 +02:00
Main::Session &HistoryInner::session() const {
2019-07-24 13:13:51 +02:00
return _controller->session();
}
2021-11-05 18:37:01 +01:00
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();
2021-11-05 18:37:01 +01:00
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<PeerData*> peer,
const QVector<MTPMessage> &messages) {
2019-01-18 09:11:15 +01:00
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<MTPMessage>());
}
}
}
void HistoryInner::messagesReceivedDown(
not_null<PeerData*> peer,
const QVector<MTPMessage> &messages) {
2019-01-18 09:11:15 +01:00
if (_history->peer == peer) {
const auto oldLoaded = _migrated
&& _history->isEmpty()
&& !_migrated->isEmpty();
_history->addNewerSlice(messages);
if (oldLoaded) {
_history->addOlderSlice(QVector<MTPMessage>());
}
} else if (_migrated && _migrated->peer == peer) {
_migrated->addNewerSlice(messages);
}
}
void HistoryInner::repaintItem(const HistoryItem *item) {
2023-03-08 12:32:10 +01:00
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) {
2020-01-24 10:50:20 +01:00
const auto range = view->verticalRepaintRange();
update(0, top + range.top, width(), range.height);
2022-01-11 16:33:14 +01:00
const auto id = view->data()->fullId();
if (const auto area = _reactionsManager->lookupEffectArea(id)) {
update(*area);
}
}
}
template <bool TopToBottom, typename Method>
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<TopToBottom>(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<TopToBottom>(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 {
2020-09-11 17:33:26 +02:00
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 <typename Method>
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<Element*> 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<EnumItemsDirection::TopToBottom>(userpicCallback);
}
template <typename Method>
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<Element*> 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<EnumItemsDirection::BottomToTop>(dateCallback);
}
TextSelection HistoryInner::computeRenderSelection(
not_null<const SelectedItems*> selected,
not_null<Element*> view) const {
if (view->isHiddenByGroup()) {
return TextSelection();
}
const auto item = view->data();
const auto itemSelection = [&](not_null<HistoryItem*> 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;
}
2019-07-24 13:13:51 +02:00
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<Element*> 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<const Ui::ChatStyle*> st,
int width,
int height) {
2019-01-18 09:11:15 +01:00
if (!_emptyPainter) {
_emptyPainter = std::make_unique<HistoryView::EmptyPainter>(
_history);
}
_emptyPainter->paint(p, st, width, height);
2019-01-18 09:11:15 +01:00
}
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) {
2022-11-30 13:55:51 +01:00
if (_controller->contentOverlapped(this, e)
|| hasPendingResizedItems()) {
return;
} else if (_recountedAfterPendingResizedItems) {
_recountedAfterPendingResizedItems = false;
mouseActionUpdate();
}
Painter p(this);
auto clip = e->rect();
auto context = preparePaintContext(clip);
context.highlightPathCache = &_highlightPathCache;
_pathGradient->startFrame(
0,
width(),
std::min(st::msgMaxWidth / 2, width() / 2));
2019-01-22 08:50:21 +01:00
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) {
2023-03-08 12:32:10 +01:00
const auto top = _botAbout->top;
context.translate(0, -top);
context.selection = computeRenderSelection(&_selected, view);
p.translate(0, top);
view->draw(p, context);
2023-03-08 12:32:10 +01:00
context.translate(0, top);
p.translate(0, -top);
}
} else if (historyDisplayedEmpty) {
paintEmpty(p, context.st, width(), height());
2019-01-22 08:50:21 +01:00
} else {
_emptyPainter = nullptr;
}
2022-01-11 16:33:14 +01:00
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<not_null<HistoryItem*>>();
const auto markingAsViewed = _widget->markingContentsRead();
const auto guard = gsl::finally([&] {
2023-02-01 08:43:57 +01:00
if (_pinnedItem) {
_translateTracker->add(_pinnedItem);
2023-01-27 16:25:47 +01:00
}
_translateTracker->finishBunch();
if (readTill && _widget->markingMessagesRead()) {
session().data().histories().readInboxTill(readTill);
}
if (markingAsViewed && !readContents.empty()) {
session().api().markContentsRead(readContents);
}
_userpicsCache.clear();
});
const auto processPainted = [&](
not_null<Element*> 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 (markingAsViewed && 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);
2022-09-12 16:18:31 +02:00
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);
context.highlight = _widget->itemHighlight(view->data());
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();
const auto item = view->data();
if ((context.clip.y() < height)
&& (hdrawtop < top + height)
&& !sendingAnimation.hasAnimatedMessage(item)) {
context.reactionInfo
= _reactionsManager->currentReactionPaintInfo();
context.outbg = view->hasOutLayout();
context.selection = itemRenderSelection(
view,
selfromy - htop,
seltoy - htop);
context.highlight = _widget->itemHighlight(item);
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<Element*> 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<Element*> 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<HistoryView::DateBadge>()) {
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<QTouchEvent*>(e);
if (ev->device()->type() == base::TouchDevice::TouchScreen) {
touchEvent(ev);
return true;
}
}
return RpWidget::eventHook(e);
}
HistoryInner::VideoUserpic *HistoryInner::validateVideoUserpic(
not_null<PeerData*> 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<Element*> 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<VideoUserpic>(
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;
2021-09-29 02:00:11 +02:00
_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
2019-09-16 13:14:06 +02:00
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;
2021-09-29 02:00:11 +02:00
_touchSelectTimer.cancel();
_touchScroll = _touchSelect = false;
_touchScrollState = Ui::TouchScrollState::Manual;
2017-06-21 23:38:31 +02:00
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;
2021-09-29 02:00:11 +02:00
_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()) {
2021-09-29 02:00:11 +02:00
_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;
2019-09-13 14:22:54 +02:00
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;
2021-09-29 02:00:11 +02:00
_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) {
2021-09-29 02:00:11 +02:00
_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));
2017-06-21 23:38:31 +02:00
if (!buttonsPressed && _mouseAction != MouseAction::None) {
mouseReleaseEvent(e);
}
if (reallyMoved) {
lastGlobalPosition = e->globalPos();
if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) {
keepScrollDateForNow();
}
}
2017-06-21 23:38:31 +02:00
mouseActionUpdate(e->globalPos());
}
2017-06-21 23:38:31 +02:00
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 {
2023-03-08 12:32:10 +01:00
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
}
2017-06-21 23:38:31 +02:00
mouseActionStart(e->globalPos(), e->button());
}
2017-06-21 23:38:31 +02:00
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();
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::None;
_mouseActionItem = mouseActionView
? mouseActionView->data().get()
: nullptr;
_dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), mouseActionView);
2019-09-16 13:14:06 +02:00
_pressWasInactive = Ui::WasInactivePress(_controller->widget());
if (_pressWasInactive) {
Ui::MarkInactivePress(_controller->widget(), false);
}
const auto pressed = ClickHandler::getPressed();
if (pressed
&& (!Element::Hovered()
|| !Element::Hovered()->allowTextSelectionByHandler(pressed))) {
2017-06-21 23:38:31 +02:00
_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;
2019-06-12 15:26:04 +02:00
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);
2017-06-21 23:38:31 +02:00
_mouseTextSymbol = dragState.symbol;
_mouseAction = MouseAction::Selecting;
_mouseSelectType = TextSelectType::Paragraphs;
mouseActionUpdate(_mousePosition);
2021-09-29 02:00:11 +02:00
_trippleClickTimer.callOnce(
QApplication::doubleClickInterval());
}
}
} else if (Element::Pressed()) {
StateRequest request;
2019-06-12 15:26:04 +02:00
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
dragState = mouseActionView->textState(_dragStartPosition, request);
}
2017-06-21 23:38:31 +02:00
if (_mouseSelectType != TextSelectType::Paragraphs) {
if (Element::Pressed()) {
2017-06-21 23:38:31 +02:00
_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;
2017-06-21 23:38:31 +02:00
if (_mouseTextSymbol < selFrom || _mouseTextSymbol >= selTo) {
uponSelected = false;
}
}
}
if (uponSelected) {
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::PrepareDrag; // start text drag
} else if (!_pressWasInactive) {
if (_mouseCursorState == CursorState::Date) {
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::PrepareDrag; // start sticker drag or by-date drag
} else {
2017-06-21 23:38:31 +02:00
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);
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::Selecting;
repaintItem(_mouseActionItem);
2021-11-05 18:37:01 +01:00
} else if (!hasSelectRestriction()) {
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::PrepareSelect;
}
}
}
2021-11-05 18:37:01 +01:00
} else if (!_pressWasInactive && !hasSelectRestriction()) {
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::PrepareSelect; // start items select
}
}
}
2017-06-21 23:38:31 +02:00
if (!_mouseActionItem) {
_mouseAction = MouseAction::None;
} else if (_mouseAction == MouseAction::None) {
_mouseActionItem = nullptr;
}
}
2017-06-21 23:38:31 +02:00
void HistoryInner::mouseActionCancel() {
_mouseActionItem = nullptr;
_dragStateItem = nullptr;
2017-06-21 23:38:31 +02:00
_mouseAction = MouseAction::None;
_dragStartPosition = QPoint(0, 0);
_dragSelFrom = _dragSelTo = nullptr;
_wasSelectedText = false;
_selectScroll.cancel();
}
std::unique_ptr<QMimeData> HistoryInner::prepareDrag() {
if (_mouseAction != MouseAction::Dragging) {
return nullptr;
}
const auto pressedHandler = ClickHandler::getPressed();
2021-11-05 18:37:01 +01:00
if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())
2021-11-05 19:32:26 +01:00
|| hasCopyRestriction()) {
return nullptr;
}
2023-09-25 19:37:00 +02:00
const auto pressedView = viewByItem(_mouseActionItem);
bool uponSelected = false;
2023-09-25 19:37:00 +02:00
if (pressedView) {
if (!_selected.empty() && _selected.cbegin()->second == FullSelection) {
2023-09-25 19:37:00 +02:00
uponSelected = _mouseActionItem
&& (_selected.find(_mouseActionItem) != _selected.cend());
} else {
StateRequest request;
2019-06-12 15:26:04 +02:00
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
2023-09-25 19:37:00 +02:00
auto dragState = pressedView->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<QUrl>();
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();
}();
2019-09-16 13:14:06 +02:00
if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {
2019-04-04 12:02:22 +02:00
updateDragSelection(nullptr, nullptr, false);
_selectScroll.cancel();
if (!urls.isEmpty()) mimeData->setUrls(urls);
if (uponSelected && !_controller->adaptive().isOneColumn()) {
2017-04-27 23:17:00 +02:00
auto selectedState = getSelectionState();
if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
2019-07-24 13:13:51 +02:00
session().data().setMimeForwardIds(getSelectedItems());
2022-11-29 22:46:36 +01:00
mimeData->setData(u"application/x-td-forward"_q, "1");
2017-04-27 23:17:00 +02:00
}
}
return mimeData;
2023-09-25 19:37:00 +02:00
} else if (pressedView) {
auto forwardIds = MessageIdsList();
if (_mouseCursorState == CursorState::Date) {
2023-09-25 19:37:00 +02:00
forwardIds = session().data().itemOrItsGroup(_mouseActionItem);
} else if (pressedView->isHiddenByGroup() && pressedHandler) {
forwardIds = MessageIdsList(1, _mouseActionItem->fullId());
} else if (const auto media = pressedView->media()) {
if (media->dragItemByHandler(pressedHandler)) {
2023-09-25 19:37:00 +02:00
forwardIds = MessageIdsList(1, _mouseActionItem->fullId());
}
}
if (forwardIds.empty()) {
return nullptr;
}
2019-07-24 13:13:51 +02:00
session().data().setMimeForwardIds(std::move(forwardIds));
auto result = std::make_unique<QMimeData>();
2022-11-29 22:46:36 +01:00
result->setData(u"application/x-td-forward"_q, "1");
2023-09-25 19:37:00 +02:00
if (const auto media = pressedView->media()) {
if (const auto document = media->getDocument()) {
const auto filepath = document->filepath(true);
if (!filepath.isEmpty()) {
QList<QUrl> 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<const HistoryItem*> item) {
if (_pinnedItem == item) {
_pinnedItem = nullptr;
}
if (_history != item->history() && _migrated != item->history()) {
return;
}
if (_reactionsItem.current() == item) {
_reactionsItem = nullptr;
}
_animatedStickersPlayed.remove(item);
2021-12-16 21:21:18 +01:00
_reactionsManager->remove(item->fullId());
auto i = _selected.find(item);
if (i != _selected.cend()) {
_selected.erase(i);
_widget->updateTopBarSelection();
}
2017-06-21 23:38:31 +02:00
if (_mouseActionItem == item) {
mouseActionCancel();
}
if (_dragStateItem == item) {
_dragStateItem = nullptr;
}
2018-01-25 15:19:14 +01:00
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<const Element*> view) {
const auto refresh = [&](auto &saved) {
if (saved == view) {
2023-03-08 12:32:10 +01:00
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) {
2017-06-21 23:38:31 +02:00
mouseActionUpdate(screenPos);
2017-10-13 21:07:04 +02:00
auto activated = ClickHandler::unpressed();
2017-06-21 23:38:31 +02:00
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) {
2023-03-08 12:32:10 +01:00
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();
2022-10-19 12:59:37 +02:00
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);
2017-06-21 23:38:31 +02:00
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);
2017-06-21 23:38:31 +02:00
repaintItem(_mouseActionItem);
} else if ((i == _selected.cend())
&& !_dragStateItem->isService()
&& _dragStateItem->isRegular()
&& inSelectionMode()) {
if (_selected.size() < MaxSelectedItems) {
_selected.emplace(_dragStateItem, FullSelection);
2017-06-21 23:38:31 +02:00
repaintItem(_mouseActionItem);
}
} else {
_selected.clear();
update();
}
2017-06-21 23:38:31 +02:00
} 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();
}
}
}
2017-06-21 23:38:31 +02:00
_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();
2023-03-08 12:32:10 +01:00
if (const auto view = viewByItem(item)) {
2019-09-16 13:14:06 +02:00
TextUtilities::SetClipboardText(
view->selectedText(selection),
QClipboard::Selection);
}
}
}
void HistoryInner::mouseReleaseEvent(QMouseEvent *e) {
2017-06-21 23:38:31 +02:00
mouseActionFinish(e->globalPos(), e->button());
if (!rect().contains(e->pos())) {
leaveEvent(e);
}
}
void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
2017-06-21 23:38:31 +02:00
mouseActionStart(e->globalPos(), e->button());
2023-03-08 12:32:10 +01:00
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;
2019-06-12 15:26:04 +02:00
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
auto dragState = mouseActionView->textState(_dragStartPosition, request);
if (dragState.cursor == CursorState::Text) {
2017-06-21 23:38:31 +02:00
_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();
2021-09-29 02:00:11 +02:00
_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
}
}
2018-02-28 13:07:15 +01:00
if (!ClickHandler::getActive()
&& !ClickHandler::getPressed()
&& (_mouseCursorState == CursorState::None
|| _mouseCursorState == CursorState::Date)
&& !inSelectionMode()
2023-04-23 17:23:16 +02:00
&& !_emptyPainter
&& e->button() == Qt::LeftButton) {
if (const auto view = Element::Moused()) {
2018-02-28 13:07:15 +01:00
mouseActionCancel();
switch (HistoryView::CurrentQuickAction()) {
case HistoryView::DoubleClickQuickAction::Reply: {
_widget->replyToMessage(view->data());
} break;
case HistoryView::DoubleClickQuickAction::React: {
toggleFavoriteReaction(view);
} break;
default: break;
}
2018-02-28 13:07:15 +01:00
}
}
}
void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
const auto item = view->data();
2022-08-24 10:42:01 +02:00
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) {
2022-08-16 13:28:17 +02:00
view->animateReaction({ .id = favorite });
}
}
item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
}
2023-10-31 19:37:59 +01:00
HistoryView::SelectedQuote HistoryInner::selectedQuote(
not_null<HistoryItem*> item) const {
if (_selected.size() != 1
|| _selected.begin()->first != item
|| _selected.begin()->second == FullSelection) {
return {};
}
const auto view = item->mainView();
if (!view) {
return {};
}
return view->selectedQuote(_selected.begin()->second);
}
void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {
showContextMenu(e);
}
void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
if (e->reason() == QContextMenuEvent::Mouse) {
2017-06-21 23:38:31 +02:00
mouseActionUpdate(e->globalPos());
}
const auto link = ClickHandler::getActive();
if (link
2022-08-16 13:28:17 +02:00
&& !link->property(
kSendReactionEmojiProperty).value<Data::ReactionId>().empty()
&& _reactionsManager->showContextMenu(
this,
e,
2022-08-24 10:42:01 +02:00
session().data().reactions().favoriteId())) {
return;
}
2017-04-27 23:17:00 +02:00
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 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 whoReactedItem = groupLeaderOrSelf(_dragStateItem);
const auto hasWhoReactedItem = whoReactedItem
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::All);
2022-08-16 13:28:17 +02:00
const auto clickedReaction = link
? link->property(
kReactionsCountEmojiProperty).value<Data::ReactionId>()
: Data::ReactionId();
_whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty()
&& whoReactedItem
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::One)) {
HistoryView::ShowWhoReactedMenu(
&_menu,
e->globalPos(),
this,
whoReactedItem,
2022-08-16 13:28:17 +02:00
clickedReaction,
_controller,
_whoReactedMenuLifetime);
e->accept();
return;
}
2022-08-17 20:03:57 +02:00
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);
const auto session = &this->session();
const auto controller = _controller;
2020-10-29 16:21:16 +01:00
const auto addItemActions = [&](
HistoryItem *item,
HistoryItem *albumPartItem) {
if (!item
|| !item->isRegular()
|| isUponSelected == 2
|| isUponSelected == -2) {
return;
}
const auto itemId = item->fullId();
2020-09-03 10:42:25 +02:00
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);
2020-09-03 10:42:25 +02:00
_menu->addAction(phrase, [=] {
controller->showRepliesForMessage(
_history,
rootId,
highlightId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconViewReplies);
2020-08-28 12:01:55 +02:00
}
2020-10-29 16:21:16 +01:00
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();
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_edit_msg(tr::now), [=] {
2020-10-29 16:21:16 +01:00
_widget->editMessage(editItemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconEdit);
}
2020-10-26 10:33:19 +01:00
const auto pinItem = (item->canPin() && item->isPinned())
? item
: groupLeaderOrSelf(item);
if (pinItem->canPin()) {
const auto isPinned = pinItem->isPinned();
const auto pinItemId = pinItem->fullId();
_menu->addAction(isPinned ? tr::lng_context_unpin_msg(tr::now) : tr::lng_context_pin_msg(tr::now), crl::guard(controller, [=] {
2020-10-26 10:33:19 +01:00
Window::ToggleMessagePinned(controller, pinItemId, !isPinned);
2021-12-09 18:56:24 +01:00
}), isPinned ? &st::menuIconUnpin : &st::menuIconPin);
}
if (!item->isService()
&& peerIsChannel(itemId.peer)
&& !_peer->isMegagroup()) {
if (const auto channel = _peer->asChannel()) {
if (channel->flags() & ChannelDataFlag::CanGetStatistics) {
auto callback = crl::guard(controller, [=] {
controller->showSection(
Info::Statistics::Make(channel, itemId));
});
_menu->addAction(
tr::lng_stats_title(tr::now),
std::move(callback),
&st::menuIconStats);
}
}
}
};
const auto addPhotoActions = [&](not_null<PhotoData*> photo, HistoryItem *item) {
const auto media = photo->activeMediaView();
const auto itemId = item ? item->fullId() : FullMsgId();
if (!photo->isNull() && media && media->loaded() && !hasCopyMediaRestriction(item)) {
2022-11-30 13:55:51 +01:00
_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {
savePhotoToFile(photo);
2021-12-09 18:56:24 +01:00
}), &st::menuIconSaveImage);
_menu->addAction(tr::lng_context_copy_image(tr::now), [=] {
copyContextImage(photo, itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconCopy);
}
if (photo->hasAttachedStickers()) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] {
session->api().attachedStickers().requestAttachedStickerSets(
controller,
photo);
2021-12-09 18:56:24 +01:00
}, &st::menuIconStickers);
}
};
const auto addDocumentActions = [&](not_null<DocumentData*> document, HistoryItem *item) {
if (document->loading()) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_cancel_download(tr::now), [=] {
cancelContextDownload(document);
2021-12-09 18:56:24 +01:00
}, &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) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_open_gif(tr::now), [=] {
openContextGif(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconShowInChat);
}
if (!hasCopyMediaRestriction(item)) {
2021-11-05 19:32:26 +01:00
_menu->addAction(tr::lng_context_save_gif(tr::now), [=] {
saveContextGif(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconGif);
2021-11-05 19:32:26 +01:00
}
}
if (!document->filepath(true).isEmpty()) {
2019-06-19 17:09:03 +02:00
_menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] {
showContextInFolder(document);
2021-12-09 18:56:24 +01:00
}, &st::menuIconShowInFolder);
}
if (item && !hasCopyMediaRestriction(item)) {
HistoryView::AddSaveSoundForNotifications(
_menu,
item,
document,
controller);
2022-11-30 13:55:51 +01:00
_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);
2021-12-09 18:56:24 +01:00
}), &st::menuIconDownload);
}
if (document->hasAttachedStickers()) {
_menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] {
session->api().attachedStickers().requestAttachedStickerSets(
controller,
document);
2021-12-09 18:56:24 +01:00
}, &st::menuIconStickers);
}
};
2021-11-05 18:37:01 +01:00
const auto addSelectMessageAction = [&](
not_null<HistoryItem*> 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)) {
2023-03-08 12:32:10 +01:00
if (const auto view = viewByItem(item)) {
2021-11-05 18:37:01 +01:00
if (asGroup) {
changeSelectionAsGroup(&_selected, item, SelectAction::Select);
} else {
changeSelection(&_selected, item, SelectAction::Select);
}
repaintItem(item);
_widget->updateTopBarSelection();
}
}
2021-12-09 18:56:24 +01:00
}, &st::menuIconSelect);
2021-11-05 18:37:01 +01:00
}
};
2023-10-25 09:00:22 +02:00
const auto addReplyAction = [&](HistoryItem *item) {
if (!item || !item->isRegular()) {
2023-10-25 09:00:22 +02:00
return;
}
const auto canSendReply = [&] {
const auto peer = item->history()->peer;
const auto topic = item->topic();
return topic
? Data::CanSendAnything(topic)
: (Data::CanSendAnything(peer)
&& (!peer->isChannel() || peer->asChannel()->amIn()));
}();
const auto canReply = canSendReply || item->allowsForward();
2023-10-25 09:00:22 +02:00
if (canReply) {
2023-10-31 19:37:59 +01:00
const auto selected = selectedQuote(item);
auto text = selected
? tr::lng_context_quote_and_reply(tr::now)
: tr::lng_context_reply_msg(tr::now);
const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
text.replace('&', u"&&"_q);
2023-10-25 09:00:22 +02:00
_menu->addAction(text, [=] {
if (canSendReply) {
_widget->replyToMessage({ itemId, quote });
if (!quote.empty()) {
_widget->clearSelected();
}
2023-10-25 09:00:22 +02:00
} else {
HistoryView::Controls::ShowReplyToChatBox(
controller->uiShow(),
{ itemId, quote });
}
}, &st::menuIconReply);
}
};
const auto lnkPhoto = link
? reinterpret_cast<PhotoData*>(
link->property(kPhotoLinkMediaProperty).toULongLong())
: nullptr;
const auto lnkDocument = link
? reinterpret_cast<DocumentData*>(
link->property(kDocumentLinkMediaProperty).toULongLong())
: nullptr;
if (lnkPhoto || lnkDocument) {
const auto item = _dragStateItem;
const auto itemId = item ? item->fullId() : FullMsgId();
2023-10-25 09:00:22 +02:00
addReplyAction(item);
if (isUponSelected > 0) {
const auto selectedText = getSelectedText();
2023-06-27 18:26:43 +02:00
if (!hasCopyRestrictionForSelected()
&& !selectedText.empty()) {
_menu->addAction(
(isUponSelected > 1
? tr::lng_context_copy_selected_items(tr::now)
: tr::lng_context_copy_selected(tr::now)),
[=] { copySelectedText(); },
&st::menuIconCopy);
}
if (item && !Ui::SkipTranslate(selectedText.rich)) {
const auto peer = item->history()->peer;
_menu->addAction(tr::lng_context_translate_selected({}), [=] {
_controller->show(Box(
Ui::TranslateBox,
peer,
MsgId(),
getSelectedText().rich,
hasCopyRestrictionForSelected()));
}, &st::menuIconTranslate);
}
}
2020-10-29 16:21:16 +01:00
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);
2021-12-09 18:56:24 +01:00
}, &st::menuIconLink);
}
if (isUponSelected > 1) {
2017-04-27 23:17:00 +02:00
if (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_forward_selected(tr::now), [=] {
_widget->forwardSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconForward);
2017-04-27 23:17:00 +02:00
}
if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_delete_selected(tr::now), [=] {
_widget->confirmDeleteSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconDelete);
}
if (selectedState.count > 0 && !hasCopyRestrictionForSelected()) {
Menu::AddDownloadFilesAction(_menu, controller, _selected, this);
}
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_clear_selection(tr::now), [=] {
_widget->clearSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconSelect);
} else if (item) {
const auto itemId = item->fullId();
const auto blockSender = item->history()->peer->isRepliesChat();
if (isUponSelected != -2) {
if (item->allowsForward()) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_forward_msg(tr::now), [=] {
forwardItem(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconForward);
}
if (item->canDelete()) {
const auto callback = [=] { deleteItem(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 (!blockSender && item->suggestReport()) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_report_msg(tr::now), [=] {
reportItem(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconReport);
}
}
2021-11-05 18:37:01 +01:00
addSelectMessageAction(item, false);
if (isUponSelected != -2 && blockSender) {
_menu->addAction(tr::lng_profile_block_user(tr::now), [=] {
blockSenderItem(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconBlock);
}
}
} else { // maybe cursor on some text history item?
2020-10-29 16:21:16 +01:00
const auto albumPartItem = _dragStateItem;
const auto item = [&] {
const auto result = Element::Hovered()
? Element::Hovered()->data().get()
: Element::HoveredLink()
? Element::HoveredLink()->data().get()
2020-10-26 10:33:19 +01:00
: 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();
2023-03-08 12:32:10 +01:00
const auto view = viewByItem(item);
const auto actionText = link
? link->copyToClipboardContextItemText()
: QString();
if (isUponSelected > 0) {
2023-10-25 09:00:22 +02:00
addReplyAction(item);
2023-09-18 07:50:09 +02:00
const auto selectedText = getSelectedText();
if (!hasCopyRestrictionForSelected() && !selectedText.empty()) {
_menu->addAction(
((isUponSelected > 1)
? tr::lng_context_copy_selected_items(tr::now)
: tr::lng_context_copy_selected(tr::now)),
2021-12-09 18:56:24 +01:00
[=] { copySelectedText(); },
&st::menuIconCopy);
}
if (item && !Ui::SkipTranslate(selectedText.rich)) {
2023-09-18 07:50:09 +02:00
const auto peer = item->history()->peer;
_menu->addAction(tr::lng_context_translate_selected({}), [=] {
_controller->show(Box(
Ui::TranslateBox,
2023-09-18 07:50:09 +02:00
peer,
MsgId(),
2023-09-18 07:50:09 +02:00
selectedText.rich,
hasCopyRestrictionForSelected()));
}, &st::menuIconTranslate);
}
2020-10-29 16:21:16 +01:00
addItemActions(item, item);
} else {
2023-10-25 09:00:22 +02:00
addReplyAction(item);
2020-10-29 16:21:16 +01:00
addItemActions(item, albumPartItem);
if (item && !isUponSelected) {
2018-12-18 12:06:57 +01:00
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) {
2019-06-19 17:09:03 +02:00
_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {
showStickerPackInfo(document);
2021-12-09 18:56:24 +01:00
}, &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->uiShow(), document, itemId);
2021-12-09 18:56:24 +01:00
}, isFaved ? &st::menuIconUnfave : &st::menuIconFave);
}
if (!hasCopyMediaRestriction(item)) {
2022-11-30 13:55:51 +01:00
_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {
saveDocumentToFile(itemId, document);
2021-12-09 18:56:24 +01:00
}), &st::menuIconDownload);
}
}
}
2018-12-19 12:20:04 +01:00
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), [=] {
2019-09-04 09:19:15 +02:00
QGuiApplication::clipboard()->setText(phone);
2021-12-09 18:56:24 +01:00
}, &st::menuIconCopy);
2018-12-19 12:20:04 +01:00
}
}
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()] {
2019-09-04 09:19:15 +02:00
QGuiApplication::clipboard()->setText(text);
2021-12-09 18:56:24 +01:00
},
&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);
2021-12-09 18:56:24 +01:00
}, &st::menuIconLink);
}
if (item && item->isSponsored()) {
if (!_menu->empty()) {
_menu->addSeparator(&st::expandedMenuSeparator);
}
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
_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) {
2017-04-27 23:17:00 +02:00
if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_forward_selected(tr::now), [=] {
_widget->forwardSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconForward);
2017-04-27 23:17:00 +02:00
}
if (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_delete_selected(tr::now), [=] {
_widget->confirmDeleteSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconDelete);
}
if (selectedState.count > 0 && !hasCopyRestrictionForSelected()) {
Menu::AddDownloadFilesAction(_menu, controller, _selected, this);
}
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_clear_selection(tr::now), [=] {
_widget->clearSelected();
2021-12-09 18:56:24 +01:00
}, &st::menuIconSelect);
} else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->isRegular())) {
if (isUponSelected != -2) {
if (canForward) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_forward_msg(tr::now), [=] {
forwardAsGroup(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconForward);
}
if (canDelete) {
const auto callback = [=] {
deleteAsGroup(itemId);
};
if (item->isUploading()) {
2021-12-09 18:56:24 +01:00
_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) {
2019-06-19 17:09:03 +02:00
_menu->addAction(tr::lng_context_report_msg(tr::now), [=] {
reportAsGroup(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconReport);
}
}
2021-11-05 18:37:01 +01:00
addSelectMessageAction(item);
if (isUponSelected != -2 && canBlockSender) {
_menu->addAction(tr::lng_profile_block_user(tr::now), [=] {
blockSenderAsGroup(itemId);
2021-12-09 18:56:24 +01:00
}, &st::menuIconBlock);
}
} else if (Element::Moused()) {
addSelectMessageAction(Element::Moused()->data());
}
}
if (_dragStateItem) {
HistoryView::AddEmojiPacksAction(
_menu,
_dragStateItem,
HistoryView::EmojiPacksSource::Message,
_controller);
}
2021-12-24 14:51:53 +01:00
if (hasWhoReactedItem) {
HistoryView::AddWhoReactedAction(
_menu,
this,
whoReactedItem,
_controller);
}
if (_menu->empty()) {
_menu = nullptr;
2022-08-17 20:03:57 +02:00
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())
2022-08-17 20:03:57 +02:00
: AttachSelectorResult::Skipped;
if (attached == AttachSelectorResult::Failed) {
_menu = nullptr;
return;
} else if (attached == AttachSelectorResult::Attached) {
_menu->popupPrepared();
} else {
2022-08-17 20:03:57 +02:00
_menu->popup(desiredPosition);
}
2022-08-17 20:03:57 +02:00
e->accept();
}
bool HistoryInner::hasCopyRestriction(HistoryItem *item) const {
return !_peer->allowsForwarding() || (item && item->forbidsForward());
2021-11-05 18:37:01 +01:00
}
bool HistoryInner::hasCopyMediaRestriction(
not_null<HistoryItem*> item) const {
return hasCopyRestriction(item) || item->forbidsSaving();
}
bool HistoryInner::showCopyRestriction(HistoryItem *item) {
if (!hasCopyRestriction(item)) {
return false;
}
_controller->showToast(_peer->isBroadcast()
? tr::lng_error_nocopy_channel(tr::now)
: tr::lng_error_nocopy_group(tr::now));
return true;
}
bool HistoryInner::showCopyMediaRestriction(not_null<HistoryItem*> item) {
if (!hasCopyMediaRestriction(item)) {
return false;
}
_controller->showToast(_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<PhotoData*> photo) {
2020-05-25 16:16:04 +02:00
const auto media = photo->activeMediaView();
if (photo->isNull() || !media || !media->loaded()) {
return;
}
2022-11-29 22:46:36 +01:00
auto filter = u"JPEG Image (*.jpg);;"_q + FileDialog::AllFilesFilter();
FileDialog::GetWritePath(
this,
2019-06-19 17:09:03 +02:00
tr::lng_save_photo(tr::now),
filter,
2022-11-29 22:46:36 +01:00
filedialogDefaultName(u"photo"_q, u".jpg"_q),
crl::guard(this, [=](const QString &result) {
if (!result.isEmpty()) {
media->saveToFile(result);
}
}));
}
void HistoryInner::copyContextImage(
not_null<PhotoData*> photo,
FullMsgId itemId) {
const auto item = session().data().message(itemId);
2020-05-25 16:16:04 +02:00
const auto media = photo->activeMediaView();
const auto restricted = item
? showCopyMediaRestriction(item)
: IsServerMsgId(itemId.msg);
2020-05-25 16:16:04 +02:00
if (photo->isNull() || !media || !media->loaded()) {
return;
} else if (!restricted) {
media->setToClipboard();
2020-05-25 16:16:04 +02:00
}
}
void HistoryInner::showStickerPackInfo(not_null<DocumentData*> document) {
StickerSetBox::Show(_controller->uiShow(), document);
}
void HistoryInner::cancelContextDownload(not_null<DocumentData*> document) {
document->cancel();
}
void HistoryInner::showContextInFolder(not_null<DocumentData*> document) {
const auto filepath = document->filepath(true);
if (!filepath.isEmpty()) {
File::ShowInFolder(filepath);
}
}
2018-07-13 23:25:47 +02:00
void HistoryInner::saveDocumentToFile(
FullMsgId contextId,
not_null<DocumentData*> document) {
2022-02-27 14:32:36 +01:00
DocumentSaveClickHandler::SaveAndTrack(
2019-02-27 12:36:19 +01:00
contextId,
document,
DocumentSaveClickHandler::Mode::ToNewFile);
}
void HistoryInner::openContextGif(FullMsgId itemId) {
2019-07-24 13:13:51 +02:00
if (const auto item = session().data().message(itemId)) {
if (const auto media = item->media()) {
if (const auto document = media->document()) {
_controller->openDocument(document, true, { itemId });
}
}
}
}
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()) {
2022-05-09 15:09:46 +02:00
Api::ToggleSavedGif(
_controller->uiShow(),
2022-05-09 15:09:46 +02:00
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;
2017-06-21 23:38:31 +02:00
if (_mouseAction == MouseAction::Selecting && _dragSelFrom && _dragSelTo) {
applyDragSelection(&selected);
}
if (selected.empty()) {
return TextForMimeData();
}
if (selected.cbegin()->second != FullSelection) {
const auto [item, selection] = *selected.cbegin();
2023-03-08 12:32:10 +01:00
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<not_null<const Data::Group*>>();
auto fullSize = 0;
auto texts = base::flat_map<Data::MessagePosition, Part>();
const auto wrapItem = [&](
not_null<HistoryItem*> item,
TextForMimeData &&unwrapped) {
const auto i = texts.emplace(item->position(), Part{
2022-08-09 13:12:19 +02:00
.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<HistoryItem*> item) {
wrapItem(item, HistoryItemText(item));
};
const auto addGroup = [&](not_null<const Data::Group*> group) {
Expects(!group->items.empty());
wrapItem(group->items.back(), HistoryGroupText(group));
};
for (const auto &[item, selection] : selected) {
2019-07-24 13:13:51 +02:00
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();
2022-11-26 22:20:17 +01:00
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()) {
2019-09-16 13:14:06 +02:00
TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);
#endif // Q_OS_MAC
} else if (e == QKeySequence::Delete || e->key() == Qt::Key_Backspace) {
2017-04-27 23:17:00 +02:00
auto selectedState = getSelectionState();
if (selectedState.count > 0
&& selectedState.canDeleteCount == selectedState.count) {
_widget->confirmDeleteSelected();
}
} else if (!(e->modifiers() & ~Qt::ShiftModifier)
&& e->key() != Qt::Key_Shift) {
_widget->tryProcessKeyInput(e);
} else {
e->ignore();
}
}
void HistoryInner::checkActivation() {
if (!_widget->markingMessagesRead()) {
return;
}
adjustCurrent(_visibleAreaBottom);
if (_history->loadedAtBottom() && _visibleAreaBottom >= height()) {
Do not remove notification reference on NotificationClosed In general we need to keep a reference to the notification id, so that we can delete the notification later from history - unless the NotificationClosed reason was that the user actively dismissed it, in which case it is not kept in history anyway (so we can dismiss our reference too). -- Background -- Some desktop environments such as KDE keep a history of notifications. An API is provided to delete notifications from that history by calling the org.freedesktop.Notifications.CloseNotification endpoint with the ID of that notification. If the notification was already closed (timed out), then this will delete the notification from history. The intent is to clear these notifications from the notification history as soon as a chat with notifications originating from that person is opened, as the user is then not interested anymore in those notifications and to prevent unnecessary clutter in the history widget. It is also cleared when the chat is read on another device. -- Problem -- Telegram already has all the code in place to support this functionality, but unfortunately this did not work on Linux before, because we listen to the NotificationClosed signal and remove our reference to the notification id from our internal manager as soon as we get that signal. This means that we do not clear that notification from history once we open the chat with that person (unless we open the chat before the notification has timed out, i.e. if we didn't get the NotificationClosed signal). -- Fix -- To fix this, we keep our notification reference (if the notification was not dismissed by the user), which means that our reference will be kept around until we open the chat with that person (or close Telegram entirely). Since all the needed functionality for deleting notifications was already in place, this patch is quite short as we only need to keep the reference around longer than we did before this patch. Note also that code is already in place to clear notifications for messages that were read on another device: History::inboxRead() calls Core::App().notifications().clearIncomingFromHistory() Fixes #17111
2022-03-04 13:46:13 +01:00
// 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;
}
}
}
}
2020-02-19 16:35:26 +01:00
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);
}
2019-01-15 12:57:45 +01:00
// With migrated history we perhaps do not need to display
// the first _history message date (just skip it by height).
_historySkipHeight = 0;
2019-01-15 12:57:45 +01:00
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;
2019-05-14 11:50:44 +02:00
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();
}
2019-05-14 11:50:44 +02:00
2020-05-28 16:32:10 +02:00
// Unload userpics.
if (_userpics.size() > kClearUserpicsAfter) {
_userpicsCache = std::move(_userpics);
}
2019-05-14 11:50:44 +02:00
// 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();
2021-09-15 16:49:06 +02:00
_emojiInteractions->visibleAreaUpdated(
2022-04-26 15:48:38 +02:00
_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);
}
2021-07-19 12:02:36 +02:00
void HistoryInner::setItemsRevealHeight(int revealHeight) {
_revealHeight = revealHeight;
}
void HistoryInner::changeItemsRevealHeight(int revealHeight) {
if (_revealHeight == revealHeight) {
return;
}
_revealHeight = revealHeight;
updateSize();
2021-07-19 12:02:36 +02:00
}
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));
}
2021-09-15 16:49:06 +02:00
if (_historyPaddingTop != newHistoryPaddingTop) {
_historyPaddingTop = newHistoryPaddingTop;
}
int newHeight = _historyPaddingTop + itemsHeight + st::historyPaddingBottom;
if (width() != _scroll->width() || height() != newHeight) {
resize(_scroll->width(), newHeight);
2021-07-19 12:02:36 +02:00
if (!_revealHeight) {
mouseActionUpdate(QCursor::pos());
}
} else {
update();
}
}
2023-02-01 08:43:57 +01:00
void HistoryInner::setShownPinned(HistoryItem *item) {
_pinnedItem = item;
2023-01-27 16:25:47 +01:00
}
void HistoryInner::enterEventHook(QEnterEvent *e) {
2017-06-21 23:38:31 +02:00
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);
2019-05-14 11:50:44 +02:00
}
delete _menu;
2017-06-21 23:38:31 +02:00
_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();
2019-04-04 12:02:22 +02:00
_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;
}
2019-05-14 11:50:44 +02:00
bool HistoryInner::elementIntersectsRange(
not_null<const Element*> 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<const Element*> view) {
_animatedStickersPlayed.emplace(view->data());
2019-08-01 13:42:24 +02:00
}
2020-01-14 12:55:18 +01:00
void HistoryInner::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
_controller->showPollResults(poll, context);
2020-01-14 12:55:18 +01:00
}
void HistoryInner::elementOpenPhoto(
not_null<PhotoData*> photo,
FullMsgId context) {
_controller->openPhoto(photo, { context });
}
void HistoryInner::elementOpenDocument(
not_null<DocumentData*> document,
FullMsgId context,
bool showInMediaView) {
_controller->openDocument(document, showInMediaView, { context });
}
void HistoryInner::elementCancelUpload(const FullMsgId &context) {
if (const auto item = session().data().message(context)) {
_controller->cancelUploadLayer(item);
}
}
2020-04-30 12:11:05 +02:00
void HistoryInner::elementShowTooltip(
const TextWithEntities &text,
Fn<void()> hiddenCallback) {
_widget->showInfoTooltip(text, std::move(hiddenCallback));
2020-04-29 14:36:51 +02:00
}
bool HistoryInner::elementAnimationsPaused() {
return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
}
void HistoryInner::elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
2021-07-27 01:18:11 +02:00
_widget->sendBotCommand({ _history->peer, command, context });
}
void HistoryInner::elementHandleViaClick(not_null<UserData*> bot) {
_widget->insertBotCommand('@' + bot->username());
}
bool HistoryInner::elementIsChatWide() {
return _isChatWide;
}
not_null<Ui::PathShiftGradient*> HistoryInner::elementPathShiftGradient() {
return _pathGradient.get();
}
2023-10-10 08:49:22 +02:00
void HistoryInner::elementReplyTo(const FullReplyTo &to) {
return _widget->replyToMessage(to);
}
2021-09-14 18:55:35 +02:00
void HistoryInner::elementStartInteraction(not_null<const Element*> view) {
2021-09-16 13:20:25 +02:00
_controller->emojiInteractions().startOutgoing(view);
2021-09-14 18:55:35 +02:00
}
void HistoryInner::elementStartPremium(
not_null<const Element*> view,
Element *replacing) {
const auto already = !_emojiInteractions->playPremiumEffect(
view,
replacing);
2022-04-22 18:23:47 +02:00
_animatedStickersPlayed.emplace(view->data());
if (already) {
_widget->showPremiumStickerTooltip(view);
}
2022-04-22 18:23:47 +02:00
}
void HistoryInner::elementCancelPremium(not_null<const Element*> view) {
_emojiInteractions->cancelPremiumEffect(view);
}
2018-01-09 18:08:31 +01:00
auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {};
for (auto &selected : _selected) {
if (selected.second == FullSelection) {
2017-04-27 23:17:00 +02:00
++result.count;
if (selected.first->canDelete()) {
2017-04-27 23:17:00 +02:00
++result.canDeleteCount;
}
if (selected.first->allowsForward()) {
2017-04-27 23:17:00 +02:00
++result.canForwardCount;
}
} else if (selected.second.from != selected.second.to) {
2017-04-27 23:17:00 +02:00
result.textSelected = true;
}
}
2017-04-27 23:17:00 +02:00
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 {};
}
2019-10-02 11:54:29 +02:00
auto result = ranges::make_subrange(
_selected.begin(),
_selected.end()
2021-03-13 19:07:29 +01:00
) | views::filter([](const auto &selected) {
const auto item = selected.first;
return item && !item->isService() && item->isRegular();
2021-03-13 19:07:29 +01:00
}) | 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;
2017-06-21 23:38:31 +02:00
mouseActionStart(_touchPos, Qt::LeftButton);
}
auto HistoryInner::reactionButtonParameters(
not_null<const Element*> view,
2021-12-25 21:01:49 +01:00
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 {};
}
2021-12-25 21:01:49 +01:00
auto result = view->reactionButtonParameters(
position,
reactionState
).translated({ 0, itemTop(view) });
result.visibleTop = _visibleAreaTop;
result.visibleBottom = _visibleAreaBottom;
result.globalPointer = _mousePosition;
2021-12-25 21:01:49 +01:00
return result;
}
void HistoryInner::mouseActionUpdate() {
2019-01-18 09:11:15 +01:00
if (hasPendingResizedItems()) {
return;
}
2017-06-21 23:38:31 +02:00
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);
2023-03-08 12:32:10 +01:00
const auto reactionView = viewByItem(reactionItem);
const auto view = reactionView
? reactionView
2023-03-08 12:32:10 +01:00
: (_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);
2022-01-10 15:22:43 +01:00
if (changed) {
repaintItem(Element::Moused());
Element::Moused(view);
repaintItem(Element::Moused());
}
m = mapPointToItem(point, view);
2021-12-25 21:01:49 +01:00
_reactionsManager->updateButton(reactionButtonParameters(
view,
m,
reactionState));
2022-01-10 15:22:43 +01:00
if (changed) {
_reactionsItem = item;
2022-01-10 15:22:43 +01:00
}
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);
}
2021-12-25 21:01:49 +01:00
_reactionsManager->updateButton({});
}
2023-03-08 12:32:10 +01:00
if (_mouseActionItem && !viewByItem(_mouseActionItem)) {
2017-06-21 23:38:31 +02:00
mouseActionCancel();
}
TextState dragState;
ClickHandlerHost *lnkhost = nullptr;
auto selectingText = (item == _mouseActionItem)
&& (view == Element::Hovered())
&& !_selected.empty()
&& (_selected.cbegin()->second != FullSelection);
2021-12-16 21:21:18 +01:00
const auto overReaction = reactionView && reactionState.link;
if (overReaction) {
dragState = reactionState;
lnkhost = reactionView;
} else if (item) {
2017-06-21 23:38:31 +02:00
if (item != _mouseActionItem || (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
if (_mouseAction == MouseAction::PrepareDrag) {
_mouseAction = MouseAction::Dragging;
InvokeQueued(this, [=] { performDrag(); });
2017-06-21 23:38:31 +02:00
} else if (_mouseAction == MouseAction::PrepareSelect) {
_mouseAction = MouseAction::Selecting;
}
}
auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
2019-04-02 11:13:30 +02:00
auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
enumerateDates([&](not_null<Element*> 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<HistoryView::DateBadge>()) {
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) {
2018-02-21 22:17:36 +01:00
_scrollDateLink = std::make_shared<Window::DateClickHandler>(item->history(), view->dateTime().date());
} else {
2018-02-21 22:17:36 +01:00
static_cast<Window::DateClickHandler*>(_scrollDateLink.get())->setDate(view->dateTime().date());
}
dragState = TextState(
nullptr,
_scrollDateLink);
2019-07-24 13:13:51 +02:00
_dragStateItem = session().data().message(dragState.itemId);
lnkhost = view;
}
}
return false;
}
return true;
});
if (!dragState.link) {
StateRequest request;
2017-06-21 23:38:31 +02:00
if (_mouseAction == MouseAction::Selecting) {
2019-06-12 15:26:04 +02:00
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);
2019-07-24 13:13:51 +02:00
_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<Element*> 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);
2017-06-21 23:38:31 +02:00
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;
2017-06-21 23:38:31 +02:00
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) {
2017-06-21 23:38:31 +02:00
if (_mouseAction == MouseAction::Selecting) {
if (selectingText) {
uint16 second = dragState.symbol;
2017-06-21 23:38:31 +02:00
if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
++second;
}
auto selState = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
if (_mouseSelectType != TextSelectType::Letters) {
2023-03-08 12:32:10 +01:00
if (const auto view = viewByItem(_mouseActionItem)) {
selState = view->adjustSelection(selState, _mouseSelectType);
}
}
if (!selState.empty()) {
// We started selecting text in web page preview.
ClickHandler::unpressed();
}
2017-06-21 23:38:31 +02:00
if (_selected[_mouseActionItem] != selState) {
_selected[_mouseActionItem] = selState;
repaintItem(_mouseActionItem);
}
if (!_wasSelectedText && (selState == FullSelection || selState.from != selState.to)) {
_wasSelectedText = true;
setFocus();
}
2019-01-18 09:11:15 +01:00
updateDragSelection(nullptr, nullptr, false);
} else {
2017-06-21 23:38:31 +02:00
auto selectingDown = (itemTop(_mouseActionItem) < itemTop(item)) || (_mouseActionItem == item && _dragStartPosition.y() < m.y());
2023-03-08 12:32:10 +01:00
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;
}
}
}
2017-06-21 23:38:31 +02:00
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);
}
2017-06-21 23:38:31 +02:00
} 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) {
2023-03-08 12:32:10 +01:00
if (const auto pressedView = viewByItem(pressedItem)) {
if (pressedItem->history() == _history || pressedItem->history() == _migrated) {
auto adjustedPoint = mapPointToItem(point, pressedView);
pressedView->updatePressed(adjustedPoint);
}
}
}
2017-06-21 23:38:31 +02:00
if (_mouseAction == MouseAction::Selecting) {
_selectScroll.checkDeltaScroll(
mousePos,
_scroll->scrollTop(),
_scroll->scrollTop() + _scroll->height());
} else {
2019-04-04 12:02:22 +02:00
updateDragSelection(nullptr, nullptr, false);
_selectScroll.cancel();
}
2017-06-21 23:38:31 +02:00
if (_mouseAction == MouseAction::None && (lnkChanged || cur != _cursor)) {
setCursor(_cursor = cur);
}
}
void HistoryInner::updateDragSelection(Element *dragSelFrom, Element *dragSelTo, bool dragSelecting) {
2017-10-13 21:07:04 +02:00
if (_dragSelFrom == dragSelFrom && _dragSelTo == dragSelTo && _dragSelecting == dragSelecting) {
return;
2021-11-05 18:37:01 +01:00
} else if (dragSelFrom && hasSelectRestriction()) {
updateDragSelection(nullptr, nullptr, false);
return;
2017-10-13 21:07:04 +02:00
}
_dragSelFrom = dragSelFrom;
_dragSelTo = dragSelTo;
int32 fromy = itemTop(_dragSelFrom), toy = itemTop(_dragSelTo);
if (fromy >= 0 && toy >= 0 && fromy > toy) {
std::swap(_dragSelFrom, _dragSelTo);
2017-10-13 21:07:04 +02:00
}
_dragSelecting = dragSelecting;
if (!_wasSelectedText && _dragSelFrom && _dragSelTo && _dragSelecting) {
_wasSelectedText = true;
setFocus();
}
update();
}
int HistoryInner::historyHeight() const {
int result = 0;
2019-01-18 09:11:15 +01:00
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();
2019-01-18 09:11:15 +01:00
return !_history->isEmpty()
? (mig >= 0
? (mig + _migrated->height() - _historySkipHeight)
: _historyPaddingTop)
: -1;
}
int HistoryInner::historyDrawTop() const {
2017-05-21 18:06:33 +02:00
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;
}
2023-03-08 12:32:10 +01:00
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 {
2023-03-08 12:32:10 +01:00
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<Element*, int> {
const auto normalTop = historyTop();
const auto oldTop = migratedTop();
const auto fromHistory = [&](not_null<History*> history, int historyTop)
-> std::pair<Element*, int> {
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<BotAbout>(
_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() {
2021-11-05 18:37:01 +01:00
if (!hasSelectRestriction()) {
applyDragSelection(&_selected);
}
}
bool HistoryInner::isSelected(
not_null<SelectedItems*> toItems,
not_null<HistoryItem*> item) const {
const auto i = toItems->find(item);
return (i != toItems->cend()) && (i->second == FullSelection);
}
bool HistoryInner::isSelectedGroup(
not_null<SelectedItems*> toItems,
not_null<const Data::Group*> group) const {
for (const auto &other : group->items) {
if (!isSelected(toItems, other)) {
return false;
}
}
return true;
}
bool HistoryInner::isSelectedAsGroup(
not_null<SelectedItems*> toItems,
not_null<HistoryItem*> item) const {
2019-07-24 13:13:51 +02:00
if (const auto group = session().data().groups().find(item)) {
return isSelectedGroup(toItems, group);
}
return isSelected(toItems, item);
}
bool HistoryInner::goodForSelection(
not_null<SelectedItems*> toItems,
not_null<HistoryItem*> 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<SelectedItems*> toItems,
not_null<HistoryItem*> 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<SelectedItems*> toItems,
not_null<HistoryItem*> item) const {
const auto i = toItems->find(item);
if (i != toItems->cend()) {
toItems->erase(i);
}
}
void HistoryInner::changeSelection(
not_null<SelectedItems*> toItems,
not_null<HistoryItem*> 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<SelectedItems*> toItems,
not_null<HistoryItem*> item,
SelectAction action) const {
2019-07-24 13:13:51 +02:00
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) {
2019-07-25 20:55:11 +02:00
Window::ShowForwardMessagesBox(_controller, { 1, itemId });
}
void HistoryInner::forwardAsGroup(FullMsgId itemId) {
2019-07-24 13:13:51 +02:00
if (const auto item = session().data().message(itemId)) {
2019-07-25 20:55:11 +02:00
Window::ShowForwardMessagesBox(
_controller,
session().data().itemOrItsGroup(item));
}
}
void HistoryInner::deleteItem(FullMsgId itemId) {
2019-07-24 13:13:51 +02:00
if (const auto item = session().data().message(itemId)) {
deleteItem(item);
}
}
void HistoryInner::deleteItem(not_null<HistoryItem*> item) {
if (item->isUploading()) {
_controller->cancelUploadLayer(item);
return;
}
const auto suggestModerateActions = true;
_controller->show(Box<DeleteMessagesBox>(item, suggestModerateActions));
}
bool HistoryInner::hasPendingResizedItems() const {
2019-01-18 09:11:15 +01:00
return _history->hasPendingResizedItems()
|| (_migrated && _migrated->hasPendingResizedItems());
}
void HistoryInner::deleteAsGroup(FullMsgId itemId) {
2019-07-24 13:13:51 +02:00
if (const auto item = session().data().message(itemId)) {
const auto group = session().data().groups().find(item);
2018-01-21 10:05:30 +01:00
if (!group) {
return deleteItem(item);
}
_controller->show(Box<DeleteMessagesBox>(
2019-07-24 16:00:30 +02:00
&session(),
2019-07-24 13:13:51 +02:00
session().data().itemsToIds(group->items)));
}
}
void HistoryInner::reportItem(FullMsgId itemId) {
_controller->show(ReportItemsBox(_peer, { 1, itemId }));
}
void HistoryInner::reportAsGroup(FullMsgId itemId) {
2019-07-24 13:13:51 +02:00
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<SelectedItems*> toItems,
not_null<History*> 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<SelectedItems*> 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();
}
2023-03-08 12:32:10 +01:00
const auto botAboutView = _botAbout ? _botAbout->view() : nullptr;
if (_dragSelecting) {
2023-03-08 12:32:10 +01:00
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<not_null<HistoryItem*>>();
2019-01-18 09:11:15 +01:00
for (const auto &item : *toItems) {
auto iy = itemTop(item.first);
if (iy < -1) {
2019-01-18 09:11:15 +01:00
toRemove.emplace_back(item.first);
} else if (iy >= 0 && iy >= selfromy && iy < seltoy) {
2019-01-18 09:11:15 +01:00
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<HistoryMessageForwarded>()) {
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);
2019-06-12 15:26:04 +02:00
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 {
2017-06-21 23:38:31 +02:00
return _mousePosition;
}
2019-09-16 13:14:06 +02:00
bool HistoryInner::tooltipWindowActive() const {
return Ui::AppInFocus() && Ui::InFocusChain(window());
2019-09-16 13:14:06 +02:00
}
void HistoryInner::onParentGeometryChanged() {
auto mousePos = QCursor::pos();
auto mouseOver = _widget->rect().contains(_widget->mapFromGlobal(mousePos));
2017-06-21 23:38:31 +02:00
auto needToUpdate = (_mouseAction != MouseAction::None || _touchScroll || mouseOver);
if (needToUpdate) {
2017-06-21 23:38:31 +02:00
mouseActionUpdate(mousePos);
}
}
Fn<HistoryView::ElementDelegate*()> HistoryInner::elementDelegateFactory(
FullMsgId itemId) const {
2022-10-19 12:59:37 +02:00
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),
2022-10-19 12:59:37 +02:00
.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<HistoryMainElementDelegateMixin> {
return std::make_unique<HistoryMainElementDelegate>();
}