Merge tag 'v4.11.3' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/info/profile/info_profile_values.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
ZavaruKitsu 2023-11-05 21:23:03 +03:00
commit ac1da83401
96 changed files with 1398 additions and 692 deletions

View file

@ -546,6 +546,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_workmode_tray" = "Show tray icon"; "lng_settings_workmode_tray" = "Show tray icon";
"lng_settings_workmode_window" = "Show taskbar icon"; "lng_settings_workmode_window" = "Show taskbar icon";
"lng_settings_close_to_taskbar" = "Close to taskbar"; "lng_settings_close_to_taskbar" = "Close to taskbar";
"lng_settings_monochrome_icon" = "Use monochrome icon";
"lng_settings_window_system" = "Window title"; "lng_settings_window_system" = "Window title";
"lng_settings_title_chat_name" = "Show chat name"; "lng_settings_title_chat_name" = "Show chat name";
"lng_settings_title_account_name" = "Show active account"; "lng_settings_title_account_name" = "Show active account";
@ -2684,6 +2685,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reply_options_quote" = "Update Quote"; "lng_reply_options_quote" = "Update Quote";
"lng_reply_header_short" = "Reply"; "lng_reply_header_short" = "Reply";
"lng_reply_quote_selected" = "Quote Selected"; "lng_reply_quote_selected" = "Quote Selected";
"lng_reply_from_private_chat" = "This reply is from a private chat.";
"lng_link_options_header" = "Link Preview Settings"; "lng_link_options_header" = "Link Preview Settings";
"lng_link_header_short" = "Link"; "lng_link_header_short" = "Link";
"lng_link_move_up" = "Move Up"; "lng_link_move_up" = "Move Up";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="4.11.1.0" /> Version="4.11.3.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,1,0 FILEVERSION 4,11,3,0
PRODUCTVERSION 4,11,1,0 PRODUCTVERSION 4,11,3,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "4.11.1.0" VALUE "FileVersion", "4.11.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.1.0" VALUE "ProductVersion", "4.11.3.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,1,0 FILEVERSION 4,11,3,0
PRODUCTVERSION 4,11,1,0 PRODUCTVERSION 4,11,3,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "4.11.1.0" VALUE "FileVersion", "4.11.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.1.0" VALUE "ProductVersion", "4.11.3.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -270,7 +270,6 @@ bool SendDice(MessageToSend &message) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
} }
const auto replyHeader = NewMessageReplyHeader(message.action);
const auto anonymousPost = peer->amAnonymous(); const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, message.action.options); const auto silentPost = ShouldSendSilent(peer, message.action.options);
InnerFillMessagePostFlags(message.action.options, peer, flags); InnerFillMessagePostFlags(message.action.options, peer, flags);
@ -399,7 +398,6 @@ void SendConfirmedFile(
if (file->to.replyTo) { if (file->to.replyTo) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
} }
const auto replyHeader = NewMessageReplyHeader(action);
const auto anonymousPost = peer->amAnonymous(); const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, file->to.options); const auto silentPost = ShouldSendSilent(peer, file->to.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);

View file

@ -3391,7 +3391,6 @@ void ApiWrap::sendSharedContact(
if (action.replyTo) { if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
} }
const auto replyHeader = NewMessageReplyHeader(action);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (action.options.scheduled) { if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled; flags |= MessageFlag::IsOrWasScheduled;
@ -3614,14 +3613,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
action.generateLocal = true; action.generateLocal = true;
sendAction(action); sendAction(action);
const auto replyTo = action.replyTo.messageId const auto clearCloudDraft = action.clearDraft;
? peer->owner().message(action.replyTo.messageId) const auto topicRootId = action.replyTo.topicRootId;
: nullptr;
const auto topicRootId = replyTo
? replyTo->topicRootId()
: action.replyTo.topicRootId
? action.replyTo.topicRootId
: Data::ForumTopic::kGeneralId;
const auto topic = peer->forumTopicFor(topicRootId); const auto topic = peer->forumTopicFor(topicRootId);
if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer))
|| Api::SendDice(message)) { || Api::SendDice(message)) {
@ -3674,7 +3667,6 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
const auto manualWebPage = exactWebPage const auto manualWebPage = exactWebPage
&& !ignoreWebPage && !ignoreWebPage
&& (message.webPage.manual || (isLast && !isFirst)); && (message.webPage.manual || (isLast && !isFirst));
const auto replyHeader = NewMessageReplyHeader(action);
MTPMessageMedia media = MTP_messageMediaEmpty(); MTPMessageMedia media = MTP_messageMediaEmpty();
if (ignoreWebPage) { if (ignoreWebPage) {
sendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage; sendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage;
@ -3718,8 +3710,6 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendFlags |= MTPmessages_SendMessage::Flag::f_entities; sendFlags |= MTPmessages_SendMessage::Flag::f_entities;
mediaFlags |= MTPmessages_SendMedia::Flag::f_entities; mediaFlags |= MTPmessages_SendMedia::Flag::f_entities;
} }
const auto clearCloudDraft = action.clearDraft;
const auto topicRootId = action.replyTo.topicRootId;
if (clearCloudDraft) { if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft; sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft;
mediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; mediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;

View file

@ -281,7 +281,7 @@ struct GiftCodeLink {
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue( [[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue(
not_null<QWidget*> parent, not_null<QWidget*> parent,
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
PeerId id) { PeerId id) {
auto result = object_ptr<Ui::AbstractButton>(parent); auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data(); const auto raw = result.data();
@ -309,7 +309,7 @@ struct GiftCodeLink {
label->setTextColorOverride(st::windowActiveTextFg->c); label->setTextColorOverride(st::windowActiveTextFg->c);
raw->setClickedCallback([=] { raw->setClickedCallback([=] {
controller->show(PrepareShortInfoBox(peer, controller)); controller->uiShow()->showBox(PrepareShortInfoBox(peer, controller));
}); });
return result; return result;
@ -350,7 +350,7 @@ not_null<Ui::FlatLabel*> AddTableRow(
void AddTableRow( void AddTableRow(
not_null<Ui::TableLayout*> table, not_null<Ui::TableLayout*> table,
rpl::producer<QString> label, rpl::producer<QString> label,
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
PeerId id) { PeerId id) {
if (!id) { if (!id) {
return; return;
@ -416,7 +416,7 @@ QString GiftDuration(int months) {
void GiftCodeBox( void GiftCodeBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
const QString &slug) { const QString &slug) {
struct State { struct State {
rpl::variable<Api::GiftCode> data; rpl::variable<Api::GiftCode> data;
@ -552,7 +552,7 @@ void GiftCodeBox(
st::giveawayGiftCodeFooterMargin); st::giveawayGiftCodeFooterMargin);
footer->setClickHandlerFilter([=](const auto &...) { footer->setClickHandlerFilter([=](const auto &...) {
const auto chosen = [=](not_null<Data::Thread*> thread) { const auto chosen = [=](not_null<Data::Thread*> thread) {
const auto content = controller->content(); const auto content = controller->parentController()->content();
return content->shareUrl( return content->shareUrl(
thread, thread,
MakeGiftCodeLink(&controller->session(), slug).link, MakeGiftCodeLink(&controller->session(), slug).link,
@ -608,13 +608,13 @@ void GiftCodeBox(
} }
void ResolveGiftCode( void ResolveGiftCode(
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
const QString &slug) { const QString &slug) {
const auto done = [=](Api::GiftCode code) { const auto done = [=](Api::GiftCode code) {
if (!code) { if (!code) {
controller->showToast(tr::lng_gift_link_expired(tr::now)); controller->showToast(tr::lng_gift_link_expired(tr::now));
} else { } else {
controller->show(Box(GiftCodeBox, controller, slug)); controller->uiShow()->showBox(Box(GiftCodeBox, controller, slug));
} }
}; };
controller->session().api().premium().checkGiftCode( controller->session().api().premium().checkGiftCode(
@ -624,7 +624,7 @@ void ResolveGiftCode(
void GiveawayInfoBox( void GiveawayInfoBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
Data::Giveaway giveaway, Data::Giveaway giveaway,
Api::GiveawayInfo info) { Api::GiveawayInfo info) {
using State = Api::GiveawayState; using State = Api::GiveawayState;
@ -784,7 +784,7 @@ void GiveawayInfoBox(
} }
void ResolveGiveawayInfo( void ResolveGiveawayInfo(
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId messageId, MsgId messageId,
Data::Giveaway giveaway) { Data::Giveaway giveaway) {
@ -793,7 +793,7 @@ void ResolveGiveawayInfo(
controller->showToast( controller->showToast(
tr::lng_confirm_phone_link_invalid(tr::now)); tr::lng_confirm_phone_link_invalid(tr::now));
} else { } else {
controller->show( controller->uiShow()->showBox(
Box(GiveawayInfoBox, controller, giveaway, info)); Box(GiveawayInfoBox, controller, giveaway, info));
} }
}; };

View file

@ -25,6 +25,7 @@ class GenericBox;
namespace Window { namespace Window {
class SessionController; class SessionController;
class SessionNavigation;
} // namespace Window } // namespace Window
class GiftPremiumValidator final { class GiftPremiumValidator final {
@ -47,14 +48,14 @@ private:
void GiftCodeBox( void GiftCodeBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
const QString &slug); const QString &slug);
void ResolveGiftCode( void ResolveGiftCode(
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
const QString &slug); const QString &slug);
void ResolveGiveawayInfo( void ResolveGiveawayInfo(
not_null<Window::SessionController*> controller, not_null<Window::SessionNavigation*> controller,
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId messageId, MsgId messageId,
Data::Giveaway giveaway); Data::Giveaway giveaway);

View file

@ -760,7 +760,7 @@ int PeerListRow::paintNameIconGetWidth(
nameWidth, nameWidth,
outerWidth, outerWidth,
{ {
.peer = _peer, .peer = peer(),
.verified = &(selected .verified = &(selected
? st::dialogsVerifiedIconOver ? st::dialogsVerifiedIconOver
: st::dialogsVerifiedIcon), : st::dialogsVerifiedIcon),

View file

@ -712,13 +712,24 @@ void Instance::destroyCurrentCall() {
} }
} }
bool Instance::hasActivePanel(not_null<Main::Session*> session) const { bool Instance::hasVisiblePanel(Main::Session *session) const {
if (inCall()) { if (inCall()) {
return (&_currentCall->user()->session() == session) return _currentCallPanel->isVisible()
&& _currentCallPanel->isActive(); && (!session || (&_currentCall->user()->session() == session));
} else if (inGroupCall()) { } else if (inGroupCall()) {
return (&_currentGroupCall->peer()->session() == session) return _currentGroupCallPanel->isVisible()
&& _currentGroupCallPanel->isActive(); && (!session || (&_currentGroupCall->peer()->session() == session));
}
return false;
}
bool Instance::hasActivePanel(Main::Session *session) const {
if (inCall()) {
return _currentCallPanel->isActive()
&& (!session || (&_currentCall->user()->session() == session));
} else if (inGroupCall()) {
return _currentGroupCallPanel->isActive()
&& (!session || (&_currentGroupCall->peer()->session() == session));
} }
return false; return false;
} }

View file

@ -89,8 +89,10 @@ public:
[[nodiscard]] rpl::producer<GroupCall*> currentGroupCallValue() const; [[nodiscard]] rpl::producer<GroupCall*> currentGroupCallValue() const;
[[nodiscard]] bool inCall() const; [[nodiscard]] bool inCall() const;
[[nodiscard]] bool inGroupCall() const; [[nodiscard]] bool inGroupCall() const;
[[nodiscard]] bool hasVisiblePanel(
Main::Session *session = nullptr) const;
[[nodiscard]] bool hasActivePanel( [[nodiscard]] bool hasActivePanel(
not_null<Main::Session*> session) const; Main::Session *session = nullptr) const;
bool activateCurrentCall(const QString &joinHash = QString()); bool activateCurrentCall(const QString &joinHash = QString());
bool minimizeCurrentActiveCall(); bool minimizeCurrentActiveCall();
bool toggleFullScreenCurrentActiveCall(); bool toggleFullScreenCurrentActiveCall();

View file

@ -106,12 +106,15 @@ Panel::Panel(not_null<Call*> call)
Panel::~Panel() = default; Panel::~Panel() = default;
bool Panel::isActive() const { bool Panel::isVisible() const {
return window()->isActiveWindow() return window()->isVisible()
&& window()->isVisible()
&& !(window()->windowState() & Qt::WindowMinimized); && !(window()->windowState() & Qt::WindowMinimized);
} }
bool Panel::isActive() const {
return window()->isActiveWindow() && isVisible();
}
void Panel::showAndActivate() { void Panel::showAndActivate() {
if (window()->isHidden()) { if (window()->isHidden()) {
window()->show(); window()->show();

View file

@ -61,6 +61,7 @@ public:
Panel(not_null<Call*> call); Panel(not_null<Call*> call);
~Panel(); ~Panel();
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const; [[nodiscard]] bool isActive() const;
void showAndActivate(); void showAndActivate();
void minimize(); void minimize();

View file

@ -258,12 +258,15 @@ not_null<GroupCall*> Panel::call() const {
return _call; return _call;
} }
bool Panel::isActive() const { bool Panel::isVisible() const {
return window()->isActiveWindow() return window()->isVisible()
&& window()->isVisible()
&& !(window()->windowState() & Qt::WindowMinimized); && !(window()->windowState() & Qt::WindowMinimized);
} }
bool Panel::isActive() const {
return window()->isActiveWindow() && isVisible();
}
base::weak_ptr<Ui::Toast::Instance> Panel::showToast( base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
const QString &text, const QString &text,
crl::time duration) { crl::time duration) {

View file

@ -91,6 +91,7 @@ public:
[[nodiscard]] not_null<Ui::RpWidget*> widget() const; [[nodiscard]] not_null<Ui::RpWidget*> widget() const;
[[nodiscard]] not_null<GroupCall*> call() const; [[nodiscard]] not_null<GroupCall*> call() const;
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const; [[nodiscard]] bool isActive() const;
base::weak_ptr<Ui::Toast::Instance> showToast( base::weak_ptr<Ui::Toast::Instance> showToast(

View file

@ -493,10 +493,7 @@ InlineBotQuery ParseInlineBotQuery(
result.lookingUpBot = true; result.lookingUpBot = true;
} }
} }
if (result.lookingUpBot) { if (result.bot
result.query = QString();
return result;
} else if (result.bot
&& (!result.bot->isBot() && (!result.bot->isBot()
|| result.bot->botInfo->inlinePlaceholder.isEmpty())) { || result.bot->botInfo->inlinePlaceholder.isEmpty())) {
result.bot = nullptr; result.bot = nullptr;

View file

@ -348,6 +348,8 @@ QByteArray Settings::serialize() const {
for (const auto &id : _recentEmojiSkip) { for (const auto &id : _recentEmojiSkip) {
stream << id; stream << id;
} }
stream
<< qint32(_trayIconMonochrome.current() ? 1 : 0);
} }
return result; return result;
} }
@ -457,6 +459,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
quint64 macRoundIconDigest = _macRoundIconDigest.value_or(0); quint64 macRoundIconDigest = _macRoundIconDigest.value_or(0);
qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0; qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0;
base::flat_set<QString> recentEmojiSkip; base::flat_set<QString> recentEmojiSkip;
qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);
stream >> themesAccentColors; stream >> themesAccentColors;
if (!stream.atEnd()) { if (!stream.atEnd()) {
@ -707,6 +710,12 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
} }
} }
} }
if (!stream.atEnd()) {
stream >> trayIconMonochrome;
} else {
// Let existing clients use the old value.
trayIconMonochrome = 0;
}
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
LOG(("App Error: " LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()")); "Bad data for Core::Settings::constructFromSerialized()"));
@ -901,6 +910,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_macRoundIconDigest = macRoundIconDigest ? macRoundIconDigest : std::optional<uint64>(); _macRoundIconDigest = macRoundIconDigest ? macRoundIconDigest : std::optional<uint64>();
_storiesClickTooltipHidden = (storiesClickTooltipHidden == 1); _storiesClickTooltipHidden = (storiesClickTooltipHidden == 1);
_recentEmojiSkip = std::move(recentEmojiSkip); _recentEmojiSkip = std::move(recentEmojiSkip);
_trayIconMonochrome = (trayIconMonochrome == 1);
} }
QString Settings::getSoundPath(const QString &key) const { QString Settings::getSoundPath(const QString &key) const {

View file

@ -708,6 +708,15 @@ public:
[[nodiscard]] rpl::producer<bool> closeToTaskbarChanges() const { [[nodiscard]] rpl::producer<bool> closeToTaskbarChanges() const {
return _closeToTaskbar.changes(); return _closeToTaskbar.changes();
} }
void setTrayIconMonochrome(bool value) {
_trayIconMonochrome = value;
}
[[nodiscard]] bool trayIconMonochrome() const {
return _trayIconMonochrome.current();
}
[[nodiscard]] rpl::producer<bool> trayIconMonochromeChanges() const {
return _trayIconMonochrome.changes();
}
void setCustomDeviceModel(const QString &model) { void setCustomDeviceModel(const QString &model) {
_customDeviceModel = model; _customDeviceModel = model;
@ -924,6 +933,7 @@ private:
rpl::variable<WorkMode> _workMode = WorkMode::WindowAndTray; rpl::variable<WorkMode> _workMode = WorkMode::WindowAndTray;
base::flags<Calls::Group::StickedTooltip> _hiddenGroupCallTooltips; base::flags<Calls::Group::StickedTooltip> _hiddenGroupCallTooltips;
rpl::variable<bool> _closeToTaskbar = false; rpl::variable<bool> _closeToTaskbar = false;
rpl::variable<bool> _trayIconMonochrome = true;
rpl::variable<QString> _customDeviceModel; rpl::variable<QString> _customDeviceModel;
rpl::variable<Media::RepeatMode> _playerRepeatMode; rpl::variable<Media::RepeatMode> _playerRepeatMode;
rpl::variable<Media::OrderMode> _playerOrderMode; rpl::variable<Media::OrderMode> _playerOrderMode;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4011001; constexpr auto AppVersion = 4011003;
constexpr auto AppVersionStr = "4.11.1"; constexpr auto AppVersionStr = "4.11.3";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/notifications_manager.h" #include "window/notifications_manager.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "core/application.h" #include "core/application.h"
#include "apiwrap.h" #include "apiwrap.h"
@ -52,8 +53,18 @@ MTPInputReplyTo ReplyToForMTP(
} }
} }
} else if (replyTo.messageId || replyTo.topicRootId) { } else if (replyTo.messageId || replyTo.topicRootId) {
const auto to = LookupReplyTo(history, replyTo.messageId);
const auto replyingToTopicId = replyTo.topicRootId
? replyTo.topicRootId
: Data::ForumTopic::kGeneralId;
const auto replyToTopicId = !to
? replyingToTopicId
: to->topicRootId()
? to->topicRootId()
: Data::ForumTopic::kGeneralId;
const auto external = replyTo.messageId const auto external = replyTo.messageId
&& (replyTo.messageId.peer != history->peer->id); && (replyTo.messageId.peer != history->peer->id
|| replyingToTopicId != replyToTopicId);
const auto quoteEntities = Api::EntitiesToMTP( const auto quoteEntities = Api::EntitiesToMTP(
&history->session(), &history->session(),
replyTo.quote.entities, replyTo.quote.entities,

View file

@ -538,6 +538,7 @@ ItemPreview Media::toGroupPreview(
auto videoCount = 0; auto videoCount = 0;
auto audioCount = 0; auto audioCount = 0;
auto fileCount = 0; auto fileCount = 0;
auto manyCaptions = false;
for (const auto &item : items) { for (const auto &item : items) {
if (const auto media = item->media()) { if (const auto media = item->media()) {
if (media->photo()) { if (media->photo()) {
@ -571,12 +572,12 @@ ItemPreview Media::toGroupPreview(
if (result.text.text.isEmpty()) { if (result.text.text.isEmpty()) {
result.text = original; result.text = original;
} else { } else {
result.text = {}; manyCaptions = true;
} }
} }
} }
} }
if (result.text.text.isEmpty()) { if (manyCaptions || result.text.text.isEmpty()) {
const auto mediaCount = photoCount + videoCount; const auto mediaCount = photoCount + videoCount;
auto genericText = (photoCount && videoCount) auto genericText = (photoCount && videoCount)
? tr::lng_in_dlg_media_count(tr::now, lt_count, mediaCount) ? tr::lng_in_dlg_media_count(tr::now, lt_count, mediaCount)

View file

@ -579,11 +579,6 @@ bool InnerWidget::elementUnderCursor(
return (Element::Hovered() == view); return (Element::Hovered() == view);
} }
float64 InnerWidget::elementHighlightOpacity(
not_null<const HistoryItem*> item) const {
return 0.;
}
bool InnerWidget::elementInSelectionMode() { bool InnerWidget::elementInSelectionMode() {
return false; return false;
} }

View file

@ -93,8 +93,6 @@ public:
HistoryView::Context elementContext() override; HistoryView::Context elementContext() override;
bool elementUnderCursor( bool elementUnderCursor(
not_null<const HistoryView::Element*> view) override; not_null<const HistoryView::Element*> view) override;
[[nodiscard]] float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const override;
bool elementInSelectionMode() override; bool elementInSelectionMode() override;
bool elementIntersectsRange( bool elementIntersectsRange(
not_null<const HistoryView::Element*> view, not_null<const HistoryView::Element*> view,

View file

@ -207,10 +207,6 @@ public:
not_null<const Element*> view) override { not_null<const Element*> view) override {
return (Element::Moused() == view); return (Element::Moused() == view);
} }
[[nodiscard]] float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const override {
return _widget ? _widget->elementHighlightOpacity(item) : 0.;
}
bool elementInSelectionMode() override { bool elementInSelectionMode() override {
return _widget ? _widget->inSelectionMode() : false; return _widget ? _widget->inSelectionMode() : false;
} }
@ -1065,6 +1061,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
auto clip = e->rect(); auto clip = e->rect();
auto context = preparePaintContext(clip); auto context = preparePaintContext(clip);
context.highlightPathCache = &_highlightPathCache;
_pathGradient->startFrame( _pathGradient->startFrame(
0, 0,
width(), width(),
@ -1148,7 +1145,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
} else if (item->isUnreadMention() } else if (item->isUnreadMention()
&& !item->isUnreadMedia()) { && !item->isUnreadMedia()) {
readContents.insert(item); readContents.insert(item);
_widget->enqueueMessageHighlight(view); _widget->enqueueMessageHighlight(view, {});
} }
} }
session().data().reactions().poll(item, context.now); session().data().reactions().poll(item, context.now);
@ -1190,6 +1187,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view, view,
selfromy - mtop, selfromy - mtop,
seltoy - mtop); seltoy - mtop);
context.highlight = _widget->itemHighlight(view->data());
view->draw(p, context); view->draw(p, context);
processPainted(view, top, height); processPainted(view, top, height);
@ -1224,9 +1222,10 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
const auto &sendingAnimation = _controller->sendingAnimation(); const auto &sendingAnimation = _controller->sendingAnimation();
while (top < drawToY) { while (top < drawToY) {
const auto height = view->height(); const auto height = view->height();
const auto item = view->data();
if ((context.clip.y() < height) if ((context.clip.y() < height)
&& (hdrawtop < top + height) && (hdrawtop < top + height)
&& !sendingAnimation.hasAnimatedMessage(view->data())) { && !sendingAnimation.hasAnimatedMessage(item)) {
context.reactionInfo context.reactionInfo
= _reactionsManager->currentReactionPaintInfo(); = _reactionsManager->currentReactionPaintInfo();
context.outbg = view->hasOutLayout(); context.outbg = view->hasOutLayout();
@ -1234,6 +1233,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view, view,
selfromy - htop, selfromy - htop,
seltoy - htop); seltoy - htop);
context.highlight = _widget->itemHighlight(item);
view->draw(p, context); view->draw(p, context);
processPainted(view, top, height); processPainted(view, top, height);
} }
@ -1678,7 +1678,10 @@ void HistoryInner::mouseActionStart(const QPoint &screenPos, Qt::MouseButton but
Ui::MarkInactivePress(_controller->widget(), false); Ui::MarkInactivePress(_controller->widget(), false);
} }
if (ClickHandler::getPressed()) { const auto pressed = ClickHandler::getPressed();
if (pressed
&& (!Element::Hovered()
|| !Element::Hovered()->allowTextSelectionByHandler(pressed))) {
_mouseAction = MouseAction::PrepareDrag; _mouseAction = MouseAction::PrepareDrag;
} else if (inSelectionMode()) { } else if (inSelectionMode()) {
if (_dragStateItem if (_dragStateItem
@ -2111,7 +2114,7 @@ void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
} }
TextWithEntities HistoryInner::selectedQuote( HistoryView::SelectedQuote HistoryInner::selectedQuote(
not_null<HistoryItem*> item) const { not_null<HistoryItem*> item) const {
if (_selected.size() != 1 if (_selected.size() != 1
|| _selected.begin()->first != item || _selected.begin()->first != item
@ -2401,25 +2404,22 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
: (Data::CanSendAnything(peer) : (Data::CanSendAnything(peer)
&& (!peer->isChannel() || peer->asChannel()->amIn())); && (!peer->isChannel() || peer->asChannel()->amIn()));
}(); }();
const auto canReply = canSendReply || [&] { const auto canReply = canSendReply || item->allowsForward();
const auto peer = item->history()->peer;
if (const auto chat = peer->asChat()) {
return !chat->isForbidden();
} else if (const auto channel = peer->asChannel()) {
return !channel->isForbidden();
}
return true;
}();
if (canReply) { if (canReply) {
const auto itemId = item->fullId(); const auto selected = selectedQuote(item);
const auto quote = selectedQuote(item); auto text = selected
auto text = quote.empty() ? tr::lng_context_quote_and_reply(tr::now)
? tr::lng_context_reply_msg(tr::now) : tr::lng_context_reply_msg(tr::now);
: tr::lng_context_quote_and_reply(tr::now); const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
text.replace('&', u"&&"_q); text.replace('&', u"&&"_q);
_menu->addAction(text, [=] { _menu->addAction(text, [=] {
if (canSendReply) { if (canSendReply) {
_widget->replyToMessage({ itemId, quote }); _widget->replyToMessage({ itemId, quote });
if (!quote.empty()) {
_widget->clearSelected();
}
} else { } else {
HistoryView::Controls::ShowReplyToChatBox( HistoryView::Controls::ShowReplyToChatBox(
controller->uiShow(), controller->uiShow(),
@ -3484,11 +3484,6 @@ void HistoryInner::elementStartStickerLoop(
_animatedStickersPlayed.emplace(view->data()); _animatedStickersPlayed.emplace(view->data());
} }
float64 HistoryInner::elementHighlightOpacity(
not_null<const HistoryItem*> item) const {
return _widget->highlightOpacity(item);
}
void HistoryInner::elementShowPollResults( void HistoryInner::elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context) { FullMsgId context) {
@ -3855,6 +3850,10 @@ void HistoryInner::mouseActionUpdate() {
selState = view->adjustSelection(selState, _mouseSelectType); selState = view->adjustSelection(selState, _mouseSelectType);
} }
} }
if (!selState.empty()) {
// We started selecting text in web page preview.
ClickHandler::unpressed();
}
if (_selected[_mouseActionItem] != selState) { if (_selected[_mouseActionItem] != selState) {
_selected[_mouseActionItem] = selState; _selected[_mouseActionItem] = selState;
repaintItem(_mouseActionItem); repaintItem(_mouseActionItem);

View file

@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/scroll_area.h" #include "ui/widgets/scroll_area.h"
#include "history/view/history_view_top_bar_widget.h" #include "history/view/history_view_top_bar_widget.h"
#include <QtGui/QPainterPath>
struct ClickContext; struct ClickContext;
struct ClickHandlerContext; struct ClickHandlerContext;
@ -33,6 +35,7 @@ class EmptyPainter;
class Element; class Element;
class TranslateTracker; class TranslateTracker;
struct PinnedId; struct PinnedId;
struct SelectedQuote;
} // namespace HistoryView } // namespace HistoryView
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
@ -136,8 +139,6 @@ public:
int from, int from,
int till) const; int till) const;
void elementStartStickerLoop(not_null<const Element*> view); void elementStartStickerLoop(not_null<const Element*> view);
[[nodiscard]] float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const;
void elementShowPollResults( void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context); FullMsgId context);
@ -314,7 +315,7 @@ private:
QPoint mapPointToItem(QPoint p, const Element *view) const; QPoint mapPointToItem(QPoint p, const Element *view) const;
QPoint mapPointToItem(QPoint p, const HistoryItem *item) const; QPoint mapPointToItem(QPoint p, const HistoryItem *item) const;
[[nodiscard]] TextWithEntities selectedQuote( [[nodiscard]] HistoryView::SelectedQuote selectedQuote(
not_null<HistoryItem*> item) const; not_null<HistoryItem*> item) const;
void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);
@ -462,6 +463,7 @@ private:
std::optional<Ui::ReportReason> _chooseForReportReason; std::optional<Ui::ReportReason> _chooseForReportReason;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient; const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
QPainterPath _highlightPathCache;
bool _isChatWide = false; bool _isChatWide = false;
base::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed; base::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed;

View file

@ -470,6 +470,14 @@ HistoryItem::HistoryItem(
const auto dropForwardInfo = original->computeDropForwardedInfo(); const auto dropForwardInfo = original->computeDropForwardedInfo();
config.reply.messageId = config.reply.topMessageId = topicRootId; config.reply.messageId = config.reply.topMessageId = topicRootId;
config.reply.topicPost = (topicRootId != 0); config.reply.topicPost = (topicRootId != 0);
if (const auto originalReply = original->Get<HistoryMessageReply>()) {
if (originalReply->external()) {
config.reply = originalReply->fields();
if (!config.reply.externalPeerId) {
config.reply.messageId = 0;
}
}
}
if (!dropForwardInfo) { if (!dropForwardInfo) {
config.originalDate = original->originalDate(); config.originalDate = original->originalDate();
if (const auto info = original->hiddenSenderInfo()) { if (const auto info = original->hiddenSenderInfo()) {
@ -3389,16 +3397,28 @@ void HistoryItem::createComponentsHelper(
? replyTo.messageId.peer ? replyTo.messageId.peer
: PeerId(); : PeerId();
const auto to = LookupReplyTo(_history, replyTo.messageId); const auto to = LookupReplyTo(_history, replyTo.messageId);
const auto replyToTop = LookupReplyToTop(_history, to); const auto replyToTop = replyTo.topicRootId
? replyTo.topicRootId
: LookupReplyToTop(_history, to);
config.reply.topMessageId = replyToTop config.reply.topMessageId = replyToTop
? replyToTop ? replyToTop
: (replyTo.messageId.peer == history()->peer->id) : (replyTo.messageId.peer == history()->peer->id)
? replyTo.messageId.msg ? replyTo.messageId.msg
: MsgId(); : MsgId();
if (!config.reply.externalPeerId
&& to
&& config.reply.topicPost
&& replyTo.topicRootId != to->topicRootId()) {
config.reply.externalPeerId = replyTo.messageId.peer;
}
const auto forum = _history->asForum(); const auto forum = _history->asForum();
config.reply.topicPost = LookupReplyIsTopicPost(to) config.reply.topicPost = config.reply.externalPeerId
|| (to && to->Has<HistoryServiceTopicInfo>()) ? (replyTo.topicRootId
|| (forum && forum->creating(config.reply.topMessageId)); && (replyTo.topicRootId != Data::ForumTopic::kGeneralId))
: (LookupReplyIsTopicPost(to)
|| (to && to->Has<HistoryServiceTopicInfo>())
|| (forum && forum->creating(config.reply.topMessageId)));
config.reply.manualQuote = !replyTo.quote.empty();
config.reply.quote = std::move(replyTo.quote); config.reply.quote = std::move(replyTo.quote);
} }
config.markup = std::move(markup); config.markup = std::move(markup);

View file

@ -397,9 +397,6 @@ ReplyFields ReplyFieldsFromMTP(
auto result = ReplyFields(); auto result = ReplyFields();
if (const auto peer = data.vreply_to_peer_id()) { if (const auto peer = data.vreply_to_peer_id()) {
result.externalPeerId = peerFromMTP(*peer); result.externalPeerId = peerFromMTP(*peer);
if (result.externalPeerId == history->peer->id) {
result.externalPeerId = 0;
}
} }
const auto owner = &history->owner(); const auto owner = &history->owner();
if (const auto id = data.vreply_to_msg_id().value_or_empty()) { if (const auto id = data.vreply_to_msg_id().value_or_empty()) {
@ -426,6 +423,7 @@ ReplyFields ReplyFieldsFromMTP(
&owner->session(), &owner->session(),
data.vquote_entities().value_or_empty()), data.vquote_entities().value_or_empty()),
}; };
result.manualQuote = data.is_quote();
return result; return result;
}, [&](const MTPDmessageReplyStoryHeader &data) { }, [&](const MTPDmessageReplyStoryHeader &data) {
return ReplyFields{ return ReplyFields{
@ -525,8 +523,7 @@ bool HistoryMessageReply::updateData(
} }
} }
const auto external = _fields.externalSenderId const auto external = this->external();
|| !_fields.externalSenderName.isEmpty();
if (resolvedMessage if (resolvedMessage
|| resolvedStory || resolvedStory
|| (external && (!_fields.messageId || force))) { || (external && (!_fields.messageId || force))) {
@ -624,13 +621,15 @@ void HistoryMessageReply::setLinkFrom(
if (externalPeerId) { if (externalPeerId) {
controller->showPeerInfo( controller->showPeerInfo(
controller->session().data().peer(externalPeerId)); controller->session().data().peer(externalPeerId));
} else {
controller->showToast(u"External reply"_q);
} }
controller->showToast(tr::lng_reply_from_private_chat(tr::now));
} }
}; };
_link = resolvedMessage _link = resolvedMessage
? JumpToMessageClickHandler(resolvedMessage.get(), holder->fullId()) ? JumpToMessageClickHandler(
resolvedMessage.get(),
holder->fullId(),
_fields.manualQuote ? _fields.quote : TextWithEntities())
: resolvedStory : resolvedStory
? JumpToStoryClickHandler(resolvedStory.get()) ? JumpToStoryClickHandler(resolvedStory.get())
: (external && !_fields.messageId) : (external && !_fields.messageId)
@ -656,10 +655,18 @@ void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {
resolvedStory.get()); resolvedStory.get());
resolvedStory = nullptr; resolvedStory = nullptr;
} }
_name.clear();
_text.clear();
_unavailable = 1; _unavailable = 1;
refreshReplyToMedia(); refreshReplyToMedia();
} }
bool HistoryMessageReply::external() const {
return _fields.externalPeerId
|| _fields.externalSenderId
|| !_fields.externalSenderName.isEmpty();
}
PeerData *HistoryMessageReply::sender(not_null<HistoryItem*> holder) const { PeerData *HistoryMessageReply::sender(not_null<HistoryItem*> holder) const {
if (resolvedStory) { if (resolvedStory) {
return resolvedStory->peer(); return resolvedStory->peer();
@ -832,7 +839,7 @@ void HistoryMessageReply::paint(
y += st::historyReplyTop; y += st::historyReplyTop;
const auto rect = QRect(x, y, w, _height); const auto rect = QRect(x, y, w, _height);
const auto hasQuote = !_fields.quote.empty(); const auto hasQuote = _fields.manualQuote && !_fields.quote.empty();
const auto selected = context.selected(); const auto selected = context.selected();
const auto colorPeer = resolvedMessage const auto colorPeer = resolvedMessage
? resolvedMessage->displayFrom() ? resolvedMessage->displayFrom()

View file

@ -239,6 +239,7 @@ struct ReplyFields {
MsgId topMessageId = 0; MsgId topMessageId = 0;
StoryId storyId = 0; StoryId storyId = 0;
bool topicPost = false; bool topicPost = false;
bool manualQuote = false;
}; };
[[nodiscard]] ReplyFields ReplyFieldsFromMTP( [[nodiscard]] ReplyFields ReplyFieldsFromMTP(
@ -273,6 +274,7 @@ struct HistoryMessageReply
// Must be called before destructor. // Must be called before destructor.
void clearData(not_null<HistoryItem*> holder); void clearData(not_null<HistoryItem*> holder);
[[nodiscard]] bool external() const;
[[nodiscard]] PeerData *sender(not_null<HistoryItem*> holder) const; [[nodiscard]] PeerData *sender(not_null<HistoryItem*> holder) const;
[[nodiscard]] QString senderName(not_null<HistoryItem*> holder) const; [[nodiscard]] QString senderName(not_null<HistoryItem*> holder) const;
[[nodiscard]] QString senderName(not_null<PeerData*> peer) const; [[nodiscard]] QString senderName(not_null<PeerData*> peer) const;
@ -300,6 +302,9 @@ struct HistoryMessageReply
bool inBubble) const; bool inBubble) const;
void unloadPersistentAnimation(); void unloadPersistentAnimation();
[[nodiscard]] ReplyFields fields() const {
return _fields;
}
[[nodiscard]] PeerId externalPeerId() const { [[nodiscard]] PeerId externalPeerId() const {
return _fields.externalPeerId; return _fields.externalPeerId;
} }
@ -321,6 +326,9 @@ struct HistoryMessageReply
[[nodiscard]] bool topicPost() const { [[nodiscard]] bool topicPost() const {
return _fields.topicPost; return _fields.topicPost;
} }
[[nodiscard]] bool manualQuote() const {
return _fields.manualQuote;
}
[[nodiscard]] QString statePhrase() const; [[nodiscard]] QString statePhrase() const;
void setLinkFrom(not_null<HistoryItem*> holder); void setLinkFrom(not_null<HistoryItem*> holder);

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "history/history_item_helpers.h" #include "history/history_item_helpers.h"
#include "api/api_text_entities.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
#include "data/notify/data_notify_settings.h" #include "data/notify/data_notify_settings.h"
#include "data/data_chat_participant_status.h" #include "data/data_chat_participant_status.h"
@ -202,6 +203,14 @@ MsgId LookupReplyToTop(not_null<History*> history, HistoryItem *replyTo) {
: 0; : 0;
} }
MsgId LookupReplyToTop(not_null<History*> history, FullReplyTo replyTo) {
return replyTo.topicRootId
? replyTo.topicRootId
: LookupReplyToTop(
history,
LookupReplyTo(history, replyTo.messageId));
}
bool LookupReplyIsTopicPost(HistoryItem *replyTo) { bool LookupReplyIsTopicPost(HistoryItem *replyTo) {
return replyTo return replyTo
&& (replyTo->topicRootId() != Data::ForumTopic::kGeneralId); && (replyTo->topicRootId() != Data::ForumTopic::kGeneralId);
@ -259,17 +268,20 @@ bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId) { FullMsgId returnToId,
TextWithEntities highlightPart) {
return JumpToMessageClickHandler( return JumpToMessageClickHandler(
item->history()->peer, item->history()->peer,
item->id, item->id,
returnToId); returnToId,
std::move(highlightPart));
} }
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId) { FullMsgId returnToId,
TextWithEntities highlightPart) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=] {
const auto separate = Core::App().separateWindowForPeer(peer); const auto separate = Core::App().separateWindowForPeer(peer);
const auto controller = separate const auto controller = separate
@ -279,6 +291,7 @@ ClickHandlerPtr JumpToMessageClickHandler(
auto params = Window::SectionShow{ auto params = Window::SectionShow{
Window::SectionShow::Way::Forward Window::SectionShow::Way::Forward
}; };
params.highlightPart = highlightPart;
params.origin = Window::SectionShow::OriginMessage{ params.origin = Window::SectionShow::OriginMessage{
returnToId returnToId
}; };
@ -369,19 +382,27 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
const auto externalPeerId = (replyTo.messageId.peer == historyPeer) const auto externalPeerId = (replyTo.messageId.peer == historyPeer)
? PeerId() ? PeerId()
: replyTo.messageId.peer; : replyTo.messageId.peer;
const auto to = LookupReplyTo(action.history, replyTo.messageId); const auto replyToTop = LookupReplyToTop(action.history, replyTo);
const auto replyToTop = LookupReplyToTop(action.history, to); auto quoteEntities = Api::EntitiesToMTP(
&action.history->session(),
replyTo.quote.entities,
Api::ConvertOption::SkipLocal);
return MTP_messageReplyHeader( return MTP_messageReplyHeader(
MTP_flags(Flag::f_reply_to_msg_id MTP_flags(Flag::f_reply_to_msg_id
| (replyToTop ? Flag::f_reply_to_top_id : Flag()) | (replyToTop ? Flag::f_reply_to_top_id : Flag())
| (externalPeerId ? Flag::f_reply_to_peer_id : Flag())), | (externalPeerId ? Flag::f_reply_to_peer_id : Flag())
| (replyTo.quote.empty() ? Flag() : Flag::f_quote)
| (replyTo.quote.empty() ? Flag() : Flag::f_quote_text)
| (quoteEntities.v.empty()
? Flag()
: Flag::f_quote_entities)),
MTP_int(replyTo.messageId.msg), MTP_int(replyTo.messageId.msg),
peerToMTP(externalPeerId), peerToMTP(externalPeerId),
MTPMessageFwdHeader(), // reply_from MTPMessageFwdHeader(), // reply_from
MTPMessageMedia(), // reply_media MTPMessageMedia(), // reply_media
MTP_int(replyToTop), // reply_to_top_id MTP_int(replyToTop),
MTPstring(), // quote_text MTP_string(replyTo.quote.text),
MTPVector<MTPMessageEntity>()); // quote_entities quoteEntities);
} }
return MTPMessageReplyHeader(); return MTPMessageReplyHeader();
} }

View file

@ -89,6 +89,9 @@ void RequestDependentMessageStory(
[[nodiscard]] MsgId LookupReplyToTop( [[nodiscard]] MsgId LookupReplyToTop(
not_null<History*> history, not_null<History*> history,
HistoryItem *replyTo); HistoryItem *replyTo);
[[nodiscard]] MsgId LookupReplyToTop(
not_null<History*> history,
FullReplyTo replyTo);
[[nodiscard]] bool LookupReplyIsTopicPost(HistoryItem *replyTo); [[nodiscard]] bool LookupReplyIsTopicPost(HistoryItem *replyTo);
struct SendingErrorRequest { struct SendingErrorRequest {
@ -120,10 +123,12 @@ struct SendingErrorRequest {
[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId = FullMsgId()); FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {});
[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId = FullMsgId()); FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {});
[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( [[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(
not_null<Data::Story*> story); not_null<Data::Story*> story);
ClickHandlerPtr JumpToStoryClickHandler( ClickHandlerPtr JumpToStoryClickHandler(

View file

@ -8,14 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_view_highlight_manager.h" #include "history/history_view_highlight_manager.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "ui/chat/chat_style.h"
namespace HistoryView { namespace HistoryView {
constexpr auto kAnimationFirstPart = st::activeFadeInDuration
/ float64(st::activeFadeInDuration + st::activeFadeOutDuration);
ElementHighlighter::ElementHighlighter( ElementHighlighter::ElementHighlighter(
not_null<Data::Session*> data, not_null<Data::Session*> data,
ViewForItem viewForItem, ViewForItem viewForItem,
@ -26,63 +25,95 @@ ElementHighlighter::ElementHighlighter(
, _animation(*this) { , _animation(*this) {
} }
void ElementHighlighter::enqueue(not_null<Element*> view) { void ElementHighlighter::enqueue(
const auto item = view->data(); not_null<Element*> view,
const auto fullId = item->fullId(); const TextWithEntities &part) {
const auto data = computeHighlight(view, part);
if (_queue.empty() && !_animation.animating()) { if (_queue.empty() && !_animation.animating()) {
highlight(fullId); highlight(data);
} else if (_highlightedMessageId != fullId } else if (_highlighted != data && !base::contains(_queue, data)) {
&& !base::contains(_queue, fullId)) { _queue.push_back(data);
_queue.push_back(fullId);
checkNextHighlight(); checkNextHighlight();
} }
} }
void ElementHighlighter::highlight(
not_null<Element*> view,
const TextWithEntities &part) {
highlight(computeHighlight(view, part));
}
void ElementHighlighter::checkNextHighlight() { void ElementHighlighter::checkNextHighlight() {
if (_animation.animating()) { if (_animation.animating()) {
return; return;
} }
const auto nextHighlight = [&] { const auto next = [&] {
while (!_queue.empty()) { while (!_queue.empty()) {
const auto fullId = _queue.front(); const auto highlight = _queue.front();
_queue.pop_front(); _queue.pop_front();
if (const auto item = _data->message(fullId)) { if (const auto item = _data->message(highlight.itemId)) {
if (_viewForItem(item)) { if (_viewForItem(item)) {
return fullId; return highlight;
} }
} }
} }
return FullMsgId(); return Highlight();
}(); }();
if (!nextHighlight) { if (next) {
return; highlight(next);
} }
highlight(nextHighlight);
} }
float64 ElementHighlighter::progress( Ui::ChatPaintHighlight ElementHighlighter::state(
not_null<const HistoryItem*> item) const { not_null<const HistoryItem*> item) const {
if (item->fullId() == _highlightedMessageId) { if (item->fullId() == _highlighted.itemId) {
const auto progress = _animation.progress(); auto result = _animation.state();
return std::min(progress / kAnimationFirstPart, 1.) result.range = _highlighted.part;
- ((progress - kAnimationFirstPart) / (1. - kAnimationFirstPart)); return result;
} }
return 0.; return {};
} }
void ElementHighlighter::highlight(FullMsgId itemId) { ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
if (const auto item = _data->message(itemId)) { not_null<const Element*> view,
const TextWithEntities &part) {
const auto item = view->data();
const auto owner = &item->history()->owner();
if (const auto group = owner->groups().find(item)) {
const auto leader = group->items.front();
const auto leaderId = leader->fullId();
const auto i = ranges::find(group->items, item);
if (i != end(group->items)) {
const auto index = int(i - begin(group->items));
if (part.empty()) {
return { leaderId, AddGroupItemSelection({}, index) };
} else if (const auto leaderView = _viewForItem(leader)) {
return {
leaderId,
leaderView->selectionFromQuote(item, part),
};
}
}
return { leaderId };
} else if (part.empty()) {
return { item->fullId() };
}
return { item->fullId(), view->selectionFromQuote(item, part) };
}
void ElementHighlighter::highlight(Highlight data) {
if (const auto item = _data->message(data.itemId)) {
if (const auto view = _viewForItem(item)) { if (const auto view = _viewForItem(item)) {
if (_highlightedMessageId if (_highlighted && _highlighted.itemId != data.itemId) {
&& _highlightedMessageId != itemId) { if (const auto was = _data->message(_highlighted.itemId)) {
if (const auto was = _data->message(_highlightedMessageId)) {
if (const auto view = _viewForItem(was)) { if (const auto view = _viewForItem(was)) {
repaintHighlightedItem(view); repaintHighlightedItem(view);
} }
} }
} }
_highlightedMessageId = itemId; _highlighted = data;
_animation.start(); _animation.start(!data.part.empty()
&& !IsSubGroupSelection(data.part));
repaintHighlightedItem(view); repaintHighlightedItem(view);
} }
@ -105,7 +136,7 @@ void ElementHighlighter::repaintHighlightedItem(
} }
void ElementHighlighter::updateMessage() { void ElementHighlighter::updateMessage() {
if (const auto item = _data->message(_highlightedMessageId)) { if (const auto item = _data->message(_highlighted.itemId)) {
if (const auto view = _viewForItem(item)) { if (const auto view = _viewForItem(item)) {
repaintHighlightedItem(view); repaintHighlightedItem(view);
} }
@ -114,7 +145,7 @@ void ElementHighlighter::updateMessage() {
void ElementHighlighter::clear() { void ElementHighlighter::clear() {
_animation.cancel(); _animation.cancel();
_highlightedMessageId = FullMsgId(); _highlighted = {};
_lastHighlightedMessageId = FullMsgId(); _lastHighlightedMessageId = FullMsgId();
_queue.clear(); _queue.clear();
} }
@ -125,60 +156,117 @@ ElementHighlighter::AnimationManager::AnimationManager(
} }
bool ElementHighlighter::AnimationManager::animating() const { bool ElementHighlighter::AnimationManager::animating() const {
if (anim::Disabled()) { if (_timer && _timer->isActive()) {
return (_timer && _timer->isActive()); return true;
} else { } else if (!anim::Disabled()) {
return _simple.animating(); return _simple.animating();
} }
return false;
} }
float64 ElementHighlighter::AnimationManager::progress() const { Ui::ChatPaintHighlight ElementHighlighter::AnimationManager::state() const {
if (anim::Disabled()) { if (anim::Disabled()) {
return (_timer && _timer->isActive()) ? kAnimationFirstPart : 0.; return {
} else { .opacity = !_timer ? 0. : 1.,
return _simple.value(0.); .collapsion = !_timer ? 0. : _fadingOut ? 1. : 0.,
};
} }
return {
.opacity = ((!_fadingOut && _collapsing)
? 1.
: _simple.value(_fadingOut ? 0. : 1.)),
.collapsion = ((!_withTextPart || !_collapsing)
? 0.
: _fadingOut
? 1.
: _simple.value(1.)),
};
} }
MsgId ElementHighlighter::latestSingleHighlightedMsgId() const { MsgId ElementHighlighter::latestSingleHighlightedMsgId() const {
return _highlightedMessageId return _highlighted.itemId
? _highlightedMessageId.msg ? _highlighted.itemId.msg
: _lastHighlightedMessageId.msg; : _lastHighlightedMessageId.msg;
} }
void ElementHighlighter::AnimationManager::start() { void ElementHighlighter::AnimationManager::start(bool withTextPart) {
_withTextPart = withTextPart;
const auto finish = [=] { const auto finish = [=] {
cancel(); cancel();
_parent._lastHighlightedMessageId = base::take( _parent._lastHighlightedMessageId = base::take(
_parent._highlightedMessageId); _parent._highlighted.itemId);
_parent.checkNextHighlight(); _parent.checkNextHighlight();
}; };
cancel(); cancel();
if (anim::Disabled()) { if (anim::Disabled()) {
_timer.emplace([=] { _timer.emplace([=] {
_parent.updateMessage(); _parent.updateMessage();
finish(); if (_withTextPart && !_fadingOut) {
_fadingOut = true;
_timer->callOnce(st::activeFadeOutDuration);
} else {
finish();
}
}); });
_timer->callOnce(st::activeFadeOutDuration); _timer->callOnce(_withTextPart
? st::activeFadeInDuration
: st::activeFadeOutDuration);
_parent.updateMessage(); _parent.updateMessage();
} else { } else {
const auto to = 1.;
_simple.start( _simple.start(
[=](float64 value) { [=](float64 value) {
_parent.updateMessage(); _parent.updateMessage();
if (value == to) { if (value == 1.) {
finish(); if (_withTextPart) {
_timer.emplace([=] {
_parent.updateMessage();
if (_collapsing) {
_fadingOut = true;
} else {
_collapsing = true;
}
_simple.start([=](float64 value) {
_parent.updateMessage();
if (_fadingOut && value == 0.) {
finish();
} else if (!_fadingOut && value == 1.) {
_timer->callOnce(
st::activeFadeOutDuration);
}
},
_fadingOut ? 1. : 0.,
_fadingOut ? 0. : 1.,
(_fadingOut
? st::activeFadeInDuration
: st::fadeWrapDuration));
});
_timer->callOnce(st::activeFadeInDuration);
} else {
_fadingOut = true;
_simple.start([=](float64 value) {
_parent.updateMessage();
if (value == 0.) {
finish();
}
},
1.,
0.,
st::activeFadeOutDuration);
}
} }
}, },
0., 0.,
to, 1.,
st::activeFadeInDuration + st::activeFadeOutDuration); st::activeFadeInDuration);
} }
} }
void ElementHighlighter::AnimationManager::cancel() { void ElementHighlighter::AnimationManager::cancel() {
_simple.stop(); _simple.stop();
_timer.reset(); _timer.reset();
_fadingOut = false;
_collapsed = false;
_collapsing = false;
} }
} // namespace HistoryView } // namespace HistoryView

View file

@ -16,6 +16,10 @@ namespace Data {
class Session; class Session;
} // namespace Data } // namespace Data
namespace Ui {
struct ChatPaintHighlight;
} // namespace Ui
namespace HistoryView { namespace HistoryView {
class Element; class Element;
@ -29,40 +33,59 @@ public:
ViewForItem viewForItem, ViewForItem viewForItem,
RepaintView repaintView); RepaintView repaintView);
void enqueue(not_null<Element*> view); void enqueue(not_null<Element*> view, const TextWithEntities &part);
void highlight(FullMsgId itemId); void highlight(not_null<Element*> view, const TextWithEntities &part);
void clear(); void clear();
[[nodiscard]] float64 progress(not_null<const HistoryItem*> item) const; [[nodiscard]] Ui::ChatPaintHighlight state(
not_null<const HistoryItem*> item) const;
[[nodiscard]] MsgId latestSingleHighlightedMsgId() const; [[nodiscard]] MsgId latestSingleHighlightedMsgId() const;
private: private:
void checkNextHighlight();
void repaintHighlightedItem(not_null<const Element*> view);
void updateMessage();
class AnimationManager final { class AnimationManager final {
public: public:
AnimationManager(ElementHighlighter &parent); AnimationManager(ElementHighlighter &parent);
[[nodiscard]] bool animating() const; [[nodiscard]] bool animating() const;
[[nodiscard]] float64 progress() const; [[nodiscard]] Ui::ChatPaintHighlight state() const;
void start(); void start(bool withTextPart);
void cancel(); void cancel();
private: private:
ElementHighlighter &_parent; ElementHighlighter &_parent;
Ui::Animations::Simple _simple; Ui::Animations::Simple _simple;
std::optional<base::Timer> _timer; std::optional<base::Timer> _timer;
bool _withTextPart = false;
bool _collapsing = false;
bool _collapsed = false;
bool _fadingOut = false;
}; };
struct Highlight {
FullMsgId itemId;
TextSelection part;
explicit operator bool() const {
return itemId.operator bool();
}
friend inline bool operator==(Highlight, Highlight) = default;
};
[[nodiscard]] Highlight computeHighlight(
not_null<const Element*> view,
const TextWithEntities &part);
void highlight(Highlight data);
void checkNextHighlight();
void repaintHighlightedItem(not_null<const Element*> view);
void updateMessage();
const not_null<Data::Session*> _data; const not_null<Data::Session*> _data;
const ViewForItem _viewForItem; const ViewForItem _viewForItem;
const RepaintView _repaintView; const RepaintView _repaintView;
FullMsgId _highlightedMessageId; Highlight _highlighted;
FullMsgId _lastHighlightedMessageId; FullMsgId _lastHighlightedMessageId;
std::deque<FullMsgId> _queue; std::deque<Highlight> _queue;
AnimationManager _animation; AnimationManager _animation;

View file

@ -847,7 +847,9 @@ HistoryWidget::HistoryWidget(
}); });
} else { } else {
fastShowAtEnd(action.history); fastShowAtEnd(action.history);
if (cancelReply(lastKeyboardUsed) && !action.clearDraft) { if (!_justMarkingAsRead
&& cancelReply(lastKeyboardUsed)
&& !action.clearDraft) {
saveCloudDraft(); saveCloudDraft();
} }
} }
@ -1077,7 +1079,7 @@ void HistoryWidget::initTabbedSelector() {
if (!data.recipientOverride) { if (!data.recipientOverride) {
return true; return true;
} else if (data.recipientOverride != _peer) { } else if (data.recipientOverride != _peer) {
showHistory(data.recipientOverride->id, ShowAtTheEndMsgId); showHistory(data.recipientOverride->id, ShowAtTheEndMsgId, {});
} }
return (data.recipientOverride == _peer); return (data.recipientOverride == _peer);
}) | rpl::start_with_next([=](ChatHelpers::InlineChosen data) { }) | rpl::start_with_next([=](ChatHelpers::InlineChosen data) {
@ -1272,13 +1274,14 @@ void HistoryWidget::scrollToAnimationCallback(
} }
void HistoryWidget::enqueueMessageHighlight( void HistoryWidget::enqueueMessageHighlight(
not_null<HistoryView::Element*> view) { not_null<HistoryView::Element*> view,
_highlighter.enqueue(view); const TextWithEntities &part) {
_highlighter.enqueue(view, part);
} }
float64 HistoryWidget::highlightOpacity( Ui::ChatPaintHighlight HistoryWidget::itemHighlight(
not_null<const HistoryItem*> item) const { not_null<const HistoryItem*> item) const {
return _highlighter.progress(item); return _highlighter.state(item);
} }
int HistoryWidget::itemTopForHighlight( int HistoryWidget::itemTopForHighlight(
@ -1395,9 +1398,7 @@ void HistoryWidget::updateInlineBotQuery() {
_inlineBotResolveRequestId = _api.request(MTPcontacts_ResolveUsername( _inlineBotResolveRequestId = _api.request(MTPcontacts_ResolveUsername(
MTP_string(username) MTP_string(username)
)).done([=](const MTPcontacts_ResolvedPeer &result) { )).done([=](const MTPcontacts_ResolvedPeer &result) {
Expects(result.type() == mtpc_contacts_resolvedPeer); const auto &data = result.data();
const auto &data = result.c_contacts_resolvedPeer();
const auto resolvedBot = [&]() -> UserData* { const auto resolvedBot = [&]() -> UserData* {
if (const auto user = session().data().processUsers( if (const auto user = session().data().processUsers(
data.vusers())) { data.vusers())) {
@ -1976,9 +1977,10 @@ bool HistoryWidget::insideJumpToEndInsteadOfToUnread() const {
void HistoryWidget::showHistory( void HistoryWidget::showHistory(
const PeerId &peerId, const PeerId &peerId,
MsgId showAtMsgId, MsgId showAtMsgId,
bool reload) { const TextWithEntities &highlightPart) {
_pinnedClickedId = FullMsgId(); _pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt; _minPinnedId = std::nullopt;
_showAtMsgHighlightPart = {};
const auto wasDialogsEntryState = computeDialogsEntryState(); const auto wasDialogsEntryState = computeDialogsEntryState();
const auto startBot = (showAtMsgId == ShowAndStartBotMsgId); const auto startBot = (showAtMsgId == ShowAndStartBotMsgId);
@ -1990,7 +1992,7 @@ void HistoryWidget::showHistory(
controller()->sendingAnimation().clear(); controller()->sendingAnimation().clear();
_topToast.hide(anim::type::instant); _topToast.hide(anim::type::instant);
if (_history) { if (_history) {
if (_peer->id == peerId && !reload) { if (_peer->id == peerId) {
updateForwarding(); updateForwarding();
if (showAtMsgId == ShowAtUnreadMsgId if (showAtMsgId == ShowAtUnreadMsgId
@ -2026,10 +2028,10 @@ void HistoryWidget::showHistory(
).arg(_history->inboxReadTillId().bare ).arg(_history->inboxReadTillId().bare
).arg(Logs::b(_history->loadedAtBottom()) ).arg(Logs::b(_history->loadedAtBottom())
).arg(showAtMsgId.bare)); ).arg(showAtMsgId.bare));
delayedShowAt(showAtMsgId); delayedShowAt(showAtMsgId, highlightPart);
} else if (_showAtMsgId != showAtMsgId) { } else if (_showAtMsgId != showAtMsgId) {
clearAllLoadRequests(); clearAllLoadRequests();
setMsgId(showAtMsgId); setMsgId(showAtMsgId, highlightPart);
firstLoadMessages(); firstLoadMessages();
doneShow(); doneShow();
} }
@ -2049,7 +2051,7 @@ void HistoryWidget::showHistory(
_cornerButtons.skipReplyReturn(skipId); _cornerButtons.skipReplyReturn(skipId);
} }
setMsgId(showAtMsgId); setMsgId(showAtMsgId, highlightPart);
if (_historyInited) { if (_historyInited) {
DEBUG_LOG(("JumpToEnd(%1, %2, %3): " DEBUG_LOG(("JumpToEnd(%1, %2, %3): "
"Showing instant at %4." "Showing instant at %4."
@ -2152,6 +2154,7 @@ void HistoryWidget::showHistory(
clearInlineBot(); clearInlineBot();
_showAtMsgId = showAtMsgId; _showAtMsgId = showAtMsgId;
_showAtMsgHighlightPart = highlightPart;
_historyInited = false; _historyInited = false;
_contactStatus = nullptr; _contactStatus = nullptr;
@ -3306,7 +3309,7 @@ void HistoryWidget::messagesReceived(
} }
_delayedShowAtRequest = 0; _delayedShowAtRequest = 0;
setMsgId(_delayedShowAtMsgId); setMsgId(_delayedShowAtMsgId, _delayedShowAtMsgHighlightPart);
historyLoaded(); historyLoaded();
} }
if (session().supportMode()) { if (session().supportMode()) {
@ -3528,9 +3531,16 @@ void HistoryWidget::loadMessagesDown() {
}); });
} }
void HistoryWidget::delayedShowAt(MsgId showAtMsgId) { void HistoryWidget::delayedShowAt(
if (!_history MsgId showAtMsgId,
|| (_delayedShowAtRequest && _delayedShowAtMsgId == showAtMsgId)) { const TextWithEntities &highlightPart) {
if (!_history) {
return;
}
if (_delayedShowAtMsgHighlightPart != highlightPart) {
_delayedShowAtMsgHighlightPart = highlightPart;
}
if (_delayedShowAtRequest && _delayedShowAtMsgId == showAtMsgId) {
return; return;
} }
@ -3974,7 +3984,12 @@ void HistoryWidget::send(Api::SendOptions options) {
ignoreSlowmodeCountdown)) { ignoreSlowmodeCountdown)) {
return; return;
} }
// Just a flag not to drop reply info if we're not sending anything.
_justMarkingAsRead = !HasSendText(_field)
&& message.webPage.url.isEmpty();
session().api().sendMessage(std::move(message)); session().api().sendMessage(std::move(message));
_justMarkingAsRead = false;
clearFieldText(); clearFieldText();
if (_preview) { if (_preview) {
@ -4123,7 +4138,12 @@ PeerData *HistoryWidget::peer() const {
} }
// Sometimes _showAtMsgId is set directly. // Sometimes _showAtMsgId is set directly.
void HistoryWidget::setMsgId(MsgId showAtMsgId) { void HistoryWidget::setMsgId(
MsgId showAtMsgId,
const TextWithEntities &highlightPart) {
if (_showAtMsgHighlightPart != highlightPart) {
_showAtMsgHighlightPart = highlightPart;
}
if (_showAtMsgId != showAtMsgId) { if (_showAtMsgId != showAtMsgId) {
_showAtMsgId = showAtMsgId; _showAtMsgId = showAtMsgId;
if (_history) { if (_history) {
@ -4244,11 +4264,11 @@ void HistoryWidget::cornerButtonsShowAtPosition(
).arg(_history->peer->name() ).arg(_history->peer->name()
).arg(_history->inboxReadTillId().bare ).arg(_history->inboxReadTillId().bare
).arg(Logs::b(_history->loadedAtBottom()))); ).arg(Logs::b(_history->loadedAtBottom())));
showHistory(_peer->id, ShowAtUnreadMsgId); showHistory(_peer->id, ShowAtUnreadMsgId, {});
} else if (_peer && position.fullId.peer == _peer->id) { } else if (_peer && position.fullId.peer == _peer->id) {
showHistory(_peer->id, position.fullId.msg); showHistory(_peer->id, position.fullId.msg, {});
} else if (_migrated && position.fullId.peer == _migrated->peer->id) { } else if (_migrated && position.fullId.peer == _migrated->peer->id) {
showHistory(_peer->id, -position.fullId.msg); showHistory(_peer->id, -position.fullId.msg, {});
} }
} }
@ -5197,7 +5217,7 @@ void HistoryWidget::updateFieldPlaceholder() {
if (!_editMsgId && _inlineBot && !_inlineLookingUpBot) { if (!_editMsgId && _inlineBot && !_inlineLookingUpBot) {
_field->setPlaceholder( _field->setPlaceholder(
rpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)), rpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)),
_inlineBot->username().size() + 2); _inlineBotUsername.size() + 2);
return; return;
} }
@ -5699,14 +5719,16 @@ int HistoryWidget::countInitialScrollTop() {
const auto item = getItemFromHistoryOrMigrated(_showAtMsgId); const auto item = getItemFromHistoryOrMigrated(_showAtMsgId);
const auto itemTop = _list->itemTop(item); const auto itemTop = _list->itemTop(item);
if (itemTop < 0) { if (itemTop < 0) {
setMsgId(0); setMsgId(ShowAtUnreadMsgId);
controller()->showToast(tr::lng_message_not_found(tr::now)); controller()->showToast(tr::lng_message_not_found(tr::now));
return countInitialScrollTop(); return countInitialScrollTop();
} else { } else {
const auto view = item->mainView(); const auto view = item->mainView();
Assert(view != nullptr); Assert(view != nullptr);
enqueueMessageHighlight(view); enqueueMessageHighlight(
view,
base::take(_showAtMsgHighlightPart));
const auto result = itemTopForHighlight(view); const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result); createUnreadBarIfBelowVisibleArea(result);
return result; return result;
@ -6278,6 +6300,8 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) {
} else { } else {
_forwardPanel->editOptions(controller()->uiShow()); _forwardPanel->editOptions(controller()->uiShow());
} }
} else if (_replyTo && (e->modifiers() & Qt::ControlModifier)) {
jumpToReply(_replyTo);
} else if (_replyTo) { } else if (_replyTo) {
editDraftOptions(); editDraftOptions();
} else if (_kbReplyTo) { } else if (_kbReplyTo) {
@ -6306,12 +6330,9 @@ void HistoryWidget::editDraftOptions() {
_preview->apply(webpage); _preview->apply(webpage);
}; };
const auto replyToId = reply.messageId; const auto replyToId = reply.messageId;
const auto highlight = [=] { const auto highlight = crl::guard(this, [=](FullReplyTo to) {
controller()->showPeerHistory( jumpToReply(to);
replyToId.peer, });
Window::SectionShow::Way::Forward,
replyToId.msg);
};
using namespace HistoryView::Controls; using namespace HistoryView::Controls;
EditDraftOptions({ EditDraftOptions({
@ -6323,10 +6344,16 @@ void HistoryWidget::editDraftOptions() {
.resolver = _preview->resolver(), .resolver = _preview->resolver(),
.done = done, .done = done,
.highlight = highlight, .highlight = highlight,
.clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); }, .clearOldDraft = [=] { ClearDraftReplyTo(history, 0, replyToId); },
}); });
} }
void HistoryWidget::jumpToReply(FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler(item, {}, to.quote)->onClick({});
}
}
void HistoryWidget::keyPressEvent(QKeyEvent *e) { void HistoryWidget::keyPressEvent(QKeyEvent *e) {
if (!_history) return; if (!_history) return;
@ -6398,7 +6425,8 @@ void HistoryWidget::handlePeerMigration() {
if (_peer != channel) { if (_peer != channel) {
showHistory( showHistory(
channel->id, channel->id,
(_showAtMsgId > 0) ? (-_showAtMsgId) : _showAtMsgId); (_showAtMsgId > 0) ? (-_showAtMsgId) : _showAtMsgId,
{});
channel->session().api().chatParticipants().requestCountDelayed( channel->session().api().chatParticipants().requestCountDelayed(
channel); channel);
} else { } else {
@ -6494,7 +6522,7 @@ bool HistoryWidget::showSlowmodeError() {
if (const auto item = _history->latestSendingMessage()) { if (const auto item = _history->latestSendingMessage()) {
if (const auto view = item->mainView()) { if (const auto view = item->mainView()) {
animatedScrollToItem(item->id); animatedScrollToItem(item->id);
enqueueMessageHighlight(view); enqueueMessageHighlight(view, {});
} }
return tr::lng_slowmode_no_many(tr::now); return tr::lng_slowmode_no_many(tr::now);
} }

View file

@ -75,6 +75,7 @@ class SpoilerAnimation;
enum class ReportReason; enum class ReportReason;
class ChooseThemeController; class ChooseThemeController;
class ContinuousScroll; class ContinuousScroll;
struct ChatPaintHighlight;
} // namespace Ui } // namespace Ui
namespace Window { namespace Window {
@ -146,7 +147,9 @@ public:
void loadMessages(); void loadMessages();
void loadMessagesDown(); void loadMessagesDown();
void firstLoadMessages(); void firstLoadMessages();
void delayedShowAt(MsgId showAtMsgId); void delayedShowAt(
MsgId showAtMsgId,
const TextWithEntities &highlightPart);
bool updateReplaceMediaButton(); bool updateReplaceMediaButton();
void updateFieldPlaceholder(); void updateFieldPlaceholder();
@ -160,7 +163,9 @@ public:
History *history() const; History *history() const;
PeerData *peer() const; PeerData *peer() const;
void setMsgId(MsgId showAtMsgId); void setMsgId(
MsgId showAtMsgId,
const TextWithEntities &highlightPart = {});
MsgId msgId() const; MsgId msgId() const;
bool hasTopBarShadow() const { bool hasTopBarShadow() const {
@ -177,8 +182,10 @@ public:
bool touchScroll(const QPoint &delta); bool touchScroll(const QPoint &delta);
void enqueueMessageHighlight(not_null<HistoryView::Element*> view); void enqueueMessageHighlight(
[[nodiscard]] float64 highlightOpacity( not_null<HistoryView::Element*> view,
const TextWithEntities &part);
[[nodiscard]] Ui::ChatPaintHighlight itemHighlight(
not_null<const HistoryItem*> item) const; not_null<const HistoryItem*> item) const;
MessageIdsList getSelectedItems() const; MessageIdsList getSelectedItems() const;
@ -218,7 +225,10 @@ public:
void fastShowAtEnd(not_null<History*> history); void fastShowAtEnd(not_null<History*> history);
bool applyDraft( bool applyDraft(
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void showHistory(const PeerId &peer, MsgId showAtMsgId, bool reload = false); void showHistory(
const PeerId &peer,
MsgId showAtMsgId,
const TextWithEntities &highlightPart);
void setChooseReportMessagesDetails( void setChooseReportMessagesDetails(
Ui::ReportReason reason, Ui::ReportReason reason,
Fn<void(MessageIdsList)> callback); Fn<void(MessageIdsList)> callback);
@ -540,6 +550,7 @@ private:
void setupPreview(); void setupPreview();
void editDraftOptions(); void editDraftOptions();
void jumpToReply(FullReplyTo to);
void messagesReceived(not_null<PeerData*> peer, const MTPmessages_Messages &messages, int requestId); void messagesReceived(not_null<PeerData*> peer, const MTPmessages_Messages &messages, int requestId);
void messagesFailed(const MTP::Error &error, int requestId); void messagesFailed(const MTP::Error &error, int requestId);
@ -684,12 +695,14 @@ private:
bool _canSendMessages = false; bool _canSendMessages = false;
bool _canSendTexts = false; bool _canSendTexts = false;
MsgId _showAtMsgId = ShowAtUnreadMsgId; MsgId _showAtMsgId = ShowAtUnreadMsgId;
TextWithEntities _showAtMsgHighlightPart;
int _firstLoadRequest = 0; // Not real mtpRequestId. int _firstLoadRequest = 0; // Not real mtpRequestId.
int _preloadRequest = 0; // Not real mtpRequestId. int _preloadRequest = 0; // Not real mtpRequestId.
int _preloadDownRequest = 0; // Not real mtpRequestId. int _preloadDownRequest = 0; // Not real mtpRequestId.
MsgId _delayedShowAtMsgId = -1; MsgId _delayedShowAtMsgId = -1;
TextWithEntities _delayedShowAtMsgHighlightPart;
int _delayedShowAtRequest = 0; // Not real mtpRequestId. int _delayedShowAtRequest = 0; // Not real mtpRequestId.
History *_supportPreloadHistory = nullptr; History *_supportPreloadHistory = nullptr;
@ -800,6 +813,7 @@ private:
int _itemsRevealHeight = 0; int _itemsRevealHeight = 0;
bool _sponsoredMessagesStateKnown = false; bool _sponsoredMessagesStateKnown = false;
bool _justMarkingAsRead = false;
object_ptr<Ui::PlainShadow> _topShadow; object_ptr<Ui::PlainShadow> _topShadow;
bool _inGrab = false; bool _inGrab = false;

View file

@ -112,6 +112,7 @@ public:
std::shared_ptr<ChatHelpers::Show> show); std::shared_ptr<ChatHelpers::Show> show);
void setHistory(const SetHistoryArgs &args); void setHistory(const SetHistoryArgs &args);
void updateTopicRootId(MsgId topicRootId);
void init(); void init();
void editMessage(FullMsgId id, bool photoEditAllowed = false); void editMessage(FullMsgId id, bool photoEditAllowed = false);
@ -129,7 +130,7 @@ public:
[[nodiscard]] FullReplyTo replyingToMessage() const; [[nodiscard]] FullReplyTo replyingToMessage() const;
[[nodiscard]] FullMsgId editMsgId() const; [[nodiscard]] FullMsgId editMsgId() const;
[[nodiscard]] rpl::producer<FullMsgId> editMsgIdValue() const; [[nodiscard]] rpl::producer<FullMsgId> editMsgIdValue() const;
[[nodiscard]] rpl::producer<FullMsgId> scrollToItemRequests() const; [[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;
[[nodiscard]] rpl::producer<> editPhotoRequests() const; [[nodiscard]] rpl::producer<> editPhotoRequests() const;
[[nodiscard]] rpl::producer<> editOptionsRequests() const; [[nodiscard]] rpl::producer<> editOptionsRequests() const;
[[nodiscard]] MessageToEdit queryToEdit(); [[nodiscard]] MessageToEdit queryToEdit();
@ -205,7 +206,7 @@ private:
QRect _shownMessagePreviewRect; QRect _shownMessagePreviewRect;
rpl::event_stream<bool> _visibleChanged; rpl::event_stream<bool> _visibleChanged;
rpl::event_stream<FullMsgId> _scrollToItemRequests; rpl::event_stream<FullReplyTo> _jumpToItemRequests;
rpl::event_stream<> _editOptionsRequests; rpl::event_stream<> _editOptionsRequests;
rpl::event_stream<> _editPhotoRequests; rpl::event_stream<> _editPhotoRequests;
@ -229,6 +230,10 @@ void FieldHeader::setHistory(const SetHistoryArgs &args) {
_topicRootId = args.topicRootId; _topicRootId = args.topicRootId;
} }
void FieldHeader::updateTopicRootId(MsgId topicRootId) {
_topicRootId = topicRootId;
}
void FieldHeader::init() { void FieldHeader::init() {
sizeValue( sizeValue(
) | rpl::start_with_next([=](QSize size) { ) | rpl::start_with_next([=](QSize size) {
@ -367,9 +372,14 @@ void FieldHeader::init() {
if (_preview.parsed) { if (_preview.parsed) {
_editOptionsRequests.fire({}); _editOptionsRequests.fire({});
} else if (isEditingMessage()) { } else if (isEditingMessage()) {
_scrollToItemRequests.fire(_editMsgId.current()); _jumpToItemRequests.fire(FullReplyTo{
.messageId = _editMsgId.current()
});
} else if (readyToForward()) { } else if (readyToForward()) {
_forwardPanel->editOptions(_show); _forwardPanel->editOptions(_show);
} else if (reply
&& (e->modifiers() & Qt::ControlModifier)) {
_jumpToItemRequests.fire_copy(reply);
} else if (reply) { } else if (reply) {
_editOptionsRequests.fire({}); _editOptionsRequests.fire({});
} }
@ -724,8 +734,8 @@ rpl::producer<FullMsgId> FieldHeader::editMsgIdValue() const {
return _editMsgId.value(); return _editMsgId.value();
} }
rpl::producer<FullMsgId> FieldHeader::scrollToItemRequests() const { rpl::producer<FullReplyTo> FieldHeader::jumpToItemRequests() const {
return _scrollToItemRequests.events(); return _jumpToItemRequests.events();
} }
rpl::producer<> FieldHeader::editPhotoRequests() const { rpl::producer<> FieldHeader::editPhotoRequests() const {
@ -855,6 +865,11 @@ Main::Session &ComposeControls::session() const {
return _show->session(); return _show->session();
} }
void ComposeControls::updateTopicRootId(MsgId topicRootId) {
_topicRootId = topicRootId;
_header->updateTopicRootId(_topicRootId);
}
void ComposeControls::setHistory(SetHistoryArgs &&args) { void ComposeControls::setHistory(SetHistoryArgs &&args) {
_showSlowmodeError = std::move(args.showSlowmodeError); _showSlowmodeError = std::move(args.showSlowmodeError);
_sendActionFactory = std::move(args.sendActionFactory); _sendActionFactory = std::move(args.sendActionFactory);
@ -1332,6 +1347,7 @@ void ComposeControls::init() {
_header->editOptionsRequests( _header->editOptionsRequests(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
const auto history = _history; const auto history = _history;
const auto topicRootId = _topicRootId;
const auto reply = _header->replyingToMessage(); const auto reply = _header->replyingToMessage();
const auto webpage = _preview->draft(); const auto webpage = _preview->draft();
@ -1344,10 +1360,11 @@ void ComposeControls::init() {
cancelReplyMessage(); cancelReplyMessage();
} }
_preview->apply(webpage); _preview->apply(webpage);
_field->setFocus();
}; };
const auto replyToId = reply.messageId; const auto replyToId = reply.messageId;
const auto highlight = crl::guard(_wrap.get(), [=] { const auto highlight = crl::guard(_wrap.get(), [=](FullReplyTo to) {
_scrollToItemRequests.fire_copy(replyToId); _jumpToItemRequests.fire_copy(to);
}); });
using namespace HistoryView::Controls; using namespace HistoryView::Controls;
@ -1360,7 +1377,10 @@ void ComposeControls::init() {
.resolver = _preview->resolver(), .resolver = _preview->resolver(),
.done = done, .done = done,
.highlight = highlight, .highlight = highlight,
.clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); }, .clearOldDraft = [=] { ClearDraftReplyTo(
history,
topicRootId,
replyToId); },
}); });
}, _wrap->lifetime()); }, _wrap->lifetime());
@ -1920,6 +1940,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
_header->replyToMessage({}); _header->replyToMessage({});
if (_preview) { if (_preview) {
_preview->apply({ .removed = true }); _preview->apply({ .removed = true });
_preview->setDisabled(false);
} }
_canReplaceMedia = false; _canReplaceMedia = false;
_photoEditMedia = nullptr; _photoEditMedia = nullptr;
@ -1958,6 +1979,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
_preview->apply( _preview->apply(
Data::WebPageDraft::FromItem(item), Data::WebPageDraft::FromItem(item),
false); false);
_preview->setDisabled(media && !media->webpage());
} }
return true; return true;
} }
@ -1989,6 +2011,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
cancelForward(); cancelForward();
} }
_header->editMessage({}); _header->editMessage({});
if (_preview) {
_preview->setDisabled(false);
}
} }
} }
@ -2873,16 +2898,10 @@ Data::WebPageDraft ComposeControls::webPageDraft() const {
return _preview ? _preview->draft() : Data::WebPageDraft(); return _preview ? _preview->draft() : Data::WebPageDraft();
} }
rpl::producer<Data::MessagePosition> ComposeControls::scrollRequests() const { rpl::producer<FullReplyTo> ComposeControls::jumpToItemRequests() const {
return rpl::merge( return rpl::merge(
_header->scrollToItemRequests(), _header->jumpToItemRequests(),
_scrollToItemRequests.events() _jumpToItemRequests.events());
) | rpl::map([=](FullMsgId id) -> Data::MessagePosition {
if (const auto item = session().data().message(id)) {
return item->position();
}
return {};
});
} }
bool ComposeControls::isEditingMessage() const { bool ComposeControls::isEditingMessage() const {

View file

@ -135,6 +135,7 @@ public:
[[nodiscard]] Main::Session &session() const; [[nodiscard]] Main::Session &session() const;
void setHistory(SetHistoryArgs &&args); void setHistory(SetHistoryArgs &&args);
void updateTopicRootId(MsgId topicRootId);
void setCurrentDialogsEntryState(Dialogs::EntryState state); void setCurrentDialogsEntryState(Dialogs::EntryState state);
[[nodiscard]] PeerData *sendAsPeer() const; [[nodiscard]] PeerData *sendAsPeer() const;
@ -158,7 +159,7 @@ public:
[[nodiscard]] rpl::producer<std::optional<bool>> attachRequests() const; [[nodiscard]] rpl::producer<std::optional<bool>> attachRequests() const;
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const; [[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const; [[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
[[nodiscard]] rpl::producer<Data::MessagePosition> scrollRequests() const; [[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const; [[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const; [[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const; [[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const;
@ -357,7 +358,7 @@ private:
const std::unique_ptr<Ui::RpWidget> _wrap; const std::unique_ptr<Ui::RpWidget> _wrap;
const std::unique_ptr<Ui::RpWidget> _writeRestricted; const std::unique_ptr<Ui::RpWidget> _writeRestricted;
rpl::event_stream<FullMsgId> _scrollToItemRequests; rpl::event_stream<FullReplyTo> _jumpToItemRequests;
std::optional<Ui::RoundRect> _backgroundRect; std::optional<Ui::RoundRect> _backgroundRect;

View file

@ -99,9 +99,8 @@ public:
not_null<History*> history); not_null<History*> history);
~PreviewWrap(); ~PreviewWrap();
[[nodiscard]] rpl::producer<TextWithEntities> showQuoteSelector( [[nodiscard]] rpl::producer<SelectedQuote> showQuoteSelector(
not_null<HistoryItem*> item, const SelectedQuote &quote);
const TextWithEntities &quote);
[[nodiscard]] rpl::producer<QString> showLinkSelector( [[nodiscard]] rpl::producer<QString> showLinkSelector(
const TextWithTags &message, const TextWithTags &message,
Data::WebPageDraft webpage, Data::WebPageDraft webpage,
@ -212,12 +211,14 @@ PreviewWrap::~PreviewWrap() {
} }
} }
rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector( rpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(
not_null<HistoryItem*> item, const SelectedQuote &quote) {
const TextWithEntities &quote) {
_selection.reset(TextSelection()); _selection.reset(TextSelection());
_element = item->createView(_delegate.get()); const auto item = quote.item;
const auto group = item->history()->owner().groups().find(item);
const auto leader = group ? group->items.front().get() : item;
_element = leader->createView(_delegate.get());
_link = _pressedLink = nullptr; _link = _pressedLink = nullptr;
if (const auto was = base::take(_draftItem)) { if (const auto was = base::take(_draftItem)) {
@ -233,10 +234,13 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
initElement(); initElement();
_selection = _element->selectionFromQuote(quote); _selection = _element->selectionFromQuote(item, quote.text);
return _selection.value( return _selection.value(
) | rpl::map([=](TextSelection selection) { ) | rpl::map([=](TextSelection selection) {
return _element->selectedQuote(selection); if (const auto result = _element->selectedQuote(selection)) {
return result;
}
return SelectedQuote{ item };
}); });
} }
@ -584,7 +588,7 @@ void DraftOptionsBox(
struct State { struct State {
rpl::variable<Section> shown; rpl::variable<Section> shown;
rpl::lifetime shownLifetime; rpl::lifetime shownLifetime;
rpl::variable<TextWithEntities> quote; rpl::variable<SelectedQuote> quote;
Data::WebPageDraft webpage; Data::WebPageDraft webpage;
WebPageData *preview = nullptr; WebPageData *preview = nullptr;
QString link; QString link;
@ -596,7 +600,7 @@ void DraftOptionsBox(
rpl::lifetime resolveLifetime; rpl::lifetime resolveLifetime;
}; };
const auto state = box->lifetime().make_state<State>(); const auto state = box->lifetime().make_state<State>();
state->quote = draft.reply.quote; state->quote = SelectedQuote{ replyItem, draft.reply.quote };
state->webpage = draft.webpage; state->webpage = draft.webpage;
state->preview = previewData; state->preview = previewData;
state->shown = previewData ? Section::Link : Section::Reply; state->shown = previewData ? Section::Link : Section::Reply;
@ -635,7 +639,10 @@ void DraftOptionsBox(
const auto &clearOldDraft = args.clearOldDraft; const auto &clearOldDraft = args.clearOldDraft;
const auto resolveReply = [=] { const auto resolveReply = [=] {
auto result = draft.reply; auto result = draft.reply;
result.quote = state->quote.current(); if (const auto current = state->quote.current()) {
result.messageId = current.item->fullId();
result.quote = current.text;
}
return result; return result;
}; };
const auto finish = [=]( const auto finish = [=](
@ -650,21 +657,26 @@ void DraftOptionsBox(
const auto setupReplyActions = [=] { const auto setupReplyActions = [=] {
AddFilledSkip(bottom); AddFilledSkip(bottom);
Settings::AddButton( const auto item = state->quote.current().item;
bottom, if (item->allowsForward()) {
tr::lng_reply_in_another_chat(), Settings::AddButton(
st::settingsButton, bottom,
{ &st::menuIconReplace } tr::lng_reply_in_another_chat(),
)->setClickedCallback([=] { st::settingsButton,
ShowReplyToChatBox(show, resolveReply(), clearOldDraft); { &st::menuIconReplace }
}); )->setClickedCallback([=] {
ShowReplyToChatBox(show, resolveReply(), clearOldDraft);
});
}
Settings::AddButton( Settings::AddButton(
bottom, bottom,
tr::lng_reply_show_in_chat(), tr::lng_reply_show_in_chat(),
st::settingsButton, st::settingsButton,
{ &st::menuIconShowInChat } { &st::menuIconShowInChat }
)->setClickedCallback(highlight); )->setClickedCallback([=] {
highlight(resolveReply());
});
Settings::AddButton( Settings::AddButton(
bottom, bottom,
@ -675,7 +687,7 @@ void DraftOptionsBox(
finish({}, state->webpage); finish({}, state->webpage);
}); });
if (!replyItem->originalText().empty()) { if (!item->originalText().empty()) {
AddFilledSkip(bottom); AddFilledSkip(bottom);
Settings::AddDividerText( Settings::AddDividerText(
bottom, bottom,
@ -804,7 +816,6 @@ void DraftOptionsBox(
state->shownLifetime.destroy(); state->shownLifetime.destroy();
if (shown == Section::Reply) { if (shown == Section::Reply) {
state->quote = state->wrap->showQuoteSelector( state->quote = state->wrap->showQuoteSelector(
replyItem,
state->quote.current()); state->quote.current());
setupReplyActions(); setupReplyActions();
} else { } else {
@ -823,8 +834,8 @@ void DraftOptionsBox(
auto save = rpl::combine( auto save = rpl::combine(
state->quote.value(), state->quote.value(),
state->shown.value() state->shown.value()
) | rpl::map([=](const TextWithEntities &quote, Section shown) { ) | rpl::map([=](const SelectedQuote &quote, Section shown) {
return (quote.empty() || shown != Section::Reply) return (quote.text.empty() || shown != Section::Reply)
? tr::lng_settings_save() ? tr::lng_settings_save()
: tr::lng_reply_quote_selected(); : tr::lng_reply_quote_selected();
}) | rpl::flatten_latest(); }) | rpl::flatten_latest();
@ -839,14 +850,20 @@ void DraftOptionsBox(
if (replyItem) { if (replyItem) {
args.show->session().data().itemRemoved( args.show->session().data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> removed) { ) | rpl::filter([=](not_null<const HistoryItem*> removed) {
return removed == replyItem; const auto current = state->quote.current().item;
if ((removed == replyItem) || (removed == current)) {
return true;
}
const auto group = current->history()->owner().groups().find(
current);
return (group && ranges::contains(group->items, removed));
}) | rpl::start_with_next([=] { }) | rpl::start_with_next([=] {
if (previewData) { if (previewData) {
state->tabs = nullptr; state->tabs = nullptr;
box->setPinnedToTopContent( box->setPinnedToTopContent(
object_ptr<Ui::RpWidget>(nullptr)); object_ptr<Ui::RpWidget>(nullptr));
box->setNoContentMargin(false); box->setNoContentMargin(false);
box->setTitle(state->quote.current().empty() box->setTitle(state->quote.current().text.empty()
? tr::lng_reply_options_header() ? tr::lng_reply_options_header()
: tr::lng_reply_options_quote()); : tr::lng_reply_options_quote());
state->shown = Section::Link; state->shown = Section::Link;

View file

@ -32,7 +32,7 @@ struct EditDraftOptionsArgs {
std::vector<MessageLinkRange> links; std::vector<MessageLinkRange> links;
std::shared_ptr<WebpageResolver> resolver; std::shared_ptr<WebpageResolver> resolver;
Fn<void(FullReplyTo, Data::WebPageDraft)> done; Fn<void(FullReplyTo, Data::WebPageDraft)> done;
Fn<void()> highlight; Fn<void(FullReplyTo)> highlight;
Fn<void()> clearOldDraft; Fn<void()> clearOldDraft;
}; };

View file

@ -400,13 +400,6 @@ void ForwardPanel::paint(
}); });
} }
void ClearDraftReplyTo(not_null<Data::Thread*> thread, FullMsgId equalTo) {
ClearDraftReplyTo(
thread->owningHistory(),
thread->topicRootId(),
equalTo);
}
void ClearDraftReplyTo( void ClearDraftReplyTo(
not_null<History*> history, not_null<History*> history,
MsgId topicRootId, MsgId topicRootId,

View file

@ -72,7 +72,6 @@ private:
}; };
void ClearDraftReplyTo(not_null<Data::Thread*> thread, FullMsgId equalTo);
void ClearDraftReplyTo( void ClearDraftReplyTo(
not_null<History*> history, not_null<History*> history,
MsgId topicRootId, MsgId topicRootId,

View file

@ -590,7 +590,7 @@ bool AddReplyToMessageAction(
const ContextMenuRequest &request, const ContextMenuRequest &request,
not_null<ListWidget*> list) { not_null<ListWidget*> list) {
const auto context = list->elementContext(); const auto context = list->elementContext();
const auto item = request.item; const auto item = request.quoteItem ? request.quoteItem : request.item;
const auto topic = item ? item->topic() : nullptr; const auto topic = item ? item->topic() : nullptr;
const auto peer = item ? item->history()->peer.get() : nullptr; const auto peer = item ? item->history()->peer.get() : nullptr;
if (!item if (!item
@ -601,15 +601,7 @@ bool AddReplyToMessageAction(
const auto canSendReply = topic const auto canSendReply = topic
? Data::CanSendAnything(topic) ? Data::CanSendAnything(topic)
: Data::CanSendAnything(peer); : Data::CanSendAnything(peer);
const auto canReply = canSendReply || [&] { const auto canReply = canSendReply || item->allowsForward();
const auto peer = item->history()->peer;
if (const auto chat = peer->asChat()) {
return !chat->isForbidden();
} else if (const auto channel = peer->asChannel()) {
return !channel->isForbidden();
}
return true;
}();
if (!canReply) { if (!canReply) {
return false; return false;
} }

View file

@ -48,6 +48,7 @@ struct ContextMenuRequest {
SelectedItems selectedItems; SelectedItems selectedItems;
TextForMimeData selectedText; TextForMimeData selectedText;
TextWithEntities quote; TextWithEntities quote;
HistoryItem *quoteItem = nullptr;
bool overSelection = false; bool overSelection = false;
PointState pointState = PointState(); PointState pointState = PointState();
}; };

View file

@ -94,6 +94,42 @@ Element *MousedElement/* = nullptr*/;
return session->tryResolveWindow(); return session->tryResolveWindow();
} }
[[nodiscard]] bool CheckQuoteEntities(
const EntitiesInText &quoteEntities,
const TextWithEntities &original,
TextSelection selection) {
auto left = quoteEntities;
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (const auto &entity : original.entities) {
const auto from = entity.offset();
const auto till = from + entity.length();
if (till <= selection.from || from >= selection.to) {
continue;
}
const auto quoteFrom = std::max(from, int(selection.from));
const auto quoteTill = std::min(till, int(selection.to));
const auto cut = EntityInText(
entity.type(),
quoteFrom - int(selection.from),
quoteTill - quoteFrom,
entity.data());
const auto i = ranges::find(left, cut);
if (i != left.end()) {
left.erase(i);
} else if (ranges::contains(allowed, cut.type())) {
return false;
}
}
return left.empty();
};
} // namespace } // namespace
std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient( std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(
@ -111,11 +147,6 @@ bool DefaultElementDelegate::elementUnderCursor(
return false; return false;
} }
float64 DefaultElementDelegate::elementHighlightOpacity(
not_null<const HistoryItem*> item) const {
return 0.;
}
bool DefaultElementDelegate::elementInSelectionMode() { bool DefaultElementDelegate::elementInSelectionMode() {
return false; return false;
} }
@ -593,6 +624,9 @@ void Element::paintHighlight(
Painter &p, Painter &p,
const PaintContext &context, const PaintContext &context,
int geometryHeight) const { int geometryHeight) const {
if (context.highlight.opacity == 0.) {
return;
}
const auto top = marginTop(); const auto top = marginTop();
const auto bottom = marginBottom(); const auto bottom = marginBottom();
const auto fill = qMin(top, bottom); const auto fill = qMin(top, bottom);
@ -608,18 +642,9 @@ void Element::paintCustomHighlight(
int y, int y,
int height, int height,
not_null<const HistoryItem*> item) const { not_null<const HistoryItem*> item) const {
const auto opacity = delegate()->elementHighlightOpacity(item);
if (opacity == 0.) {
return;
}
const auto o = p.opacity(); const auto o = p.opacity();
p.setOpacity(o * opacity); p.setOpacity(o * context.highlight.opacity);
p.fillRect( p.fillRect(0, y, width(), height, context.st->msgSelectOverlay());
0,
y,
width(),
height,
context.st->msgSelectOverlay());
p.setOpacity(o); p.setOpacity(o);
} }
@ -1402,7 +1427,12 @@ HistoryMessageReply *Element::displayedReply() const {
} }
bool Element::toggleSelectionByHandlerClick( bool Element::toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const { const ClickHandlerPtr &handler) const {
return false;
}
bool Element::allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const {
return false; return false;
} }
@ -1572,6 +1602,105 @@ TextSelection Element::adjustSelection(
return selection; return selection;
} }
SelectedQuote Element::FindSelectedQuote(
const Ui::Text::String &text,
TextSelection selection,
not_null<HistoryItem*> item) {
if (selection.to > text.length()) {
return {};
}
auto modified = selection;
for (const auto &modification : text.modifications()) {
if (modification.position >= selection.to) {
break;
} else if (modification.position <= selection.from) {
modified.from += modification.skipped;
if (modification.added
&& modification.position < selection.from) {
--modified.from;
}
}
modified.to += modification.skipped;
if (modification.added && modified.to > modified.from) {
--modified.to;
}
}
auto result = item->originalText();
if (modified.empty() || modified.to > result.text.size()) {
return {};
}
result.text = result.text.mid(
modified.from,
modified.to - modified.from);
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (auto i = result.entities.begin(); i != result.entities.end();) {
const auto offset = i->offset();
const auto till = offset + i->length();
if ((till <= modified.from)
|| (offset >= modified.to)
|| !ranges::contains(allowed, i->type())) {
i = result.entities.erase(i);
} else {
if (till > modified.to) {
i->shrinkFromRight(till - modified.to);
}
i->shiftLeft(modified.from);
++i;
}
}
return { item, result };
}
TextSelection Element::FindSelectionFromQuote(
const Ui::Text::String &text,
not_null<HistoryItem*> item,
const TextWithEntities &quote) {
if (quote.empty()) {
return {};
}
const auto &original = item->originalText();
auto result = TextSelection();
auto offset = 0;
while (true) {
const auto i = original.text.indexOf(quote.text, offset);
if (i < 0) {
return {};
}
auto selection = TextSelection{
uint16(i),
uint16(i + quote.text.size()),
};
if (CheckQuoteEntities(quote.entities, original, selection)) {
result = selection;
break;
}
offset = i + 1;
}
//for (const auto &modification : text.modifications()) {
// if (modification.position >= selection.to) {
// break;
// } else if (modification.position <= selection.from) {
// modified.from += modification.skipped;
// if (modification.added
// && modification.position < selection.from) {
// --modified.from;
// }
// }
// modified.to += modification.skipped;
// if (modification.added && modified.to > modified.from) {
// --modified.to;
// }
//}
return result;
}
Reactions::ButtonParameters Element::reactionButtonParameters( Reactions::ButtonParameters Element::reactionButtonParameters(
QPoint position, QPoint position,
const TextState &reactionState) const { const TextState &reactionState) const {

View file

@ -69,8 +69,6 @@ class ElementDelegate {
public: public:
virtual Context elementContext() = 0; virtual Context elementContext() = 0;
virtual bool elementUnderCursor(not_null<const Element*> view) = 0; virtual bool elementUnderCursor(not_null<const Element*> view) = 0;
[[nodiscard]] virtual float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const = 0;
virtual bool elementInSelectionMode() = 0; virtual bool elementInSelectionMode() = 0;
virtual bool elementIntersectsRange( virtual bool elementIntersectsRange(
not_null<const Element*> view, not_null<const Element*> view,
@ -120,8 +118,6 @@ public:
class DefaultElementDelegate : public ElementDelegate { class DefaultElementDelegate : public ElementDelegate {
public: public:
bool elementUnderCursor(not_null<const Element*> view) override; bool elementUnderCursor(not_null<const Element*> view) override;
[[nodiscard]] float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const override;
bool elementInSelectionMode() override; bool elementInSelectionMode() override;
bool elementIntersectsRange( bool elementIntersectsRange(
not_null<const Element*> view, not_null<const Element*> view,
@ -270,6 +266,16 @@ struct TopicButton {
int nameVersion = 0; int nameVersion = 0;
}; };
struct SelectedQuote {
HistoryItem *item = nullptr;
TextWithEntities text;
explicit operator bool() const {
return item && !text.empty();
}
friend inline bool operator==(SelectedQuote, SelectedQuote) = default;
};
class Element class Element
: public Object : public Object
, public RuntimeComposer<Element> , public RuntimeComposer<Element>
@ -391,19 +397,24 @@ public:
QPoint point, QPoint point,
InfoDisplayType type) const; InfoDisplayType type) const;
virtual TextForMimeData selectedText(TextSelection selection) const = 0; virtual TextForMimeData selectedText(TextSelection selection) const = 0;
virtual TextWithEntities selectedQuote(TextSelection selection) const = 0; virtual SelectedQuote selectedQuote(
virtual TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const = 0; TextSelection selection) const = 0;
virtual TextSelection selectionFromQuote( virtual TextSelection selectionFromQuote(
const TextWithEntities &quote) const = 0; not_null<HistoryItem*> item,
virtual TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const = 0; const TextWithEntities &quote) const = 0;
[[nodiscard]] virtual TextSelection adjustSelection( [[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const; TextSelectType type) const;
[[nodiscard]] static SelectedQuote FindSelectedQuote(
const Ui::Text::String &text,
TextSelection selection,
not_null<HistoryItem*> item);
[[nodiscard]] static TextSelection FindSelectionFromQuote(
const Ui::Text::String &text,
not_null<HistoryItem*> item,
const TextWithEntities &quote);
[[nodiscard]] virtual auto reactionButtonParameters( [[nodiscard]] virtual auto reactionButtonParameters(
QPoint position, QPoint position,
const TextState &reactionState) const -> Reactions::ButtonParameters; const TextState &reactionState) const -> Reactions::ButtonParameters;
@ -451,6 +462,8 @@ public:
} }
[[nodiscard]] virtual bool toggleSelectionByHandlerClick( [[nodiscard]] virtual bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const; const ClickHandlerPtr &handler) const;
[[nodiscard]] virtual bool allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const;
struct VerticalRepaintRange { struct VerticalRepaintRange {
int top = 0; int top = 0;

View file

@ -707,8 +707,12 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
return _items.front()->data()->position() > position; return _items.front()->data()->position() > position;
} }
void ListWidget::highlightMessage(FullMsgId itemId) { void ListWidget::highlightMessage(
_highlighter.highlight(itemId); FullMsgId itemId,
const TextWithEntities &part) {
if (const auto view = viewForItem(itemId)) {
_highlighter.highlight(view, part);
}
} }
void ListWidget::showAroundPosition( void ListWidget::showAroundPosition(
@ -741,12 +745,12 @@ bool ListWidget::jumpToBottomInsteadOfUnread() const {
void ListWidget::showAtPosition( void ListWidget::showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,
anim::type animated, const Window::SectionShow &params,
Fn<void(bool found)> done) { Fn<void(bool found)> done) {
const auto showAtUnread = (position == Data::UnreadMessagePosition); const auto showAtUnread = (position == Data::UnreadMessagePosition);
if (showAtUnread && jumpToBottomInsteadOfUnread()) { if (showAtUnread && jumpToBottomInsteadOfUnread()) {
showAtPosition(Data::MaxMessagePosition, animated, std::move(done)); showAtPosition(Data::MaxMessagePosition, params, std::move(done));
return; return;
} }
@ -766,24 +770,24 @@ void ListWidget::showAtPosition(
_bar = {}; _bar = {};
} }
checkUnreadBarCreation(); checkUnreadBarCreation();
return showAtPositionNow(position, animated, done); return showAtPositionNow(position, params, done);
}); });
} else if (!showAtPositionNow(position, animated, done)) { } else if (!showAtPositionNow(position, params, done)) {
showAroundPosition(position, [=] { showAroundPosition(position, [=] {
return showAtPositionNow(position, animated, done); return showAtPositionNow(position, params, done);
}); });
} }
} }
bool ListWidget::showAtPositionNow( bool ListWidget::showAtPositionNow(
Data::MessagePosition position, Data::MessagePosition position,
anim::type animated, const Window::SectionShow &params,
Fn<void(bool found)> done) { Fn<void(bool found)> done) {
if (const auto scrollTop = scrollTopForPosition(position)) { if (const auto scrollTop = scrollTopForPosition(position)) {
computeScrollTo(*scrollTop, position, animated); computeScrollTo(*scrollTop, position, params.animated);
if (position != Data::MaxMessagePosition if (position != Data::MaxMessagePosition
&& position != Data::UnreadMessagePosition) { && position != Data::UnreadMessagePosition) {
highlightMessage(position.fullId); highlightMessage(position.fullId, params.highlightPart);
} }
if (done) { if (done) {
const auto found = !position.fullId.peer const auto found = !position.fullId.peer
@ -1655,11 +1659,6 @@ bool ListWidget::elementUnderCursor(
return (_overElement == view); return (_overElement == view);
} }
float64 ListWidget::elementHighlightOpacity(
not_null<const HistoryItem*> item) const {
return _highlighter.progress(item);
}
bool ListWidget::elementInSelectionMode() { bool ListWidget::elementInSelectionMode() {
return inSelectionMode(); return inSelectionMode();
} }
@ -2088,6 +2087,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
}); });
auto context = preparePaintContext(clip); auto context = preparePaintContext(clip);
context.highlightPathCache = &_highlightPathCache;
if (from == end(_items)) { if (from == end(_items)) {
_delegate->listPaintEmpty(p, context); _delegate->listPaintEmpty(p, context);
return; return;
@ -2108,6 +2108,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
= _reactionsManager->currentReactionPaintInfo(); = _reactionsManager->currentReactionPaintInfo();
context.outbg = view->hasOutLayout(); context.outbg = view->hasOutLayout();
context.selection = itemRenderSelection(view); context.selection = itemRenderSelection(view);
context.highlight = _highlighter.state(item);
view->draw(p, context); view->draw(p, context);
} }
if (_translateTracker) { if (_translateTracker) {
@ -2142,7 +2143,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
} else if (item->isUnreadMention() } else if (item->isUnreadMention()
&& !item->isUnreadMedia()) { && !item->isUnreadMedia()) {
readContents.insert(item); readContents.insert(item);
_highlighter.enqueue(view); _highlighter.enqueue(view, {});
} }
} }
session->data().reactions().poll(item, context.now); session->data().reactions().poll(item, context.now);
@ -2576,7 +2577,6 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
: _overElement : _overElement
? _overElement->data().get() ? _overElement->data().get()
: nullptr; : nullptr;
const auto overItemView = viewForItem(overItem);
const auto clickedReaction = link const auto clickedReaction = link
? link->property( ? link->property(
kReactionsCountEmojiProperty).value<Data::ReactionId>() kReactionsCountEmojiProperty).value<Data::ReactionId>()
@ -2603,9 +2603,12 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
request.view = _overElement; request.view = _overElement;
request.item = overItem; request.item = overItem;
request.pointState = _overState.pointState; request.pointState = _overState.pointState;
request.quote = (overItemView && _selectedTextItem == overItem) const auto quote = (_overElement
? overItemView->selectedQuote(_selectedTextRange) && _selectedTextItem == _overElement->data())
: TextWithEntities(); ? _overElement->selectedQuote(_selectedTextRange)
: SelectedQuote();
request.quote = quote.text;
request.quoteItem = quote.item;
request.selectedText = _selectedText; request.selectedText = _selectedText;
request.selectedItems = collectSelectedItems(); request.selectedItems = collectSelectedItems();
const auto hasSelection = !request.selectedItems.empty() const auto hasSelection = !request.selectedItems.empty()

View file

@ -48,6 +48,10 @@ struct ChosenReaction;
struct ButtonParameters; struct ButtonParameters;
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions
namespace Window {
struct SectionShow;
} // namespace Window
namespace HistoryView { namespace HistoryView {
struct TextState; struct TextState;
@ -227,11 +231,13 @@ public:
[[nodiscard]] bool animatedScrolling() const; [[nodiscard]] bool animatedScrolling() const;
bool isAbovePosition(Data::MessagePosition position) const; bool isAbovePosition(Data::MessagePosition position) const;
bool isBelowPosition(Data::MessagePosition position) const; bool isBelowPosition(Data::MessagePosition position) const;
void highlightMessage(FullMsgId itemId); void highlightMessage(
FullMsgId itemId,
const TextWithEntities &part);
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,
anim::type animated = anim::type::normal, const Window::SectionShow &params,
Fn<void(bool found)> done = nullptr); Fn<void(bool found)> done = nullptr);
void refreshViewer(); void refreshViewer();
@ -292,8 +298,6 @@ public:
// ElementDelegate interface. // ElementDelegate interface.
Context elementContext() override; Context elementContext() override;
bool elementUnderCursor(not_null<const Element*> view) override; bool elementUnderCursor(not_null<const Element*> view) override;
[[nodiscard]] float64 elementHighlightOpacity(
not_null<const HistoryItem*> item) const override;
bool elementInSelectionMode() override; bool elementInSelectionMode() override;
bool elementIntersectsRange( bool elementIntersectsRange(
not_null<const Element*> view, not_null<const Element*> view,
@ -430,7 +434,7 @@ private:
Fn<bool()> overrideInitialScroll); Fn<bool()> overrideInitialScroll);
bool showAtPositionNow( bool showAtPositionNow(
Data::MessagePosition position, Data::MessagePosition position,
anim::type animated, const Window::SectionShow &params,
Fn<void(bool found)> done); Fn<void(bool found)> done);
Ui::ChatPaintContext preparePaintContext(const QRect &clip) const; Ui::ChatPaintContext preparePaintContext(const QRect &clip) const;
@ -645,6 +649,7 @@ private:
base::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics; base::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient; const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
QPainterPath _highlightPathCache;
base::unique_qptr<Ui::RpWidget> _emptyInfo = nullptr; base::unique_qptr<Ui::RpWidget> _emptyInfo = nullptr;

View file

@ -65,42 +65,6 @@ const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
return std::nullopt; return std::nullopt;
} }
[[nodiscard]] bool CheckQuoteEntities(
const EntitiesInText &quoteEntities,
const TextWithEntities &original,
TextSelection selection) {
auto left = quoteEntities;
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (const auto &entity : original.entities) {
const auto from = entity.offset();
const auto till = from + entity.length();
if (till <= selection.from || from >= selection.to) {
continue;
}
const auto quoteFrom = std::max(from, int(selection.from));
const auto quoteTill = std::min(till, int(selection.to));
const auto cut = EntityInText(
entity.type(),
quoteFrom - int(selection.from),
quoteTill - quoteFrom,
entity.data());
const auto i = ranges::find(left, cut);
if (i != left.end()) {
left.erase(i);
} else if (ranges::contains(allowed, cut.type())) {
return false;
}
}
return left.empty();
};
class KeyboardStyle : public ReplyKeyboard::Style { class KeyboardStyle : public ReplyKeyboard::Style {
public: public:
KeyboardStyle(const style::BotKeyboardButton &st); KeyboardStyle(const style::BotKeyboardButton &st);
@ -1018,6 +982,10 @@ void Message::draw(Painter &p, const PaintContext &context) const {
p.translate(-reactionsPosition); p.translate(-reactionsPosition);
} }
if (context.highlightPathCache) {
context.highlightInterpolateTo = g;
context.highlightPathCache->clear();
}
if (bubble) { if (bubble) {
if (displayFromName() if (displayFromName()
&& item->displayFrom() && item->displayFrom()
@ -1110,6 +1078,7 @@ void Message::draw(Painter &p, const PaintContext &context) const {
- (_bottomInfo.height() - st::msgDateFont->height)); - (_bottomInfo.height() - st::msgDateFont->height));
} }
auto textSelection = context.selection; auto textSelection = context.selection;
auto highlightRange = context.highlight.range;
const auto mediaHeight = mediaDisplayed ? media->height() : 0; const auto mediaHeight = mediaDisplayed ? media->height() : 0;
const auto paintMedia = [&](int top) { const auto paintMedia = [&](int top) {
if (!mediaDisplayed) { if (!mediaDisplayed) {
@ -1118,6 +1087,8 @@ void Message::draw(Painter &p, const PaintContext &context) const {
const auto mediaSelection = _invertMedia const auto mediaSelection = _invertMedia
? context.selection ? context.selection
: skipTextSelection(context.selection); : skipTextSelection(context.selection);
const auto maybeMediaHighlight = context.highlightPathCache
&& context.highlightPathCache->isEmpty();
auto mediaPosition = QPoint(inner.left(), top); auto mediaPosition = QPoint(inner.left(), top);
p.translate(mediaPosition); p.translate(mediaPosition);
media->draw(p, context.translated( media->draw(p, context.translated(
@ -1130,6 +1101,10 @@ void Message::draw(Painter &p, const PaintContext &context) const {
context.reactionInfo->effectOffset -= add; context.reactionInfo->effectOffset -= add;
} }
} }
if (maybeMediaHighlight
&& !context.highlightPathCache->isEmpty()) {
context.highlightPathCache->translate(mediaPosition);
}
p.translate(-mediaPosition); p.translate(-mediaPosition);
}; };
if (mediaDisplayed && _invertMedia) { if (mediaDisplayed && _invertMedia) {
@ -1141,8 +1116,12 @@ void Message::draw(Painter &p, const PaintContext &context) const {
+ mediaHeight + mediaHeight
+ (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + (mediaOnBottom ? 0 : st::mediaInBubbleSkip));
textSelection = media->skipSelection(textSelection); textSelection = media->skipSelection(textSelection);
highlightRange = media->skipSelection(highlightRange);
} }
paintText(p, trect, context.withSelection(textSelection)); auto copy = context;
copy.selection = textSelection;
copy.highlight.range = highlightRange;
paintText(p, trect, copy);
if (mediaDisplayed && !_invertMedia) { if (mediaDisplayed && !_invertMedia) {
paintMedia(trect.y() + trect.height() - mediaHeight); paintMedia(trect.y() + trect.height() - mediaHeight);
if (context.reactionInfo && !displayInfo && !_reactions) { if (context.reactionInfo && !displayInfo && !_reactions) {
@ -1224,6 +1203,20 @@ void Message::draw(Painter &p, const PaintContext &context) const {
p.restoreTextPalette(); p.restoreTextPalette();
if (context.highlightPathCache
&& !context.highlightPathCache->isEmpty()) {
const auto alpha = int(0.25
* context.highlight.collapsion
* context.highlight.opacity
* 255);
if (alpha > 0) {
context.highlightPathCache->setFillRule(Qt::WindingFill);
auto color = context.messageStyle()->textPalette.linkFg->c;
color.setAlpha(alpha);
p.fillPath(*context.highlightPathCache, color);
}
}
if (roll) { if (roll) {
p.restore(); p.restore();
} }
@ -1651,6 +1644,7 @@ void Message::paintText(
width()); width());
trect.setY(trect.y() + botTop->height); trect.setY(trect.y() + botTop->height);
} }
auto highlightRequest = context.computeHighlightCache();
text().draw(p, { text().draw(p, {
.position = trect.topLeft(), .position = trect.topLeft(),
.availableWidth = trect.width(), .availableWidth = trect.width(),
@ -1663,6 +1657,7 @@ void Message::paintText(
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection, .selection = context.selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} }
@ -2651,7 +2646,7 @@ TextForMimeData Message::selectedText(TextSelection selection) const {
return result; return result;
} }
TextWithEntities Message::selectedQuote(TextSelection selection) const { SelectedQuote Message::selectedQuote(TextSelection selection) const {
const auto item = data(); const auto item = data();
const auto &translated = item->translatedText(); const auto &translated = item->translatedText();
const auto &original = item->originalText(); const auto &original = item->originalText();
@ -2666,7 +2661,7 @@ TextWithEntities Message::selectedQuote(TextSelection selection) const {
const auto textSelection = mediaBefore const auto textSelection = mediaBefore
? media->skipSelection(selection) ? media->skipSelection(selection)
: selection; : selection;
return selectedQuote(text(), textSelection); return FindSelectedQuote(text(), textSelection, data());
} else if (const auto media = this->media()) { } else if (const auto media = this->media()) {
if (media->isDisplayed() || isHiddenByGroup()) { if (media->isDisplayed() || isHiddenByGroup()) {
return media->selectedQuote(selection); return media->selectedQuote(selection);
@ -2675,124 +2670,30 @@ TextWithEntities Message::selectedQuote(TextSelection selection) const {
return {}; return {};
} }
TextWithEntities Message::selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const {
if (selection.to > text.length()) {
return {};
}
auto modified = selection;
for (const auto &modification : text.modifications()) {
if (modification.position >= selection.to) {
break;
} else if (modification.position <= selection.from) {
modified.from += modification.skipped;
if (modification.added
&& modification.position < selection.from) {
--modified.from;
}
}
modified.to += modification.skipped;
if (modification.added && modified.to > modified.from) {
--modified.to;
}
}
auto result = data()->originalText();
if (modified.empty() || modified.to > result.text.size()) {
return {};
}
result.text = result.text.mid(
modified.from,
modified.to - modified.from);
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (auto i = result.entities.begin(); i != result.entities.end();) {
const auto offset = i->offset();
const auto till = offset + i->length();
if ((till <= modified.from)
|| (offset >= modified.to)
|| !ranges::contains(allowed, i->type())) {
i = result.entities.erase(i);
} else {
if (till > modified.to) {
i->shrinkFromRight(till - modified.to);
}
i->shiftLeft(modified.from);
++i;
}
}
return result;
}
TextSelection Message::selectionFromQuote( TextSelection Message::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
const auto item = data(); if (quote.empty()) {
return {};
}
const auto &translated = item->translatedText(); const auto &translated = item->translatedText();
const auto &original = item->originalText(); const auto &original = item->originalText();
if (&translated != &original || quote.empty()) { if (&translated != &original) {
return {}; return {};
} else if (hasVisibleText()) { } else if (hasVisibleText()) {
const auto media = this->media(); const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed(); const auto mediaDisplayed = media && media->isDisplayed();
const auto mediaBefore = mediaDisplayed && invertMedia(); const auto mediaBefore = mediaDisplayed && invertMedia();
const auto result = selectionFromQuote(text(), quote); const auto result = FindSelectionFromQuote(text(), item, quote);
return mediaBefore ? media->unskipSelection(result) : result; return mediaBefore ? media->unskipSelection(result) : result;
} else if (const auto media = this->media()) { } else if (const auto media = this->media()) {
if (media->isDisplayed() || isHiddenByGroup()) { if (media->isDisplayed() || isHiddenByGroup()) {
return media->selectionFromQuote(quote); return media->selectionFromQuote(item, quote);
} }
} }
return {}; return {};
} }
TextSelection Message::selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const {
if (quote.empty()) {
return {};
}
const auto &original = data()->originalText();
auto result = TextSelection();
auto offset = 0;
while (true) {
const auto i = original.text.indexOf(quote.text, offset);
if (i < 0) {
return {};
}
auto selection = TextSelection{
uint16(i),
uint16(i + quote.text.size()),
};
if (CheckQuoteEntities(quote.entities, original, selection)) {
result = selection;
break;
}
offset = i + 1;
}
//for (const auto &modification : text.modifications()) {
// if (modification.position >= selection.to) {
// break;
// } else if (modification.position <= selection.from) {
// modified.from += modification.skipped;
// if (modification.added
// && modification.position < selection.from) {
// --modified.from;
// }
// }
// modified.to += modification.skipped;
// if (modification.added && modified.to > modified.from) {
// --modified.to;
// }
//}
return result;
}
TextSelection Message::adjustSelection( TextSelection Message::adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const { TextSelectType type) const {
@ -3247,6 +3148,16 @@ bool Message::toggleSelectionByHandlerClick(
return false; return false;
} }
bool Message::allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const {
if (const auto media = this->media()) {
if (media->allowTextSelectionByHandler(handler)) {
return true;
}
}
return false;
}
bool Message::hasFromName() const { bool Message::hasFromName() const {
switch (context()) { switch (context()) {
case Context::AdminLog: case Context::AdminLog:
@ -4148,7 +4059,7 @@ void Message::refreshInfoSkipBlock() {
return false; return false;
} else if (item->Has<HistoryMessageLogEntryOriginal>()) { } else if (item->Has<HistoryMessageLogEntryOriginal>()) {
return false; return false;
} else if (media && media->isDisplayed()) { } else if (media && media->isDisplayed() && !_invertMedia) {
return false; return false;
} else if (_reactions) { } else if (_reactions) {
return false; return false;

View file

@ -95,14 +95,9 @@ public:
QPoint point, QPoint point,
InfoDisplayType type) const override; InfoDisplayType type) const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
const TextWithEntities &quote) const override; not_null<HistoryItem*> item,
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
TextSelection adjustSelection( TextSelection adjustSelection(
TextSelection selection, TextSelection selection,
@ -145,6 +140,8 @@ public:
[[nodiscard]] HistoryMessageReply *displayedReply() const override; [[nodiscard]] HistoryMessageReply *displayedReply() const override;
[[nodiscard]] bool toggleSelectionByHandlerClick( [[nodiscard]] bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const override; const ClickHandlerPtr &handler) const override;
[[nodiscard]] bool allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const override;
[[nodiscard]] int infoWidth() const override; [[nodiscard]] int infoWidth() const override;
[[nodiscard]] int bottomInfoFirstLineWidth() const override; [[nodiscard]] int bottomInfoFirstLineWidth() const override;
[[nodiscard]] bool bottomInfoIsWide() const override; [[nodiscard]] bool bottomInfoIsWide() const override;

View file

@ -262,7 +262,7 @@ void PinnedWidget::showAtPosition(
FullMsgId originId) { FullMsgId originId) {
_inner->showAtPosition( _inner->showAtPosition(
position, position,
anim::type::normal, {},
_cornerButtons.doneJumpFrom(position.fullId, originId)); _cornerButtons.doneJumpFrom(position.fullId, originId));
} }
@ -346,7 +346,7 @@ void PinnedWidget::restoreState(not_null<PinnedMemento*> memento) {
? FullMsgId(_history->peer->id, highlight) ? FullMsgId(_history->peer->id, highlight)
: FullMsgId(_migratedPeer->id, -highlight)), : FullMsgId(_migratedPeer->id, -highlight)),
.date = TimeId(0), .date = TimeId(0),
}, anim::type::instant); }, { Window::SectionShow::Way::Forward, anim::type::instant });
} }
} }

View file

@ -128,10 +128,12 @@ rpl::producer<Ui::MessageBarContent> RootViewContent(
RepliesMemento::RepliesMemento( RepliesMemento::RepliesMemento(
not_null<History*> history, not_null<History*> history,
MsgId rootId, MsgId rootId,
MsgId highlightId) MsgId highlightId,
const TextWithEntities &highlightPart)
: _history(history) : _history(history)
, _rootId(rootId) , _rootId(rootId)
, _highlightId(highlightId) { , _highlightId(highlightId)
, _highlightPart(highlightPart) {
if (highlightId) { if (highlightId) {
_list.setAroundPosition({ _list.setAroundPosition({
.fullId = FullMsgId(_history->peer->id, highlightId), .fullId = FullMsgId(_history->peer->id, highlightId),
@ -328,6 +330,7 @@ RepliesWidget::RepliesWidget(
Controls::ShowReplyToChatBox(controller->uiShow(), { fullId }); Controls::ShowReplyToChatBox(controller->uiShow(), { fullId });
} else { } else {
replyToMessage(fullId); replyToMessage(fullId);
_composeControls->focus();
} }
}, _inner->lifetime()); }, _inner->lifetime());
@ -490,6 +493,7 @@ void RepliesWidget::setupTopicViewer() {
) | rpl::start_with_next([=](const Data::Session::IdChange &change) { ) | rpl::start_with_next([=](const Data::Session::IdChange &change) {
if (_rootId == change.oldId) { if (_rootId == change.oldId) {
_rootId = change.newId.msg; _rootId = change.newId.msg;
_composeControls->updateTopicRootId(_rootId);
_sendAction = owner->sendActionManager().repliesPainter( _sendAction = owner->sendActionManager().repliesPainter(
_history, _history,
_rootId); _rootId);
@ -787,9 +791,11 @@ void RepliesWidget::setupComposeControls() {
sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); sendInlineResult(chosen.result, chosen.bot, chosen.options, localId);
}, lifetime()); }, lifetime());
_composeControls->scrollRequests( _composeControls->jumpToItemRequests(
) | rpl::start_with_next([=](Data::MessagePosition pos) { ) | rpl::start_with_next([=](FullReplyTo to) {
showAtPosition(pos); if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler(item, {}, to.quote)->onClick({});
}
}, lifetime()); }, lifetime());
_composeControls->scrollKeyEvents( _composeControls->scrollKeyEvents(
@ -1859,16 +1865,22 @@ void RepliesWidget::finishSending() {
refreshTopBarActiveChat(); refreshTopBarActiveChat();
} }
void RepliesWidget::showAtPosition(
Data::MessagePosition position,
FullMsgId originItemId) {
showAtPosition(position, originItemId, {});
}
void RepliesWidget::showAtPosition( void RepliesWidget::showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,
FullMsgId originItemId, FullMsgId originItemId,
anim::type animated) { const Window::SectionShow &params) {
_lastShownAt = position.fullId; _lastShownAt = position.fullId;
controller()->setActiveChatEntry(activeChat()); controller()->setActiveChatEntry(activeChat());
const auto ignore = (position.fullId.msg == _rootId); const auto ignore = (position.fullId.msg == _rootId);
_inner->showAtPosition( _inner->showAtPosition(
position, position,
animated, params,
_cornerButtons.doneJumpFrom(position.fullId, originItemId, ignore)); _cornerButtons.doneJumpFrom(position.fullId, originItemId, ignore));
} }
@ -1960,7 +1972,7 @@ bool RepliesWidget::showInternal(
if (logMemento->getHistory() == history() if (logMemento->getHistory() == history()
&& logMemento->getRootId() == _rootId) { && logMemento->getRootId() == _rootId) {
restoreState(logMemento); restoreState(logMemento);
if (!logMemento->getHighlightId()) { if (!logMemento->highlightId()) {
showAtPosition(Data::UnreadMessagePosition); showAtPosition(Data::UnreadMessagePosition);
} }
if (params.reapplyLocalDraft) { if (params.reapplyLocalDraft) {
@ -2008,7 +2020,7 @@ bool RepliesWidget::showMessage(
} }
const auto id = FullMsgId(_history->peer->id, messageId); const auto id = FullMsgId(_history->peer->id, messageId);
const auto message = _history->owner().message(id); const auto message = _history->owner().message(id);
if (!message) { if (!message || !message->inThread(_rootId)) {
return false; return false;
} }
const auto originMessage = [&]() -> HistoryItem* { const auto originMessage = [&]() -> HistoryItem* {
@ -2024,13 +2036,13 @@ bool RepliesWidget::showMessage(
} }
return nullptr; return nullptr;
}(); }();
if (!originMessage) { const auto currentReplyReturn = _cornerButtons.replyReturn();
return false; const auto originItemId = !originMessage
} ? FullMsgId()
const auto originItemId = (_cornerButtons.replyReturn() != originMessage) : (currentReplyReturn != originMessage)
? originMessage->fullId() ? originMessage->fullId()
: FullMsgId(); : FullMsgId();
showAtPosition(message->position(), originItemId); showAtPosition(message->position(), originItemId, params);
return true; return true;
} }
@ -2132,11 +2144,15 @@ void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) {
} }
_cornerButtons.setReplyReturns(memento->replyReturns()); _cornerButtons.setReplyReturns(memento->replyReturns());
_inner->restoreState(memento->list()); _inner->restoreState(memento->list());
if (const auto highlight = memento->getHighlightId()) { if (const auto highlight = memento->highlightId()) {
auto params = Window::SectionShow(
Window::SectionShow::Way::Forward,
anim::type::instant);
params.highlightPart = memento->highlightPart();
showAtPosition(Data::MessagePosition{ showAtPosition(Data::MessagePosition{
.fullId = FullMsgId(_history->peer->id, highlight), .fullId = FullMsgId(_history->peer->id, highlight),
.date = TimeId(0), .date = TimeId(0),
}, {}, anim::type::instant); }, {}, params);
} }
} }

View file

@ -208,8 +208,11 @@ private:
void showAtEnd(); void showAtEnd();
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,
FullMsgId originItemId = {}, FullMsgId originItemId = {});
anim::type animated = anim::type::normal); void showAtPosition(
Data::MessagePosition position,
FullMsgId originItemId,
const Window::SectionShow &params);
void finishSending(); void finishSending();
void setupComposeControls(); void setupComposeControls();
@ -377,7 +380,8 @@ public:
RepliesMemento( RepliesMemento(
not_null<History*> history, not_null<History*> history,
MsgId rootId, MsgId rootId,
MsgId highlightId = 0); MsgId highlightId = 0,
const TextWithEntities &highlightPart = {});
explicit RepliesMemento( explicit RepliesMemento(
not_null<HistoryItem*> commentsItem, not_null<HistoryItem*> commentsItem,
MsgId commentId = 0); MsgId commentId = 0);
@ -421,9 +425,12 @@ public:
[[nodiscard]] not_null<ListMemento*> list() { [[nodiscard]] not_null<ListMemento*> list() {
return &_list; return &_list;
} }
[[nodiscard]] MsgId getHighlightId() const { [[nodiscard]] MsgId highlightId() const {
return _highlightId; return _highlightId;
} }
[[nodiscard]] const TextWithEntities &highlightPart() const {
return _highlightPart;
}
private: private:
void setupTopicViewer(); void setupTopicViewer();
@ -431,6 +438,7 @@ private:
const not_null<History*> _history; const not_null<History*> _history;
MsgId _rootId = 0; MsgId _rootId = 0;
const MsgId _highlightId = 0; const MsgId _highlightId = 0;
const TextWithEntities _highlightPart;
ListMemento _list; ListMemento _list;
std::shared_ptr<Data::RepliesList> _replies; std::shared_ptr<Data::RepliesList> _replies;
QVector<FullMsgId> _replyReturns; QVector<FullMsgId> _replyReturns;

View file

@ -274,9 +274,15 @@ void ScheduledWidget::setupComposeControls() {
sendInlineResult(chosen.result, chosen.bot); sendInlineResult(chosen.result, chosen.bot);
}, lifetime()); }, lifetime());
_composeControls->scrollRequests( _composeControls->jumpToItemRequests(
) | rpl::start_with_next([=](Data::MessagePosition pos) { ) | rpl::start_with_next([=](FullReplyTo to) {
showAtPosition(pos); if (const auto item = session().data().message(to.messageId)) {
if (item->isScheduled() && item->history() == _history) {
showAtPosition(item->position());
} else {
JumpToMessageClickHandler(item, {}, to.quote)->onClick({});
}
}
}, lifetime()); }, lifetime());
_composeControls->scrollKeyEvents( _composeControls->scrollKeyEvents(
@ -858,7 +864,7 @@ void ScheduledWidget::showAtPosition(
FullMsgId originId) { FullMsgId originId) {
_inner->showAtPosition( _inner->showAtPosition(
position, position,
anim::type::normal, {},
_cornerButtons.doneJumpFrom(position.fullId, originId)); _cornerButtons.doneJumpFrom(position.fullId, originId));
} }

View file

@ -669,23 +669,12 @@ TextForMimeData Service::selectedText(TextSelection selection) const {
return text().toTextForMimeData(selection); return text().toTextForMimeData(selection);
} }
TextWithEntities Service::selectedQuote(TextSelection selection) const { SelectedQuote Service::selectedQuote(TextSelection selection) const {
return {};
}
TextWithEntities Service::selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const {
return {}; return {};
} }
TextSelection Service::selectionFromQuote( TextSelection Service::selectionFromQuote(
const TextWithEntities &quote) const { not_null<HistoryItem*> item,
return {};
}
TextSelection Service::selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return {}; return {};
} }

View file

@ -43,14 +43,9 @@ public:
StateRequest request) const override; StateRequest request) const override;
void updatePressed(QPoint point) override; void updatePressed(QPoint point) override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
const TextWithEntities &quote) const override; not_null<HistoryItem*> item,
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
TextSelection adjustSelection( TextSelection adjustSelection(
TextSelection selection, TextSelection selection,

View file

@ -744,6 +744,7 @@ void Document::draw(
if (const auto captioned = Get<HistoryDocumentCaptioned>()) { if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
p.setPen(stm->historyTextFg); p.setPen(stm->historyTextFg);
_parent->prepareCustomEmojiPaint(p, context, captioned->caption); _parent->prepareCustomEmojiPaint(p, context, captioned->caption);
auto highlightRequest = context.computeHighlightCache();
captioned->caption.draw(p, { captioned->caption.draw(p, {
.position = { st::msgPadding.left(), captiontop }, .position = { st::msgPadding.left(), captiontop },
.availableWidth = captionw, .availableWidth = captionw,
@ -756,6 +757,7 @@ void Document::draw(
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = selection, .selection = selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} }
} }
@ -1210,7 +1212,7 @@ TextForMimeData Document::selectedText(TextSelection selection) const {
return result; return result;
} }
TextWithEntities Document::selectedQuote(TextSelection selection) const { SelectedQuote Document::selectedQuote(TextSelection selection) const {
if (const auto voice = Get<HistoryDocumentVoice>()) { if (const auto voice = Get<HistoryDocumentVoice>()) {
const auto length = voice->transcribeText.length(); const auto length = voice->transcribeText.length();
if (selection.from < length) { if (selection.from < length) {
@ -1221,16 +1223,21 @@ TextWithEntities Document::selectedQuote(TextSelection selection) const {
voice->transcribeText); voice->transcribeText);
} }
if (const auto captioned = Get<HistoryDocumentCaptioned>()) { if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
return parent()->selectedQuote(captioned->caption, selection); return Element::FindSelectedQuote(
captioned->caption,
selection,
_realParent);
} }
return {}; return {};
} }
TextSelection Document::selectionFromQuote( TextSelection Document::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) { if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
const auto result = parent()->selectionFromQuote( const auto result = Element::FindSelectionFromQuote(
captioned->caption, captioned->caption,
item,
quote); quote);
if (result.empty()) { if (result.empty()) {
return {}; return {};
@ -1390,6 +1397,8 @@ void Document::drawGrouped(
float64 highlightOpacity, float64 highlightOpacity,
not_null<uint64*> cacheKey, not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const { not_null<QPixmap*> cache) const {
const auto maybeMediaHighlight = context.highlightPathCache
&& context.highlightPathCache->isEmpty();
p.translate(geometry.topLeft()); p.translate(geometry.topLeft());
draw( draw(
p, p,
@ -1397,6 +1406,10 @@ void Document::drawGrouped(
geometry.width(), geometry.width(),
LayoutMode::Grouped, LayoutMode::Grouped,
rounding); rounding);
if (maybeMediaHighlight
&& !context.highlightPathCache->isEmpty()) {
context.highlightPathCache->translate(geometry.topLeft());
}
p.translate(-geometry.topLeft()); p.translate(-geometry.topLeft());
} }

View file

@ -46,8 +46,9 @@ public:
bool hasTextForCopy() const override; bool hasTextForCopy() const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
bool uploading() const override; bool uploading() const override;

View file

@ -229,6 +229,7 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const {
if (!_caption.isEmpty()) { if (!_caption.isEmpty()) {
p.setPen(stm->historyTextFg); p.setPen(stm->historyTextFg);
_parent->prepareCustomEmojiPaint(p, context, _caption); _parent->prepareCustomEmojiPaint(p, context, _caption);
auto highlightRequest = context.computeHighlightCache();
_caption.draw(p, { _caption.draw(p, {
.position = QPoint( .position = QPoint(
st::msgPadding.left(), st::msgPadding.left(),
@ -243,6 +244,7 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const {
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection, .selection = context.selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} else if (!inWebPage) { } else if (!inWebPage) {
auto fullRight = paintx + paintw; auto fullRight = paintx + paintw;

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/cached_round_corners.h" #include "ui/cached_round_corners.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/power_saving.h" #include "ui/power_saving.h"
#include "core/ui_integration.h" #include "core/ui_integration.h"
@ -226,6 +227,13 @@ void Game::draw(Painter &p, const PaintContext &context) const {
Ui::Text::ValidateQuotePaintCache(*cache, _st); Ui::Text::ValidateQuotePaintCache(*cache, _st);
Ui::Text::FillQuotePaint(p, outer, *cache, _st); Ui::Text::FillQuotePaint(p, outer, *cache, _st);
if (_ripple) {
_ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg);
if (_ripple->empty()) {
_ripple = nullptr;
}
}
auto lineHeight = UnitedLineHeight(); auto lineHeight = UnitedLineHeight();
if (_titleLines) { if (_titleLines) {
p.setPen(cache->icon); p.setPen(cache->icon);
@ -322,7 +330,6 @@ TextState Game::textState(QPoint point, StateRequest request) const {
auto tshift = inner.top(); auto tshift = inner.top();
auto paintw = inner.width(); auto paintw = inner.width();
auto inThumb = false;
auto symbolAdd = 0; auto symbolAdd = 0;
auto lineHeight = UnitedLineHeight(); auto lineHeight = UnitedLineHeight();
if (_titleLines) { if (_titleLines) {
@ -353,11 +360,7 @@ TextState Game::textState(QPoint point, StateRequest request) const {
} }
tshift += _descriptionLines * lineHeight; tshift += _descriptionLines * lineHeight;
} }
if (inThumb) { if (_attach) {
if (_parent->data()->isHistoryEntry()) {
result.link = _openl;
}
} else if (_attach) {
auto attachAtTop = !_titleLines && !_descriptionLines; auto attachAtTop = !_titleLines && !_descriptionLines;
if (!attachAtTop) tshift += st::mediaInBubbleSkip; if (!attachAtTop) tshift += st::mediaInBubbleSkip;
@ -375,6 +378,12 @@ TextState Game::textState(QPoint point, StateRequest request) const {
} }
} }
} }
if (_parent->data()->isHistoryEntry()) {
if (!result.link && outer.contains(point)) {
result.link = _openl;
}
}
_lastPoint = point - outer.topLeft();
result.symbol += symbolAdd; result.symbol += symbolAdd;
return result; return result;
@ -399,11 +408,41 @@ void Game::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
} }
void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (p == _openl) {
if (pressed) {
if (!_ripple) {
const auto full = QRect(0, 0, width(), height());
const auto outer = full.marginsRemoved(inBubblePadding());
const auto owner = &parent()->history()->owner();
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
outer.size(),
_st.radius),
[=] { owner->requestViewRepaint(parent()); });
}
_ripple->add(_lastPoint);
} else if (_ripple) {
_ripple->lastStop();
}
}
if (_attach) { if (_attach) {
_attach->clickHandlerPressedChanged(p, pressed); _attach->clickHandlerPressedChanged(p, pressed);
} }
} }
bool Game::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
return _attach && _attach->toggleSelectionByHandlerClick(p);
}
bool Game::allowTextSelectionByHandler(const ClickHandlerPtr &p) const {
return (p == _openl);
}
bool Game::dragItemByHandler(const ClickHandlerPtr &p) const {
return _attach && _attach->dragItemByHandler(p);
}
TextForMimeData Game::selectedText(TextSelection selection) const { TextForMimeData Game::selectedText(TextSelection selection) const {
auto titleResult = _title.toTextForMimeData(selection); auto titleResult = _title.toTextForMimeData(selection);
auto descriptionResult = _description.toTextForMimeData( auto descriptionResult = _description.toTextForMimeData(

View file

@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class ReplyMarkupClickHandler; class ReplyMarkupClickHandler;
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView { namespace HistoryView {
class Game : public Media { class Game : public Media {
@ -35,12 +39,11 @@ public:
return false; // we do not add _title and _description in FullSelection text copy. return false; // we do not add _title and _description in FullSelection text copy.
} }
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { bool toggleSelectionByHandlerClick(
return _attach && _attach->toggleSelectionByHandlerClick(p); const ClickHandlerPtr &p) const override;
} bool allowTextSelectionByHandler(
bool dragItemByHandler(const ClickHandlerPtr &p) const override { const ClickHandlerPtr &p) const override;
return _attach && _attach->dragItemByHandler(p); bool dragItemByHandler(const ClickHandlerPtr &p) const override;
}
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
@ -102,7 +105,9 @@ private:
const not_null<GameData*> _data; const not_null<GameData*> _data;
std::shared_ptr<ReplyMarkupClickHandler> _openl; std::shared_ptr<ReplyMarkupClickHandler> _openl;
std::unique_ptr<Media> _attach; std::unique_ptr<Media> _attach;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable QPoint _lastPoint;
int _gameTagWidth = 0; int _gameTagWidth = 0;
int _descriptionLines = 0; int _descriptionLines = 0;
int _titleLines = 0; int _titleLines = 0;

View file

@ -716,6 +716,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
_parent->width()); _parent->width());
top += botTop->height; top += botTop->height;
} }
auto highlightRequest = context.computeHighlightCache();
_caption.draw(p, { _caption.draw(p, {
.position = QPoint(st::msgPadding.left(), top), .position = QPoint(st::msgPadding.left(), top),
.availableWidth = captionw, .availableWidth = captionw,
@ -728,6 +729,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection, .selection = context.selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} else if (!inWebPage && !skipDrawingSurrounding) { } else if (!inWebPage && !skipDrawingSurrounding) {
auto fullRight = paintx + usex + usew; auto fullRight = paintx + usex + usew;
@ -1203,13 +1205,14 @@ TextForMimeData Gif::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Gif::selectedQuote(TextSelection selection) const { SelectedQuote Gif::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection); return Element::FindSelectedQuote(_caption, selection, _realParent);
} }
TextSelection Gif::selectionFromQuote( TextSelection Gif::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote); return Element::FindSelectionFromQuote(_caption, item, quote);
} }
bool Gif::fullFeaturedGrouped(RectParts sides) const { bool Gif::fullFeaturedGrouped(RectParts sides) const {

View file

@ -68,8 +68,9 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
bool uploading() const override; bool uploading() const override;

View file

@ -181,6 +181,11 @@ Storage::SharedMediaTypesMask Media::sharedMediaTypes() const {
return {}; return {};
} }
bool Media::allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const {
return false;
}
not_null<Element*> Media::parent() const { not_null<Element*> Media::parent() const {
return _parent; return _parent;
} }
@ -189,6 +194,10 @@ not_null<History*> Media::history() const {
return _parent->history(); return _parent->history();
} }
SelectedQuote Media::selectedQuote(TextSelection selection) const {
return {};
}
bool Media::isDisplayed() const { bool Media::isDisplayed() const {
return true; return true;
} }

View file

@ -49,6 +49,7 @@ struct StateRequest;
struct MediaSpoiler; struct MediaSpoiler;
class StickerPlayer; class StickerPlayer;
class Element; class Element;
struct SelectedQuote;
using PaintContext = Ui::ChatPaintContext; using PaintContext = Ui::ChatPaintContext;
@ -88,11 +89,10 @@ public:
TextSelection selection) const { TextSelection selection) const {
return {}; return {};
} }
[[nodiscard]] virtual TextWithEntities selectedQuote( [[nodiscard]] virtual SelectedQuote selectedQuote(
TextSelection selection) const { TextSelection selection) const;
return {};
}
[[nodiscard]] virtual TextSelection selectionFromQuote( [[nodiscard]] virtual TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return {}; return {};
} }
@ -136,6 +136,8 @@ public:
// toggle selection instead of activating the pressed link // toggle selection instead of activating the pressed link
[[nodiscard]] virtual bool toggleSelectionByHandlerClick( [[nodiscard]] virtual bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const = 0; const ClickHandlerPtr &p) const = 0;
[[nodiscard]] virtual bool allowTextSelectionByHandler(
const ClickHandlerPtr &p) const;
[[nodiscard]] virtual TextSelection adjustSelection( [[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection, TextSelection selection,

View file

@ -286,19 +286,45 @@ void GroupedMedia::drawHighlight(
Painter &p, Painter &p,
const PaintContext &context, const PaintContext &context,
int top) const { int top) const {
if (_mode != Mode::Column) { if (context.highlight.opacity == 0.) {
return; return;
} }
auto selection = context.highlight.range;
if (_mode != Mode::Column) {
if (!selection.empty() && !IsSubGroupSelection(selection)) {
_parent->paintCustomHighlight(
p,
context,
top,
height(),
_parent->data().get());
}
return;
}
const auto empty = selection.empty();
const auto subpart = IsSubGroupSelection(selection);
const auto skip = top + groupedPadding().top(); const auto skip = top + groupedPadding().top();
for (auto i = 0, count = int(_parts.size()); i != count; ++i) { for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i]; const auto &part = _parts[i];
const auto rect = part.geometry.translated(0, skip); const auto rect = part.geometry.translated(0, skip);
_parent->paintCustomHighlight( const auto full = (!i && empty)
p, || (subpart && IsGroupItemSelection(selection, i))
context, || (!subpart
rect.y(), && !selection.empty()
rect.height(), && (selection.from < part.content->fullSelectionLength()));
part.item); if (!subpart) {
selection = part.content->skipSelection(selection);
}
if (full) {
auto copy = context;
copy.highlight.range = {};
_parent->paintCustomHighlight(
p,
copy,
rect.y(),
rect.height(),
part.item);
}
} }
} }
@ -316,21 +342,31 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
const auto rounding = inWebPage const auto rounding = inWebPage
? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall }
: adjustedBubbleRoundingWithCaption(_caption); : adjustedBubbleRoundingWithCaption(_caption);
auto highlight = context.highlight.range;
const auto subpartHighlight = IsSubGroupSelection(highlight);
for (auto i = 0, count = int(_parts.size()); i != count; ++i) { for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i]; const auto &part = _parts[i];
const auto partContext = context.withSelection(fullSelection auto partContext = context.withSelection(fullSelection
? FullSelection ? FullSelection
: textSelection : textSelection
? selection ? selection
: IsGroupItemSelection(selection, i) : IsGroupItemSelection(selection, i)
? FullSelection ? FullSelection
: TextSelection()); : TextSelection());
const auto highlighted = (highlight.empty() && !i)
|| IsGroupItemSelection(highlight, i);
const auto highlightOpacity = highlighted
? context.highlight.opacity
: 0.;
partContext.highlight.range = highlighted
? TextSelection()
: highlight;
if (textSelection) { if (textSelection) {
selection = part.content->skipSelection(selection); selection = part.content->skipSelection(selection);
} }
const auto highlightOpacity = (_mode == Mode::Grid) if (!subpartHighlight) {
? _parent->delegate()->elementHighlightOpacity(part.item) highlight = part.content->skipSelection(highlight);
: 0.; }
if (!part.cache.isNull()) { if (!part.cache.isNull()) {
wasCache = true; wasCache = true;
} }
@ -361,6 +397,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
const auto stm = context.messageStyle(); const auto stm = context.messageStyle();
p.setPen(stm->historyTextFg); p.setPen(stm->historyTextFg);
_parent->prepareCustomEmojiPaint(p, context, _caption); _parent->prepareCustomEmojiPaint(p, context, _caption);
auto highlightRequest = context.computeHighlightCache();
_caption.draw(p, { _caption.draw(p, {
.position = QPoint( .position = QPoint(
st::msgPadding.left(), st::msgPadding.left(),
@ -375,6 +412,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection, .selection = context.selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} else if (_parent->media() == this) { } else if (_parent->media() == this) {
auto fullRight = width(); auto fullRight = width();
@ -514,6 +552,7 @@ TextSelection GroupedMedia::adjustSelection(
selection.to = modified.to; selection.to = modified.to;
return selection; return selection;
} }
checked = till;
} }
return selection; return selection;
} }
@ -561,6 +600,50 @@ TextForMimeData GroupedMedia::selectedText(
return result; return result;
} }
SelectedQuote GroupedMedia::selectedQuote(TextSelection selection) const {
if (_mode != Mode::Column) {
return _captionItem
? Element::FindSelectedQuote(_caption, selection, _captionItem)
: SelectedQuote();
}
for (const auto &part : _parts) {
const auto next = part.content->skipSelection(selection);
if (next.to - next.from != selection.to - selection.from) {
if (!next.empty()) {
return SelectedQuote();
}
auto result = part.content->selectedQuote(selection);
result.item = part.item;
return result;
}
selection = next;
}
return {};
}
TextSelection GroupedMedia::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const {
if (_mode != Mode::Column) {
return (_captionItem == item)
? Element::FindSelectionFromQuote(_caption, item, quote)
: TextSelection();
}
const auto i = ranges::find(_parts, item, &Part::item);
if (i == end(_parts)) {
return {};
}
const auto index = int(i - begin(_parts));
auto result = i->content->selectionFromQuote(item, quote);
if (result.empty()) {
return AddGroupItemSelection({}, index);
}
for (auto j = i; j != begin(_parts);) {
result = (--j)->content->unskipSelection(result);
}
return result;
}
auto GroupedMedia::getBubbleSelectionIntervals( auto GroupedMedia::getBubbleSelectionIntervals(
TextSelection selection) const TextSelection selection) const
-> std::vector<Ui::BubbleSelectionInterval> { -> std::vector<Ui::BubbleSelectionInterval> {
@ -663,16 +746,15 @@ bool GroupedMedia::validateGroupParts(
} }
void GroupedMedia::refreshCaption() { void GroupedMedia::refreshCaption() {
using PartPtrOpt = std::optional<const Part*>; const auto part = [&]() -> const Part* {
const auto captionPart = [&]() -> PartPtrOpt {
if (_mode == Mode::Column) { if (_mode == Mode::Column) {
return std::nullopt; return nullptr;
} }
auto result = PartPtrOpt(); auto result = (const Part*)nullptr;
for (const auto &part : _parts) { for (const auto &part : _parts) {
if (!part.item->emptyText()) { if (!part.item->emptyText()) {
if (result) { if (result) {
return std::nullopt; return nullptr;
} else { } else {
result = &part; result = &part;
} }
@ -680,8 +762,7 @@ void GroupedMedia::refreshCaption() {
} }
return result; return result;
}(); }();
if (captionPart) { if (part) {
const auto &part = (*captionPart);
_caption = createCaption(part->item); _caption = createCaption(part->item);
_captionItem = part->item; _captionItem = part->item;
} else { } else {

View file

@ -55,6 +55,10 @@ public:
DocumentData *getDocument() const override; DocumentData *getDocument() const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override;
std::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals( std::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals(
TextSelection selection) const override; TextSelection selection) const override;

View file

@ -104,6 +104,9 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) {
const auto surroundingWidth = _additionalOnTop const auto surroundingWidth = _additionalOnTop
? std::min(newWidth - st::msgReplyPadding.left(), additional) ? std::min(newWidth - st::msgReplyPadding.left(), additional)
: (newWidth - _contentSize.width() - st::msgReplyPadding.left()); : (newWidth - _contentSize.width() - st::msgReplyPadding.left());
if (reply) {
[[maybe_unused]] auto h = reply->resizeToWidth(surroundingWidth);
}
const auto surrounding = surroundingInfo(topic, via, reply, forwarded, surroundingWidth); const auto surrounding = surroundingInfo(topic, via, reply, forwarded, surroundingWidth);
if (_additionalOnTop) { if (_additionalOnTop) {
_topAdded = surrounding.height + st::msgMargin.bottom(); _topAdded = surrounding.height + st::msgMargin.bottom();

View file

@ -401,6 +401,7 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
_parent->width()); _parent->width());
top += botTop->height; top += botTop->height;
} }
auto highlightRequest = context.computeHighlightCache();
_caption.draw(p, { _caption.draw(p, {
.position = QPoint(st::msgPadding.left(), top), .position = QPoint(st::msgPadding.left(), top),
.availableWidth = captionw, .availableWidth = captionw,
@ -413,6 +414,7 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection, .selection = context.selection,
.highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} else if (!inWebPage) { } else if (!inWebPage) {
auto fullRight = paintx + paintw; auto fullRight = paintx + paintw;
@ -1049,13 +1051,14 @@ TextForMimeData Photo::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Photo::selectedQuote(TextSelection selection) const { SelectedQuote Photo::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection); return Element::FindSelectedQuote(_caption, selection, _realParent);
} }
TextSelection Photo::selectionFromQuote( TextSelection Photo::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote); return Element::FindSelectionFromQuote(_caption, item, quote);
} }
void Photo::hideSpoilers() { void Photo::hideSpoilers() {

View file

@ -57,8 +57,9 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
PhotoData *getPhoto() const override { PhotoData *getPhoto() const override {

View file

@ -155,7 +155,7 @@ bool WebPage::HasButton(not_null<WebPageData*> webpage) {
} }
QSize WebPage::countOptimalSize() { QSize WebPage::countOptimalSize() {
if (_data->pendingTill) { if (_data->pendingTill || _data->failed) {
return { 0, 0 }; return { 0, 0 };
} }
@ -366,7 +366,7 @@ QSize WebPage::countOptimalSize() {
} }
QSize WebPage::countCurrentSize(int newWidth) { QSize WebPage::countCurrentSize(int newWidth) {
if (_data->pendingTill) { if (_data->pendingTill || _data->failed) {
return { newWidth, minHeight() }; return { newWidth, minHeight() };
} }
@ -891,6 +891,7 @@ void WebPage::playAnimation(bool autoplay) {
bool WebPage::isDisplayed() const { bool WebPage::isDisplayed() const {
const auto item = _parent->data(); const auto item = _parent->data();
return !_data->pendingTill return !_data->pendingTill
&& !_data->failed
&& !item->Has<HistoryMessageLogEntryOriginal>(); && !item->Has<HistoryMessageLogEntryOriginal>();
} }
@ -903,6 +904,11 @@ bool WebPage::toggleSelectionByHandlerClick(
return _attach && _attach->toggleSelectionByHandlerClick(p); return _attach && _attach->toggleSelectionByHandlerClick(p);
} }
bool WebPage::allowTextSelectionByHandler(
const ClickHandlerPtr &p) const {
return (p == _openl);
}
bool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const { bool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const {
return _attach && _attach->dragItemByHandler(p); return _attach && _attach->dragItemByHandler(p);
} }

View file

@ -50,6 +50,8 @@ public:
bool toggleSelectionByHandlerClick( bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override; const ClickHandlerPtr &p) const override;
bool allowTextSelectionByHandler(
const ClickHandlerPtr &p) const override;
bool dragItemByHandler(const ClickHandlerPtr &p) const override; bool dragItemByHandler(const ClickHandlerPtr &p) const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "info/profile/info_profile_actions.h" #include "info/profile/info_profile_actions.h"
#include "base/options.h"
#include "data/data_peer_values.h" #include "data/data_peer_values.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_folder.h" #include "data/data_folder.h"
@ -77,6 +78,12 @@ namespace Info {
namespace Profile { namespace Profile {
namespace { namespace {
base::options::toggle ShowPeerIdBelowAbout({
.id = kOptionShowPeerIdBelowAbout,
.name = "Show Peer IDs in Profile",
.description = "Show peer IDs from API below their Bio / Description.",
});
[[nodiscard]] rpl::producer<TextWithEntities> UsernamesSubtext( [[nodiscard]] rpl::producer<TextWithEntities> UsernamesSubtext(
not_null<PeerData*> peer, not_null<PeerData*> peer,
rpl::producer<QString> fallback) { rpl::producer<QString> fallback) {
@ -148,6 +155,27 @@ namespace {
return result; return result;
} }
[[nodiscard]] rpl::producer<TextWithEntities> AboutWithIdValue(
not_null<PeerData*> peer) {
return AboutValue(
peer
) | rpl::map([=](TextWithEntities &&value) {
if (!ShowPeerIdBelowAbout.value()) {
return std::move(value);
}
using namespace Ui::Text;
if (!value.empty()) {
value.append("\n");
}
value.append(Italic(u"id: "_q));
const auto raw = peer->id.value & PeerId::kChatTypeMask;
const auto id = QString::number(raw);
value.append(Link(Italic(id), "internal:copy:" + id));
return std::move(value);
});
}
template <typename Text, typename ToggleOn, typename Callback> template <typename Text, typename ToggleOn, typename Callback>
auto AddActionButton( auto AddActionButton(
not_null<Ui::VerticalLayout*> parent, not_null<Ui::VerticalLayout*> parent,
@ -425,8 +453,8 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
? tr::lng_info_about_label() ? tr::lng_info_about_label()
: tr::lng_info_bio_label(); : tr::lng_info_bio_label();
addTranslateToMenu( addTranslateToMenu(
addInfoLine(std::move(label), AboutValue(user)).text, addInfoLine(std::move(label), AboutWithIdValue(user)).text,
AboutValue(user)); AboutWithIdValue(user));
const auto usernameLine = addInfoOneLine( const auto usernameLine = addInfoOneLine(
UsernamesSubtext(_peer, tr::lng_info_username_label()), UsernamesSubtext(_peer, tr::lng_info_username_label()),
@ -576,11 +604,11 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
).text->setLinksTrusted(); ).text->setLinksTrusted();
} }
const auto about = addInfoLine( const auto about = addInfoLine(tr::lng_info_about_label(), _topic
tr::lng_info_about_label(), ? rpl::single(TextWithEntities())
_topic ? rpl::single(TextWithEntities()) : AboutValue(_peer)); : AboutWithIdValue(_peer));
if (!_topic) { if (!_topic) {
addTranslateToMenu(about.text, AboutValue(_peer)); addTranslateToMenu(about.text, AboutWithIdValue(_peer));
} }
if (settings->showPeerId != 0 && !_topic) if (settings->showPeerId != 0 && !_topic)
@ -1129,6 +1157,8 @@ object_ptr<Ui::RpWidget> ActionsFiller::fill() {
} // namespace } // namespace
const char kOptionShowPeerIdBelowAbout[] = "show-peer-id-below-about";
object_ptr<Ui::RpWidget> SetupDetails( object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller, not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,

View file

@ -23,6 +23,8 @@ class Controller;
namespace Info::Profile { namespace Info::Profile {
extern const char kOptionShowPeerIdBelowAbout[];
object_ptr<Ui::RpWidget> SetupDetails( object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller, not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,

View file

@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_values.h"
#include "base/options.h"
#include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_badge.h"
#include "core/application.h" #include "core/application.h"
#include "core/click_handler_types.h" #include "core/click_handler_types.h"
@ -38,13 +37,6 @@ namespace {
using UpdateFlag = Data::PeerUpdate::Flag; using UpdateFlag = Data::PeerUpdate::Flag;
base::options::toggle ShowPeerIdBelowAbout({
.id = kOptionShowPeerIdBelowAbout,
.name = "Show Peer IDs in Profile",
.description = "Show peer IDs from API below their Bio / Description.",
.scope = static_cast<base::options::details::ScopeFlag>(0),
});
auto PlainAboutValue(not_null<PeerData*> peer) { auto PlainAboutValue(not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue( return peer->session().changes().peerFlagsValue(
peer, peer,
@ -95,8 +87,6 @@ void StripExternalLinks(TextWithEntities &text) {
} // namespace } // namespace
const char kOptionShowPeerIdBelowAbout[] = "show-peer-id-below-about";
rpl::producer<QString> NameValue(not_null<PeerData*> peer) { rpl::producer<QString> NameValue(not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue( return peer->session().changes().peerFlagsValue(
peer, peer,
@ -219,16 +209,6 @@ TextWithEntities AboutWithEntities(
if (stripExternal) { if (stripExternal) {
StripExternalLinks(result); StripExternalLinks(result);
} }
if (ShowPeerIdBelowAbout.value()) {
using namespace Ui::Text;
if (!result.empty()) {
result.append("\n");
}
result.append(Italic(u"id: "_q));
const auto raw = peer->id.value & PeerId::kChatTypeMask;
const auto id = QString::number(raw);
result.append(Link(Italic(id), "internal:copy:" + id));
}
return result; return result;
} }

View file

@ -35,8 +35,6 @@ enum class SharedMediaType : signed char;
namespace Info::Profile { namespace Info::Profile {
extern const char kOptionShowPeerIdBelowAbout[];
inline auto ToSingleLine() { inline auto ToSingleLine() {
return rpl::map([](const QString &text) { return rpl::map([](const QString &text) {
return TextUtilities::SingleLine(text); return TextUtilities::SingleLine(text);

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_statistics.h" #include "api/api_statistics.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "base/event_filter.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/history_item.h" #include "history/history_item.h"
@ -28,9 +29,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h" #include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
#include "styles/style_statistics.h" #include "styles/style_statistics.h"
@ -43,6 +46,39 @@ struct Descriptor final {
not_null<QWidget*> toastParent; not_null<QWidget*> toastParent;
}; };
void AddContextMenu(
not_null<Ui::RpWidget*> button,
not_null<Controller*> controller,
not_null<HistoryItem*> item) {
const auto fullId = item->fullId();
const auto contextMenu = button->lifetime()
.make_state<base::unique_qptr<Ui::PopupMenu>>();
const auto showMenu = [=] {
*contextMenu = base::make_unique_q<Ui::PopupMenu>(
button,
st::popupMenuWithIcons);
const auto go = [=] {
const auto &session = controller->parentController();
if (const auto item = session->session().data().message(fullId)) {
session->showMessage(item);
}
};
contextMenu->get()->addAction(
tr::lng_context_to_msg(tr::now),
crl::guard(controller, go),
&st::menuIconShowInChat);
contextMenu->get()->popup(QCursor::pos());
};
base::install_event_filter(button, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::ContextMenu) {
showMenu();
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
}
void ProcessZoom( void ProcessZoom(
const Descriptor &d, const Descriptor &d,
not_null<Statistic::ChartWidget*> widget, not_null<Statistic::ChartWidget*> widget,
@ -544,7 +580,9 @@ void InnerWidget::fill() {
if (_state.stats.message) { if (_state.stats.message) {
if (const auto i = _peer->owner().message(_contextId)) { if (const auto i = _peer->owner().message(_contextId)) {
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
inner->add(object_ptr<MessagePreview>(this, i, -1, -1, QImage())); const auto preview = inner->add(
object_ptr<MessagePreview>(this, i, -1, -1, QImage()));
AddContextMenu(preview, _controller, i);
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
::Settings::AddDivider(inner); ::Settings::AddDivider(inner);
} }
@ -639,6 +677,8 @@ void InnerWidget::fillRecentPosts() {
info.forwardsCount, info.forwardsCount,
std::move(cachedPreview)); std::move(cachedPreview));
AddContextMenu(button, _controller, item);
_messagePreviews.push_back(raw); _messagePreviews.push_back(raw);
raw->show(); raw->show();
button->sizeValue( button->sizeValue(

View file

@ -828,6 +828,19 @@ void AttachWebView::requestWithOptionalConfirm(
void AttachWebView::request(const WebViewButton &button) { void AttachWebView::request(const WebViewButton &button) {
Expects(_context != nullptr && _bot != nullptr); Expects(_context != nullptr && _bot != nullptr);
if (button.fromAttachMenu) {
const auto bot = ranges::find(
_attachBots,
not_null{ _bot },
&AttachWebViewBot::user);
if (bot == end(_attachBots) || bot->inactive) {
requestAddToMenu(_bot, AddToMenuOpenAttach{
.startCommand = button.startCommand,
});
return;
}
}
_startCommand = button.startCommand; _startCommand = button.startCommand;
const auto &action = _context->action; const auto &action = _context->action;
@ -1524,8 +1537,10 @@ void AttachWebView::confirmAddToMenu(
} }
_confirmAddBox = active->show(Box([=](not_null<Ui::GenericBox*> box) { _confirmAddBox = active->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto allowed = std::make_shared<Ui::Checkbox*>(); const auto allowed = std::make_shared<Ui::Checkbox*>();
const auto disclaimer = !disclaimerAccepted(bot);
const auto done = [=](Fn<void()> close) { const auto done = [=](Fn<void()> close) {
const auto state = ((*allowed) && (*allowed)->checked()) const auto state = (disclaimer
|| ((*allowed) && (*allowed)->checked()))
? ToggledState::AllowedToWrite ? ToggledState::AllowedToWrite
: ToggledState::Added; : ToggledState::Added;
toggleInMenu(bot.user, state, [=] { toggleInMenu(bot.user, state, [=] {
@ -1538,13 +1553,22 @@ void AttachWebView::confirmAddToMenu(
}); });
close(); close();
}; };
const auto disclaimer = !disclaimerAccepted(bot);
if (disclaimer) { if (disclaimer) {
FillDisclaimerBox(box, [=] { FillDisclaimerBox(box, [=] {
_disclaimerAccepted.emplace(bot.user); _disclaimerAccepted.emplace(bot.user);
_attachBotsUpdates.fire({}); _attachBotsUpdates.fire({});
done([] {}); done([] {});
}); });
box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::boxRowPadding.left()));
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_bot_will_be_added(
lt_bot,
rpl::single(Ui::Text::Bold(bot.name)),
Ui::Text::WithEntities),
st::boxLabel));
} else { } else {
Ui::ConfirmBox(box, { Ui::ConfirmBox(box, {
(bot.inMainMenu (bot.inMainMenu
@ -1556,40 +1580,26 @@ void AttachWebView::confirmAddToMenu(
Ui::Text::WithEntities), Ui::Text::WithEntities),
done, done,
}); });
} if (bot.requestWriteAccess) {
if (bot.requestWriteAccess) { (*allowed) = box->addRow(
(*allowed) = box->addRow( object_ptr<Ui::Checkbox>(
object_ptr<Ui::Checkbox>( box,
box, tr::lng_url_auth_allow_messages(
tr::lng_url_auth_allow_messages( tr::now,
tr::now, lt_bot,
lt_bot, Ui::Text::Bold(bot.name),
Ui::Text::Bold(bot.name), Ui::Text::WithEntities),
Ui::Text::WithEntities), true,
true, st::urlAuthCheckbox),
st::urlAuthCheckbox), style::margins(
style::margins( st::boxRowPadding.left(),
st::boxRowPadding.left(), (disclaimer
(disclaimer ? st::boxPhotoCaptionSkip
? st::boxPhotoCaptionSkip : st::boxRowPadding.left()),
: st::boxRowPadding.left()), st::boxRowPadding.right(),
st::boxRowPadding.right(), st::boxRowPadding.left()));
st::boxRowPadding.left())); (*allowed)->setAllowTextLines();
(*allowed)->setAllowTextLines();
}
if (disclaimer) {
if (!bot.requestWriteAccess) {
box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::boxRowPadding.left()));
} }
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_bot_will_be_added(
lt_bot,
rpl::single(Ui::Text::Bold(bot.name)),
Ui::Text::WithEntities),
st::boxLabel));
} }
})); }));
} }

View file

@ -134,6 +134,9 @@ public:
[[nodiscard]] rpl::producer<> attachBotsUpdates() const { [[nodiscard]] rpl::producer<> attachBotsUpdates() const {
return _attachBotsUpdates.events(); return _attachBotsUpdates.events();
} }
void notifyBotIconLoaded() {
_attachBotsUpdates.fire({});
}
[[nodiscard]] bool disclaimerAccepted( [[nodiscard]] bool disclaimerAccepted(
const AttachWebViewBot &bot) const; const AttachWebViewBot &bot) const;
[[nodiscard]] bool showMainMenuNewBadge( [[nodiscard]] bool showMainMenuNewBadge(

View file

@ -1411,7 +1411,7 @@ void MainWidget::showHistory(
&& way != Way::Forward) { && way != Way::Forward) {
clearBotStartToken(_history->peer()); clearBotStartToken(_history->peer());
} }
_history->showHistory(peerId, showAtMsgId); _history->showHistory(peerId, showAtMsgId, params.highlightPart);
if (alreadyThatPeer && params.reapplyLocalDraft) { if (alreadyThatPeer && params.reapplyLocalDraft) {
_history->applyDraft(HistoryWidget::FieldHistoryAction::NewEntry); _history->applyDraft(HistoryWidget::FieldHistoryAction::NewEntry);
} }
@ -1772,7 +1772,7 @@ void MainWidget::showNewSection(
} else { } else {
_mainSection = std::move(newMainSection); _mainSection = std::move(newMainSection);
_history->finishAnimating(); _history->finishAnimating();
_history->showHistory(0, 0); _history->showHistory(0, 0, {});
if (const auto entry = _mainSection->activeChat(); entry.key) { if (const auto entry = _mainSection->activeChat(); entry.key) {
_controller->setActiveChatEntry(entry); _controller->setActiveChatEntry(entry);

View file

@ -67,4 +67,8 @@ private:
}; };
inline bool HasMonochromeSetting() {
return false;
}
} // namespace Platform } // namespace Platform

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h" #include "mainwindow.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "calls/calls_instance.h"
#include "core/sandbox.h" #include "core/sandbox.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
@ -175,7 +176,11 @@ ApplicationDelegate *_sharedDelegate = nil;
Core::App().handleAppActivated(); Core::App().handleAppActivated();
if (const auto window = Core::App().activeWindow()) { if (const auto window = Core::App().activeWindow()) {
if (window->widget()->isHidden()) { if (window->widget()->isHidden()) {
window->widget()->showFromTray(); if (Core::App().calls().hasVisiblePanel()) {
Core::App().calls().activateCurrentCall();
} else {
window->widget()->showFromTray();
}
} }
} }
} }

View file

@ -55,4 +55,8 @@ private:
}; };
inline bool HasMonochromeSetting() {
return false;
}
} // namespace Platform } // namespace Platform

View file

@ -11,6 +11,8 @@ namespace Platform {
class Tray; class Tray;
[[nodiscard]] bool HasMonochromeSetting();
} // namespace Platform } // namespace Platform
// Platform dependent implementations. // Platform dependent implementations.

View file

@ -233,18 +233,19 @@ void Tray::updateIcon() {
: !controller->sessionController() : !controller->sessionController()
? nullptr ? nullptr
: &controller->sessionController()->session(); : &controller->sessionController()->session();
const auto monochrome = Core::App().settings().trayIconMonochrome();
const auto supportMode = session && session->supportMode(); const auto supportMode = session && session->supportMode();
const auto iconSizeWidth = GetSystemMetrics(SM_CXSMICON); const auto iconSizeWidth = GetSystemMetrics(SM_CXSMICON);
auto iconSmallPixmap16 = Tray::IconWithCounter( auto iconSmallPixmap16 = Tray::IconWithCounter(
CounterLayerArgs(16, counter, muted), CounterLayerArgs(16, counter, muted),
true, true,
true, monochrome,
supportMode); supportMode);
auto iconSmallPixmap32 = Tray::IconWithCounter( auto iconSmallPixmap32 = Tray::IconWithCounter(
CounterLayerArgs(32, counter, muted), CounterLayerArgs(32, counter, muted),
true, true,
true, monochrome,
supportMode); supportMode);
auto iconSmall = QIcon(); auto iconSmall = QIcon();
iconSmall.addPixmap(iconSmallPixmap16); iconSmall.addPixmap(iconSmallPixmap16);
@ -351,8 +352,15 @@ QPixmap Tray::IconWithCounter(
bool smallIcon, bool smallIcon,
bool monochrome, bool monochrome,
bool supportMode) { bool supportMode) {
return Ui::PixmapFromImage( return Ui::PixmapFromImage(ImageIconWithCounter(
ImageIconWithCounter(std::move(args), supportMode, smallIcon, monochrome)); std::move(args),
supportMode,
smallIcon,
monochrome));
}
bool HasMonochromeSetting() {
return IsDarkTaskbar().has_value();
} }
} // namespace Platform } // namespace Platform

View file

@ -453,18 +453,35 @@ void SetupSystemIntegrationContent(
st::settingsCheckboxPadding)); st::settingsCheckboxPadding));
}; };
const auto settings = &Core::App().settings();
if (Platform::TrayIconSupported()) { if (Platform::TrayIconSupported()) {
const auto trayEnabled = [] { const auto trayEnabled = [=] {
const auto workMode = Core::App().settings().workMode(); const auto workMode = settings->workMode();
return (workMode == WorkMode::TrayOnly) return (workMode == WorkMode::TrayOnly)
|| (workMode == WorkMode::WindowAndTray); || (workMode == WorkMode::WindowAndTray);
}; };
const auto tray = addCheckbox( const auto tray = addCheckbox(
tr::lng_settings_workmode_tray(), tr::lng_settings_workmode_tray(),
trayEnabled()); trayEnabled());
const auto monochrome = Platform::HasMonochromeSetting()
? addSlidingCheckbox(
tr::lng_settings_monochrome_icon(),
settings->trayIconMonochrome())
: nullptr;
if (monochrome) {
monochrome->toggle(tray->checked(), anim::type::instant);
const auto taskbarEnabled = [] { monochrome->entity()->checkedChanges(
const auto workMode = Core::App().settings().workMode(); ) | rpl::filter([=](bool value) {
return (value != settings->trayIconMonochrome());
}) | rpl::start_with_next([=](bool value) {
settings->setTrayIconMonochrome(value);
Core::App().saveSettingsDelayed();
}, monochrome->lifetime());
}
const auto taskbarEnabled = [=] {
const auto workMode = settings->workMode();
return (workMode == WorkMode::WindowOnly) return (workMode == WorkMode::WindowOnly)
|| (workMode == WorkMode::WindowAndTray); || (workMode == WorkMode::WindowAndTray);
}; };
@ -482,10 +499,10 @@ void SetupSystemIntegrationContent(
: WorkMode::WindowOnly; : WorkMode::WindowOnly;
if ((newMode == WorkMode::WindowAndTray if ((newMode == WorkMode::WindowAndTray
|| newMode == WorkMode::TrayOnly) || newMode == WorkMode::TrayOnly)
&& Core::App().settings().workMode() != newMode) { && settings->workMode() != newMode) {
cSetSeenTrayTooltip(false); cSetSeenTrayTooltip(false);
} }
Core::App().settings().setWorkMode(newMode); settings->setWorkMode(newMode);
Core::App().saveSettingsDelayed(); Core::App().saveSettingsDelayed();
}; };
@ -498,6 +515,9 @@ void SetupSystemIntegrationContent(
} else { } else {
updateWorkmode(); updateWorkmode();
} }
if (monochrome) {
monochrome->toggle(checked, anim::type::normal);
}
}, tray->lifetime()); }, tray->lifetime());
if (taskbar) { if (taskbar) {
@ -519,19 +539,19 @@ void SetupSystemIntegrationContent(
tr::lng_settings_mac_warn_before_quit( tr::lng_settings_mac_warn_before_quit(
lt_text, lt_text,
rpl::single(Platform::ConfirmQuit::QuitKeysString())), rpl::single(Platform::ConfirmQuit::QuitKeysString())),
Core::App().settings().macWarnBeforeQuit()); settings->macWarnBeforeQuit());
warnBeforeQuit->checkedChanges( warnBeforeQuit->checkedChanges(
) | rpl::filter([=](bool checked) { ) | rpl::filter([=](bool checked) {
return (checked != Core::App().settings().macWarnBeforeQuit()); return (checked != settings->macWarnBeforeQuit());
}) | rpl::start_with_next([=](bool checked) { }) | rpl::start_with_next([=](bool checked) {
Core::App().settings().setMacWarnBeforeQuit(checked); settings->setMacWarnBeforeQuit(checked);
Core::App().saveSettingsDelayed(); Core::App().saveSettingsDelayed();
}, warnBeforeQuit->lifetime()); }, warnBeforeQuit->lifetime());
#ifndef OS_MAC_STORE #ifndef OS_MAC_STORE
const auto enabled = [] { const auto enabled = [=] {
const auto digest = base::Platform::CurrentCustomAppIconDigest(); const auto digest = base::Platform::CurrentCustomAppIconDigest();
return digest && (Core::App().settings().macRoundIconDigest() == digest); return digest && (settings->macRoundIconDigest() == digest);
}; };
const auto roundIcon = addCheckbox( const auto roundIcon = addCheckbox(
tr::lng_settings_mac_round_icon(), tr::lng_settings_mac_round_icon(),
@ -548,7 +568,7 @@ void SetupSystemIntegrationContent(
} }
Window::OverrideApplicationIcon(checked ? IconMacRound() : QImage()); Window::OverrideApplicationIcon(checked ? IconMacRound() : QImage());
Core::App().refreshApplicationIcon(); Core::App().refreshApplicationIcon();
Core::App().settings().setMacRoundIconDigest(digest); settings->setMacRoundIconDigest(digest);
Core::App().saveSettings(); Core::App().saveSettings();
}, roundIcon->lifetime()); }, roundIcon->lifetime());
#endif // OS_MAC_STORE #endif // OS_MAC_STORE
@ -557,10 +577,10 @@ void SetupSystemIntegrationContent(
if (!Platform::RunInBackground()) { if (!Platform::RunInBackground()) {
const auto closeToTaskbar = addSlidingCheckbox( const auto closeToTaskbar = addSlidingCheckbox(
tr::lng_settings_close_to_taskbar(), tr::lng_settings_close_to_taskbar(),
Core::App().settings().closeToTaskbar()); settings->closeToTaskbar());
const auto closeToTaskbarShown = std::make_shared<rpl::variable<bool>>(false); const auto closeToTaskbarShown = std::make_shared<rpl::variable<bool>>(false);
Core::App().settings().workModeValue( settings->workModeValue(
) | rpl::start_with_next([=](WorkMode workMode) { ) | rpl::start_with_next([=](WorkMode workMode) {
*closeToTaskbarShown = !Core::App().tray().has(); *closeToTaskbarShown = !Core::App().tray().has();
}, closeToTaskbar->lifetime()); }, closeToTaskbar->lifetime());
@ -568,9 +588,9 @@ void SetupSystemIntegrationContent(
closeToTaskbar->toggleOn(closeToTaskbarShown->value()); closeToTaskbar->toggleOn(closeToTaskbarShown->value());
closeToTaskbar->entity()->checkedChanges( closeToTaskbar->entity()->checkedChanges(
) | rpl::filter([=](bool checked) { ) | rpl::filter([=](bool checked) {
return (checked != Core::App().settings().closeToTaskbar()); return (checked != settings->closeToTaskbar());
}) | rpl::start_with_next([=](bool checked) { }) | rpl::start_with_next([=](bool checked) {
Core::App().settings().setCloseToTaskbar(checked); settings->setCloseToTaskbar(checked);
Local::writeSettings(); Local::writeSettings();
}, closeToTaskbar->lifetime()); }, closeToTaskbar->lifetime());
} }

View file

@ -19,7 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/launcher.h" #include "core/launcher.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_widget.h"
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_actions.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "media/player/media_player_instance.h" #include "media/player/media_player_instance.h"

View file

@ -1018,9 +1018,9 @@ void ChartWidget::updateBottomDates() {
const auto k = _chartArea->width() / d; const auto k = _chartArea->width() / d;
const auto stepRaw = int(k / 6); const auto stepRaw = int(k / 6);
const auto by = int(_chartArea->width() / float64(_chartData.x.size()));
_bottomLine.captionIndicesOffset = 0 _bottomLine.captionIndicesOffset = 0
+ st::statisticsChartBottomCaptionMaxWidth + st::statisticsChartBottomCaptionMaxWidth / std::max(by, 1);
/ int(_chartArea->width() / float64(_chartData.x.size()));
const auto isCurrentNull = (_bottomLine.current.stepMinFast == 0); const auto isCurrentNull = (_bottomLine.current.stepMinFast == 0);
if (!isCurrentNull if (!isCurrentNull

View file

@ -46,6 +46,11 @@ void Tray::create() {
} }
}, _tray.lifetime()); }, _tray.lifetime());
Core::App().settings().trayIconMonochromeChanges(
) | rpl::start_with_next([=] {
updateIconCounters();
}, _tray.lifetime());
Core::App().passcodeLockChanges( Core::App().passcodeLockChanges(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
rebuildMenu(); rebuildMenu();

View file

@ -146,6 +146,7 @@ outReplyTextPaletteSelected: TextPalette(outTextPaletteSelected) {
} }
imgReplyTextPalette: TextPalette(defaultTextPalette) { imgReplyTextPalette: TextPalette(defaultTextPalette) {
linkFg: msgImgReplyBarColor; linkFg: msgImgReplyBarColor;
monoFg: msgImgReplyBarColor;
} }
inSemiboldPalette: TextPalette(inTextPalette) { inSemiboldPalette: TextPalette(inTextPalette) {
linkFg: msgInServiceFg; linkFg: msgInServiceFg;

View file

@ -142,6 +142,12 @@ struct BackgroundEmojiData {
uint8 colorIndexPlusOne); uint8 colorIndexPlusOne);
}; };
struct ChatPaintHighlight {
float64 opacity = 0.;
float64 collapsion = 0.;
TextSelection range;
};
struct ChatPaintContext { struct ChatPaintContext {
not_null<const ChatStyle*> st; not_null<const ChatStyle*> st;
const BubblePattern *bubblesPattern = nullptr; const BubblePattern *bubblesPattern = nullptr;
@ -149,11 +155,15 @@ struct ChatPaintContext {
QRect viewport; QRect viewport;
QRect clip; QRect clip;
TextSelection selection; TextSelection selection;
ChatPaintHighlight highlight;
QPainterPath *highlightPathCache = nullptr;
mutable QRect highlightInterpolateTo;
crl::time now = 0; crl::time now = 0;
void translate(int x, int y) { void translate(int x, int y) {
viewport.translate(x, y); viewport.translate(x, y);
clip.translate(x, y); clip.translate(x, y);
highlightInterpolateTo.translate(x, y);
} }
void translate(QPoint point) { void translate(QPoint point) {
translate(point.x(), point.y()); translate(point.x(), point.y());
@ -181,6 +191,19 @@ struct ChatPaintContext {
result.selection = selection; result.selection = selection;
return result; return result;
} }
[[nodiscard]] auto computeHighlightCache() const
-> std::optional<Ui::Text::HighlightInfoRequest> {
if (highlight.range.empty() || highlight.collapsion <= 0.) {
return {};
}
return Ui::Text::HighlightInfoRequest{
.range = highlight.range,
.interpolateTo = highlightInterpolateTo,
.interpolateProgress = (1. - highlight.collapsion),
.outPath = highlightPathCache,
};
};
// This is supported only in unwrapped media for now. // This is supported only in unwrapped media for now.
enum class SkipDrawingParts { enum class SkipDrawingParts {

View file

@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_basic.h" #include "styles/style_basic.h"
namespace Ui { namespace Ui {
namespace {
constexpr auto kAlmostIndex = float64(.99);
} // namespace
PickerAnimation::PickerAnimation() = default; PickerAnimation::PickerAnimation() = default;
@ -26,6 +31,23 @@ void PickerAnimation::jumpToOffset(int offset) {
value); value);
_updates.fire(_result.current - was); _updates.fire(_result.current - was);
}; };
if (anim::Disabled()) {
auto value = float64(0.);
const auto diff = _result.to - _result.from;
const auto step = std::min(
kAlmostIndex,
1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));
while (true) {
value += step;
if (value >= 1.) {
callback(1.);
break;
} else {
callback(value);
}
}
return;
}
_animation.start( _animation.start(
std::move(callback), std::move(callback),
0., 0.,
@ -94,20 +116,14 @@ VerticalDrumPicker::VerticalDrumPicker(
}, lifetime()); }, lifetime());
_animation.updates( _animation.updates(
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](PickerAnimation::Shift shift) { ) | rpl::start_with_next([=](PickerAnimation::Shift shift) {
increaseShift(shift); increaseShift(shift);
if (anim::Disabled()) {
animationDataFromIndex();
_animation.jumpToOffset(0);
}
}, lifetime()); }, lifetime());
} }
void VerticalDrumPicker::increaseShift(float64 by) { void VerticalDrumPicker::increaseShift(float64 by) {
{ {
// Guard input. // Guard input.
constexpr auto kAlmostIndex = .99;
if (by >= 1.) { if (by >= 1.) {
by = kAlmostIndex; by = kAlmostIndex;
} else if (by <= -1.) { } else if (by <= -1.) {

View file

@ -59,6 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h" #include "main/main_domain.h"
#include "mtproto/mtp_instance.h" #include "mtproto/mtp_instance.h"
#include "mtproto/mtproto_config.h" #include "mtproto/mtproto_config.h"
#include "data/data_document_media.h"
#include "data/data_folder.h" #include "data/data_folder.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -216,6 +217,9 @@ void SetupMenuBots(
const auto wrap = container->add( const auto wrap = container->add(
object_ptr<Ui::VerticalLayout>(container)); object_ptr<Ui::VerticalLayout>(container));
const auto bots = &controller->session().attachWebView(); const auto bots = &controller->session().attachWebView();
const auto iconLoadLifetime = wrap->lifetime().make_state<
rpl::lifetime
>();
rpl::single( rpl::single(
rpl::empty rpl::empty
@ -225,7 +229,20 @@ void SetupMenuBots(
const auto width = container->widthNoMargins(); const auto width = container->widthNoMargins();
wrap->clear(); wrap->clear();
for (const auto &bot : bots->attachBots()) { for (const auto &bot : bots->attachBots()) {
if (!bot.inMainMenu) { const auto user = bot.user;
if (!bot.inMainMenu || !bot.media) {
continue;
} else if (const auto media = bot.media; !media->loaded()) {
if (!*iconLoadLifetime) {
auto &session = user->session();
*iconLoadLifetime = session.downloaderTaskFinished(
) | rpl::start_with_next([=] {
if (media->loaded()) {
iconLoadLifetime->destroy();
bots->notifyBotIconLoaded();
}
});
}
continue; continue;
} }
const auto button = Settings::AddButton( const auto button = Settings::AddButton(
@ -244,7 +261,6 @@ void SetupMenuBots(
st::mainMenuButton.iconLeft, st::mainMenuButton.iconLeft,
(height - icon->height()) / 2); (height - icon->height()) / 2);
}, button->lifetime()); }, button->lifetime());
const auto user = bot.user;
const auto weak = Ui::MakeWeak(container); const auto weak = Ui::MakeWeak(container);
button->setAcceptBoth(true); button->setAcceptBoth(true);
button->clicks( button->clicks(

View file

@ -689,6 +689,7 @@ void SessionNavigation::applyBoost(
done(false); done(false);
return; return;
} }
auto slot = int();
auto different = PeerId(); auto different = PeerId();
auto earliest = TimeId(-1); auto earliest = TimeId(-1);
const auto now = base::unixtime::now(); const auto now = base::unixtime::now();
@ -699,13 +700,13 @@ void SessionNavigation::applyBoost(
? peerFromMTP(*data.vpeer()) ? peerFromMTP(*data.vpeer())
: PeerId(); : PeerId();
if (!peerId && cooldown <= now) { if (!peerId && cooldown <= now) {
applyBoostChecked(channel, done); applyBoostChecked(channel, data.vslot().v, done);
return; return;
} else if (peerId != channel->id) { } else if (peerId != channel->id
&& (earliest < 0 || cooldown < earliest)) {
slot = data.vslot().v;
different = peerId; different = peerId;
if (earliest < 0 || cooldown < earliest) { earliest = cooldown;
earliest = cooldown;
}
} }
} }
if (different) { if (different) {
@ -731,7 +732,7 @@ void SessionNavigation::applyBoost(
done(false); done(false);
} else { } else {
const auto peer = _session->data().peer(different); const auto peer = _session->data().peer(different);
replaceBoostConfirm(peer, channel, done); replaceBoostConfirm(peer, channel, slot, done);
} }
} else { } else {
uiShow()->show(Ui::MakeConfirmBox({ uiShow()->show(Ui::MakeConfirmBox({
@ -752,11 +753,12 @@ void SessionNavigation::applyBoost(
void SessionNavigation::replaceBoostConfirm( void SessionNavigation::replaceBoostConfirm(
not_null<PeerData*> from, not_null<PeerData*> from,
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
int slot,
Fn<void(bool)> done) { Fn<void(bool)> done) {
const auto forwarded = std::make_shared<bool>(false); const auto forwarded = std::make_shared<bool>(false);
const auto confirmed = [=](Fn<void()> close) { const auto confirmed = [=](Fn<void()> close) {
*forwarded = true; *forwarded = true;
applyBoostChecked(channel, done); applyBoostChecked(channel, slot, done);
close(); close();
}; };
const auto box = uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) { const auto box = uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {
@ -785,10 +787,11 @@ void SessionNavigation::replaceBoostConfirm(
void SessionNavigation::applyBoostChecked( void SessionNavigation::applyBoostChecked(
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
int slot,
Fn<void(bool)> done) { Fn<void(bool)> done) {
_api.request(MTPpremium_ApplyBoost( _api.request(MTPpremium_ApplyBoost(
MTP_flags(0), MTP_flags(MTPpremium_ApplyBoost::Flag::f_slots),
MTPVector<MTPint>(), // slots MTP_vector<MTPint>({ MTP_int(slot) }),
channel->input channel->input
)).done([=](const MTPpremium_MyBoosts &result) { )).done([=](const MTPpremium_MyBoosts &result) {
done(true); done(true);
@ -854,7 +857,8 @@ void SessionNavigation::showRepliesForMessage(
auto memento = std::make_shared<HistoryView::RepliesMemento>( auto memento = std::make_shared<HistoryView::RepliesMemento>(
history, history,
rootId, rootId,
commentId); commentId,
params.highlightPart);
memento->setFromTopic(topic); memento->setFromTopic(topic);
showSection(std::move(memento), params); showSection(std::move(memento), params);
return; return;

View file

@ -166,6 +166,7 @@ struct SectionShow {
return copy; return copy;
} }
TextWithEntities highlightPart;
Way way = Way::Forward; Way way = Way::Forward;
anim::type animated = anim::type::normal; anim::type animated = anim::type::normal;
anim::activation activation = anim::activation::normal; anim::activation activation = anim::activation::normal;
@ -317,9 +318,11 @@ private:
void replaceBoostConfirm( void replaceBoostConfirm(
not_null<PeerData*> from, not_null<PeerData*> from,
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
int slot,
Fn<void(bool)> done); Fn<void(bool)> done);
void applyBoostChecked( void applyBoostChecked(
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
int slot,
Fn<void(bool)> done); Fn<void(bool)> done);
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;

View file

@ -1211,7 +1211,7 @@ stage('crashpad', """
mac: mac:
git clone https://github.com/desktop-app/crashpad.git git clone https://github.com/desktop-app/crashpad.git
cd crashpad cd crashpad
git checkout 171b601938 git checkout f07f49e287
git submodule init git submodule init
git submodule update third_party/mini_chromium git submodule update third_party/mini_chromium
ZLIB_PATH=$USED_PREFIX/include ZLIB_PATH=$USED_PREFIX/include

View file

@ -1,7 +1,7 @@
AppVersion 4011001 AppVersion 4011003
AppVersionStrMajor 4.11 AppVersionStrMajor 4.11
AppVersionStrSmall 4.11.1 AppVersionStrSmall 4.11.3
AppVersionStr 4.11.1 AppVersionStr 4.11.3
BetaChannel 0 BetaChannel 0
AlphaVersion 0 AlphaVersion 0
AppVersionOriginal 4.11.1 AppVersionOriginal 4.11.3

View file

@ -1,3 +1,17 @@
4.11.3 (02.11.23)
- Fix adding a link to media captions in scheduled / comments.
- Fix crash in link preview options saving.
- Fix possible crash in statistics.
4.11.2 (01.11.23)
- Highlight quoted parts in jump-to-message from replies.
- Ctrl+Click on message field reply bar to jump to message.
- Fix empty link preview displaying when generation failed.
- Fix external replies in topic groups.
- Allow enabling legacy tray icon on Windows.
4.11.1 (29.10.23) 4.11.1 (29.10.23)
- Fix crash in emoji status select. - Fix crash in emoji status select.