Merge branch 'AyuGram:dev' into dev

This commit is contained in:
Andrey Bash 2025-07-20 22:55:19 -07:00 committed by GitHub
commit 7761ff070e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 4683 additions and 1937 deletions

View file

@ -1339,6 +1339,8 @@ PRIVATE
media/view/media_view_playback_controls.h media/view/media_view_playback_controls.h
media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.cpp
media/view/media_view_playback_progress.h media/view/media_view_playback_progress.h
media/view/media_view_playback_sponsored.cpp
media/view/media_view_playback_sponsored.h
media/system_media_controls_manager.h media/system_media_controls_manager.h
media/system_media_controls_manager.cpp media/system_media_controls_manager.cpp
menu/menu_antispam_validator.cpp menu/menu_antispam_validator.cpp
@ -2071,7 +2073,7 @@ if (MSVC)
/DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib /DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
/DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll # /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 470 B

View file

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 899 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,10 +1,10 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1067_20)"> <g clip-path="url(#clip0_1362_52)">
<path d="M7.58588 9.73341C8.52939 10.6769 9.41338 12.269 10.085 13.6905C10.5357 14.6445 12.0164 14.6078 12.3503 13.6068L15.8086 3.23922C16.1618 2.1804 15.1541 1.17304 14.0954 1.52654L3.74092 4.98387C2.73902 5.3184 2.70382 6.80252 3.65913 7.2532C5.0706 7.91907 6.64716 8.79469 7.58588 9.73341Z" fill="white"/> <path d="M6.92088 9.06526C7.86439 10.0088 8.74838 11.6009 9.41997 13.0223C9.87073 13.9764 11.3514 13.9397 11.6853 12.9387L15.1436 2.57106C15.4968 1.51225 14.4891 0.504885 13.4304 0.858384L3.07592 4.31571C2.07402 4.65025 2.03882 6.13437 2.99412 6.58505C4.4056 7.25092 5.98216 8.12654 6.92088 9.06526Z" fill="white"/>
</g> </g>
<defs> <defs>
<clipPath id="clip0_1067_20"> <clipPath id="clip0_1362_52">
<rect width="16" height="16.0037" fill="white" transform="translate(0.665001 0.668152)"/> <rect width="16" height="16.0037" fill="white"/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 566 B

View file

@ -434,6 +434,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_dlg_new_channel_name" = "Channel name"; "lng_dlg_new_channel_name" = "Channel name";
"lng_dlg_new_bot_name" = "Bot name"; "lng_dlg_new_bot_name" = "Bot name";
"lng_no_chats" = "Your chats will be here"; "lng_no_chats" = "Your chats will be here";
"lng_no_conversations" = "You have no\nconversations yet.";
"lng_no_conversations_button" = "New Message";
"lng_no_conversations_subtitle" = "Your contacts on Telegram";
"lng_no_chats_filter" = "No chats currently belong to this folder."; "lng_no_chats_filter" = "No chats currently belong to this folder.";
"lng_no_saved_sublists" = "You can save messages from other chats here."; "lng_no_saved_sublists" = "You can save messages from other chats here.";
"lng_contacts_loading" = "Loading..."; "lng_contacts_loading" = "Loading...";
@ -4260,6 +4263,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_to_msg" = "Go To Message"; "lng_context_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply"; "lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_edit_msg" = "Edit"; "lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check"; "lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit Fact Check"; "lng_context_edit_factcheck" = "Edit Fact Check";
@ -4450,6 +4454,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_inline_switch_cant" = "Sorry, no way to write here :("; "lng_inline_switch_cant" = "Sorry, no way to write here :(";
"lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to" = "Reply to {name}";
"lng_preview_reply_to_quote" = "Reply to quote from {name}"; "lng_preview_reply_to_quote" = "Reply to quote from {name}";
"lng_preview_reply_to_task" = "Reply to task from {title}";
"lng_suggest_bar_title" = "Suggest a Post Below"; "lng_suggest_bar_title" = "Suggest a Post Below";
"lng_suggest_bar_text" = "Click to offer a price for publishing."; "lng_suggest_bar_text" = "Click to offer a price for publishing.";

View file

@ -38,6 +38,7 @@
<file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file> <file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file>
<file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file> <file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file>
<file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file> <file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file>
<file alias="no_chats.tgs">../../animations/no_chats.tgs</file>
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file> <file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file> <file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>

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="5.16.3.0" /> Version="5.16.4.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 5,16,3,0 FILEVERSION 5,16,4,0
PRODUCTVERSION 5,16,3,0 PRODUCTVERSION 5,16,4,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", "5.16.3.0" VALUE "FileVersion", "5.16.4.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.16.3.0" VALUE "ProductVersion", "5.16.4.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 5,16,3,0 FILEVERSION 5,16,4,0
PRODUCTVERSION 5,16,3,0 PRODUCTVERSION 5,16,4,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", "5.16.3.0" VALUE "FileVersion", "5.16.4.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.16.3.0" VALUE "ProductVersion", "5.16.4.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -47,10 +47,10 @@ bool UnreadThings::trackReactions(Data::Thread *thread) const {
return false; return false;
} }
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
if (peer->isChannel() && !peer->isMegagroup() && !settings.hideChannelReactions) { if (peer->isChannel() && !peer->isMegagroup() && !settings.showChannelReactions) {
return false; return false;
} }
if (peer->isMegagroup() && !settings.hideGroupReactions) { if (peer->isMegagroup() && !settings.showGroupReactions) {
return false; return false;
} }
return peer->isUser() || peer->isChat() || peer->isMegagroup(); return peer->isUser() || peer->isChat() || peer->isMegagroup();

View file

@ -231,8 +231,8 @@ AyuGramSettings::AyuGramSettings() {
disableNotificationsDelay = false; disableNotificationsDelay = false;
localPremium = false; localPremium = false;
hideChannelReactions = true; showChannelReactions = true;
hideGroupReactions = true; showGroupReactions = true;
// ~ Customization // ~ Customization
appIcon = appIcon =
@ -420,11 +420,11 @@ void set_localPremium(bool val) {
} }
void set_hideChannelReactions(bool val) { void set_hideChannelReactions(bool val) {
settings->hideChannelReactions = val; settings->showChannelReactions = val;
} }
void set_hideGroupReactions(bool val) { void set_hideGroupReactions(bool val) {
settings->hideGroupReactions = val; settings->showGroupReactions = val;
} }
void set_appIcon(const QString &val) { void set_appIcon(const QString &val) {

View file

@ -76,8 +76,8 @@ public:
bool disableNotificationsDelay; bool disableNotificationsDelay;
bool localPremium; bool localPremium;
bool hideChannelReactions; bool showChannelReactions;
bool hideGroupReactions; bool showGroupReactions;
QString appIcon; QString appIcon;
bool simpleQuotesAndReplies; bool simpleQuotesAndReplies;
@ -259,8 +259,8 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(showGhostToggleInTray) NLOHMANN_JSON_TO(showGhostToggleInTray)
NLOHMANN_JSON_TO(showStreamerToggleInTray) NLOHMANN_JSON_TO(showStreamerToggleInTray)
NLOHMANN_JSON_TO(monoFont) NLOHMANN_JSON_TO(monoFont)
NLOHMANN_JSON_TO(hideChannelReactions) NLOHMANN_JSON_TO(showChannelReactions)
NLOHMANN_JSON_TO(hideGroupReactions) NLOHMANN_JSON_TO(showGroupReactions)
NLOHMANN_JSON_TO(hideNotificationCounters) NLOHMANN_JSON_TO(hideNotificationCounters)
NLOHMANN_JSON_TO(hideNotificationBadge) NLOHMANN_JSON_TO(hideNotificationBadge)
NLOHMANN_JSON_TO(hideAllChatsFolder) NLOHMANN_JSON_TO(hideAllChatsFolder)
@ -324,8 +324,8 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInTray) NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInTray)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInTray) NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInTray)
NLOHMANN_JSON_FROM_WITH_DEFAULT(monoFont) NLOHMANN_JSON_FROM_WITH_DEFAULT(monoFont)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideChannelReactions) NLOHMANN_JSON_FROM_WITH_DEFAULT(showChannelReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideGroupReactions) NLOHMANN_JSON_FROM_WITH_DEFAULT(showGroupReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationCounters) NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationCounters)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationBadge) NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationBadge)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder) NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder)

View file

@ -99,8 +99,11 @@ std::pair<QString, QString> stateName(const PeerId &id) {
void ForwardState::updateBottomBar(const Main::Session &session, const PeerId *peer, const State &st) { void ForwardState::updateBottomBar(const Main::Session &session, const PeerId *peer, const State &st) {
state = st; state = st;
auto peerCopy = *peer;
session.changes().peerUpdated(session.data().peer(*peer), Data::PeerUpdate::Flag::Rights); crl::on_main([&, peerCopy]
{
session.changes().peerUpdated(session.data().peer(peerCopy), Data::PeerUpdate::Flag::Rights);
});
} }
static Ui::PreparedList prepareMedia(not_null<Main::Session*> session, static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
@ -111,6 +114,10 @@ static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
{ {
groupMedia.emplace_back(media); groupMedia.emplace_back(media);
auto prepared = Ui::PreparedFile(AyuSync::filePath(session, media)); auto prepared = Ui::PreparedFile(AyuSync::filePath(session, media));
if (prepared.path.isEmpty()) {
// otherwise will fail assertion in PrepareDetails
return prepared;
}
Storage::PrepareDetails(prepared, st::sendMediaPreviewSize, PhotoSideLimit()); Storage::PrepareDetails(prepared, st::sendMediaPreviewSize, PhotoSideLimit());
return prepared; return prepared;
}; };
@ -120,7 +127,9 @@ static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
const auto groupId = startItem->groupId(); const auto groupId = startItem->groupId();
Ui::PreparedList list; Ui::PreparedList list;
list.files.emplace_back(prepare(media)); if (auto prepared = prepare(media); !prepared.path.isEmpty()) {
list.files.emplace_back(std::move(prepared));
}
if (!groupId.value) { if (!groupId.value) {
return list; return list;
@ -132,7 +141,9 @@ static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
break; break;
} }
if (const auto nextMedia = nextItem->media()) { if (const auto nextMedia = nextItem->media()) {
list.files.emplace_back(prepare(nextMedia)); if (auto prepared = prepare(nextMedia); !prepared.path.isEmpty()) {
list.files.emplace_back(std::move(prepared));
}
i = k; i = k;
} }
} }
@ -296,7 +307,10 @@ void forwardMessages(
const auto history = action.history; const auto history = action.history;
const auto peer = history->peer; const auto peer = history->peer;
crl::on_main([&]
{
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {}); history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
});
std::shared_ptr<ForwardState> state; std::shared_ptr<ForwardState> state;

View file

@ -5,8 +5,9 @@
// //
// Copyright @Radolyn, 2025 // Copyright @Radolyn, 2025
#include "ayu_sync.h" #include "ayu_sync.h"
#include "apiwrap.h"
#include "api/api_sending.h" #include "api/api_sending.h"
#include "apiwrap.h"
#include "ayu/utils/telegram_helpers.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "core/file_utilities.h" #include "core/file_utilities.h"
@ -127,6 +128,8 @@ void loadDocumentSync(not_null<Main::Session*> session, DocumentData *data, not_
auto latch = std::make_shared<TimedCountDownLatch>(1); auto latch = std::make_shared<TimedCountDownLatch>(1);
auto lifetime = std::make_shared<rpl::lifetime>(); auto lifetime = std::make_shared<rpl::lifetime>();
crl::on_main([&]
{
data->save(Data::FileOriginMessage(item->fullId()), filePath(session, item->media())); data->save(Data::FileOriginMessage(item->fullId()), filePath(session, item->media()));
rpl::single() | rpl::then( rpl::single() | rpl::then(
@ -140,6 +143,7 @@ void loadDocumentSync(not_null<Main::Session*> session, DocumentData *data, not_
base::take(lifetime)->destroy(); base::take(lifetime)->destroy();
}, },
*lifetime); *lifetime);
});
latch->await(std::chrono::minutes(5)); latch->await(std::chrono::minutes(5));
} }
@ -207,6 +211,8 @@ void loadPhotoSync(not_null<Main::Session*> session, const std::pair<not_null<Ph
if (finalCheck()) { if (finalCheck()) {
saveToFiles(); saveToFiles();
} else { } else {
crl::on_main([&]
{
session->downloaderTaskFinished() | rpl::filter([&] session->downloaderTaskFinished() | rpl::filter([&]
{ {
return finalCheck(); return finalCheck();
@ -217,10 +223,10 @@ void loadPhotoSync(not_null<Main::Session*> session, const std::pair<not_null<Ph
base::take(lifetime)->destroy(); base::take(lifetime)->destroy();
}, },
*lifetime); *lifetime);
} });
latch->await(std::chrono::minutes(5)); latch->await(std::chrono::minutes(5));
} }
}
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &message) { void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &message) {
crl::on_main([=, &message] crl::on_main([=, &message]
@ -240,7 +246,8 @@ void waitForMsgSync(not_null<Main::Session*> session, const Api::SendAction &act
auto latch = std::make_shared<TimedCountDownLatch>(1); auto latch = std::make_shared<TimedCountDownLatch>(1);
auto lifetime = std::make_shared<rpl::lifetime>(); auto lifetime = std::make_shared<rpl::lifetime>();
crl::on_main([&]
{
session->data().itemIdChanged() session->data().itemIdChanged()
| rpl::filter([&](const Data::Session::IdChange &update) | rpl::filter([&](const Data::Session::IdChange &update)
{ {
@ -251,6 +258,7 @@ void waitForMsgSync(not_null<Main::Session*> session, const Api::SendAction &act
base::take(lifetime)->destroy(); base::take(lifetime)->destroy();
}, },
*lifetime); *lifetime);
});
latch->await(std::chrono::minutes(2)); latch->await(std::chrono::minutes(2));
} }
@ -260,9 +268,6 @@ void sendDocumentSync(not_null<Main::Session*> session,
SendMediaType type, SendMediaType type,
TextWithTags &&caption, TextWithTags &&caption,
const Api::SendAction &action) { const Api::SendAction &action) {
const auto size = group.list.files.size();
auto latch = std::make_shared<TimedCountDownLatch>(size);
auto lifetime = std::make_shared<rpl::lifetime>();
auto groupId = std::make_shared<SendingAlbum>(); auto groupId = std::make_shared<SendingAlbum>();
groupId->groupId = base::RandomValue<uint64>(); groupId->groupId = base::RandomValue<uint64>();
@ -272,27 +277,7 @@ void sendDocumentSync(not_null<Main::Session*> session,
session->api().sendFiles(std::move(lst), type, std::move(caption), groupId, action); session->api().sendFiles(std::move(lst), type, std::move(caption), groupId, action);
}); });
waitForMsgSync(session, action);
// probably need to handle
// session->uploader().photoFailed()
// and
// session->uploader().documentFailed()
// too
rpl::merge(
session->uploader().documentReady(),
session->uploader().photoReady()
) | rpl::filter([&](const Storage::UploadedMedia &docOrPhoto)
{
return docOrPhoto.fullId.peer == action.history->peer->id;
}) | rpl::start_with_next([&]
{
latch->countDown();
},
*lifetime);
latch->await(std::chrono::minutes(5 * size));
base::take(lifetime)->destroy();
} }
void sendStickerSync(not_null<Main::Session*> session, void sendStickerSync(not_null<Main::Session*> session,

View file

@ -209,8 +209,7 @@ bool MessageShotDelegate::elementHideReply(not_null<const HistoryView::Element*>
HistoryView::ElementChatMode MessageShotDelegate::elementChatMode() { HistoryView::ElementChatMode MessageShotDelegate::elementChatMode() {
using Mode = HistoryView::ElementChatMode; using Mode = HistoryView::ElementChatMode;
// Mode::Wide; return Mode::Wide;
return Mode::Default;
} }
QImage removeEmptySpaceAround(const QImage &original) { QImage removeEmptySpaceAround(const QImage &original) {

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
** This file is an amalgamation of many separate C source files from SQLite ** This file is an amalgamation of many separate C source files from SQLite
** version 3.50.0. By combining all the individual C code files into this ** version 3.50.2. By combining all the individual C code files into this
** single large file, the entire code can be compiled as a single translation ** single large file, the entire code can be compiled as a single translation
** unit. This allows many compilers to do optimizations that would not be ** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements ** possible if the files were compiled separately. Performance improvements
@ -18,7 +18,7 @@
** separate file. This file contains only code for the core SQLite library. ** separate file. This file contains only code for the core SQLite library.
** **
** The content in this amalgamation comes from Fossil check-in ** The content in this amalgamation comes from Fossil check-in
** dfc790f998f450d9c35e3ba1c8c89c17466c with changes in files: ** 2af157d77fb1304a74176eaee7fbc7c7e932 with changes in files:
** **
** **
*/ */
@ -465,9 +465,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.50.0" #define SQLITE_VERSION "3.50.2"
#define SQLITE_VERSION_NUMBER 3050000 #define SQLITE_VERSION_NUMBER 3050002
#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742" #define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -4398,7 +4398,7 @@ SQLITE_API sqlite3_file *sqlite3_database_file_object(const char*);
** **
** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of ** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of
** database filename D with corresponding journal file J and WAL file W and ** database filename D with corresponding journal file J and WAL file W and
** with N URI parameters key/values pairs in the array P. The result from ** an array P of N URI Key/Value pairs. The result from
** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that ** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that
** is safe to pass to routines like: ** is safe to pass to routines like:
** <ul> ** <ul>
@ -5079,7 +5079,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants, ** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants,
** literals may be replaced by a [parameter] that matches one of following ** literals may be replaced by a [parameter] that matches one of the following
** templates: ** templates:
** **
** <ul> ** <ul>
@ -5124,7 +5124,7 @@ typedef struct sqlite3_context sqlite3_context;
** **
** [[byte-order determination rules]] ^The byte-order of ** [[byte-order determination rules]] ^The byte-order of
** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF) ** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF)
** found in first character, which is removed, or in the absence of a BOM ** found in the first character, which is removed, or in the absence of a BOM
** the byte order is the native byte order of the host ** the byte order is the native byte order of the host
** machine for sqlite3_bind_text16() or the byte order specified in ** machine for sqlite3_bind_text16() or the byte order specified in
** the 6th parameter for sqlite3_bind_text64().)^ ** the 6th parameter for sqlite3_bind_text64().)^
@ -5144,7 +5144,7 @@ typedef struct sqlite3_context sqlite3_context;
** or sqlite3_bind_text16() or sqlite3_bind_text64() then ** or sqlite3_bind_text16() or sqlite3_bind_text64() then
** that parameter must be the byte offset ** that parameter must be the byte offset
** where the NUL terminator would occur assuming the string were NUL ** where the NUL terminator would occur assuming the string were NUL
** terminated. If any NUL characters occurs at byte offsets less than ** terminated. If any NUL characters occur at byte offsets less than
** the value of the fourth parameter then the resulting string value will ** the value of the fourth parameter then the resulting string value will
** contain embedded NULs. The result of expressions involving strings ** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined. ** with embedded NULs is undefined.
@ -5356,7 +5356,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^These routines provide a means to determine the database, table, and ** ^These routines provide a means to determine the database, table, and
** table column that is the origin of a particular result column in ** table column that is the origin of a particular result column in a
** [SELECT] statement. ** [SELECT] statement.
** ^The name of the database or table or column can be returned as ** ^The name of the database or table or column can be returned as
** either a UTF-8 or UTF-16 string. ^The _database_ routines return ** either a UTF-8 or UTF-16 string. ^The _database_ routines return
@ -5925,8 +5925,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** **
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for ** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be ** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of ** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL ** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state. ** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of ** Without this flag, an attacker might be able to modify the schema of
** a database file to include invocations of the function with parameters ** a database file to include invocations of the function with parameters
@ -5957,7 +5957,7 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** [user-defined window functions|available here]. ** [user-defined window functions|available here].
** **
** ^(If the final parameter to sqlite3_create_function_v2() or ** ^(If the final parameter to sqlite3_create_function_v2() or
** sqlite3_create_window_function() is not NULL, then it is destructor for ** sqlite3_create_window_function() is not NULL, then it is the destructor for
** the application data pointer. The destructor is invoked when the function ** the application data pointer. The destructor is invoked when the function
** is deleted, either by being overloaded or when the database connection ** is deleted, either by being overloaded or when the database connection
** closes.)^ ^The destructor is also invoked if the call to ** closes.)^ ^The destructor is also invoked if the call to
@ -6357,7 +6357,7 @@ SQLITE_API unsigned int sqlite3_value_subtype(sqlite3_value*);
** METHOD: sqlite3_value ** METHOD: sqlite3_value
** **
** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value] ** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value]
** object D and returns a pointer to that copy. ^The [sqlite3_value] returned ** object V and returns a pointer to that copy. ^The [sqlite3_value] returned
** is a [protected sqlite3_value] object even if the input is not. ** is a [protected sqlite3_value] object even if the input is not.
** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a ** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
** memory allocation fails. ^If V is a [pointer value], then the result ** memory allocation fails. ^If V is a [pointer value], then the result
@ -6395,7 +6395,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs. ** allocation error occurs.
** **
** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is ** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
** determined by the N parameter on first successful call. Changing the ** determined by the N parameter on the first successful call. Changing the
** value of N in any subsequent call to sqlite3_aggregate_context() within ** value of N in any subsequent call to sqlite3_aggregate_context() within
** the same aggregate function instance will not resize the memory ** the same aggregate function instance will not resize the memory
** allocation.)^ Within the xFinal callback, it is customary to set ** allocation.)^ Within the xFinal callback, it is customary to set
@ -6557,7 +6557,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
** **
** Security Warning: These interfaces should not be exposed in scripting ** Security Warning: These interfaces should not be exposed in scripting
** languages or in other circumstances where it might be possible for an ** languages or in other circumstances where it might be possible for an
** an attacker to invoke them. Any agent that can invoke these interfaces ** attacker to invoke them. Any agent that can invoke these interfaces
** can probably also take control of the process. ** can probably also take control of the process.
** **
** Database connection client data is only available for SQLite ** Database connection client data is only available for SQLite
@ -6671,7 +6671,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** pointed to by the 2nd parameter are taken as the application-defined ** pointed to by the 2nd parameter are taken as the application-defined
** function result. If the 3rd parameter is non-negative, then it ** function result. If the 3rd parameter is non-negative, then it
** must be the byte offset into the string where the NUL terminator would ** must be the byte offset into the string where the NUL terminator would
** appear if the string where NUL terminated. If any NUL characters occur ** appear if the string were NUL terminated. If any NUL characters occur
** in the string at a byte offset that is less than the value of the 3rd ** in the string at a byte offset that is less than the value of the 3rd
** parameter, then the resulting string will contain embedded NULs and the ** parameter, then the resulting string will contain embedded NULs and the
** result of expressions operating on strings with embedded NULs is undefined. ** result of expressions operating on strings with embedded NULs is undefined.
@ -6729,7 +6729,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** string and preferably a string literal. The sqlite3_result_pointer() ** string and preferably a string literal. The sqlite3_result_pointer()
** routine is part of the [pointer passing interface] added for SQLite 3.20.0. ** routine is part of the [pointer passing interface] added for SQLite 3.20.0.
** **
** If these routines are called from within the different thread ** If these routines are called from within a different thread
** than the one containing the application-defined function that received ** than the one containing the application-defined function that received
** the [sqlite3_context] pointer, the results are undefined. ** the [sqlite3_context] pointer, the results are undefined.
*/ */
@ -7135,7 +7135,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name ** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
** for the N-th database on database connection D, or a NULL pointer of N is ** for the N-th database on database connection D, or a NULL pointer if N is
** out of range. An N value of 0 means the main database file. An N of 1 is ** out of range. An N value of 0 means the main database file. An N of 1 is
** the "temp" schema. Larger values of N correspond to various ATTACH-ed ** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** databases. ** databases.
@ -7230,7 +7230,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_READ state means that the database is currently ** <dd>The SQLITE_TXN_READ state means that the database is currently
** in a read transaction. Content has been read from the database file ** in a read transaction. Content has been read from the database file
** but nothing in the database file has changed. The transaction state ** but nothing in the database file has changed. The transaction state
** will advanced to SQLITE_TXN_WRITE if any changes occur and there are ** will be advanced to SQLITE_TXN_WRITE if any changes occur and there are
** no other conflicting concurrent write transactions. The transaction ** no other conflicting concurrent write transactions. The transaction
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or ** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [COMMIT].</dd> ** [COMMIT].</dd>
@ -7239,7 +7239,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_WRITE state means that the database is currently ** <dd>The SQLITE_TXN_WRITE state means that the database is currently
** in a write transaction. Content has been written to the database file ** in a write transaction. Content has been written to the database file
** but has not yet committed. The transaction state will change to ** but has not yet committed. The transaction state will change to
** to SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd> ** SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
*/ */
#define SQLITE_TXN_NONE 0 #define SQLITE_TXN_NONE 0
#define SQLITE_TXN_READ 1 #define SQLITE_TXN_READ 1
@ -7520,7 +7520,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size ** CAPI3REF: Impose A Limit On Heap Size
** **
** These interfaces impose limits on the amount of heap memory that will be ** These interfaces impose limits on the amount of heap memory that will be
** by all database connections within a single process. ** used by all database connections within a single process.
** **
** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the ** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the
** soft limit on the amount of heap memory that may be allocated by SQLite. ** soft limit on the amount of heap memory that may be allocated by SQLite.
@ -7578,7 +7578,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** </ul>)^ ** </ul>)^
** **
** The circumstances under which SQLite will enforce the heap limits may ** The circumstances under which SQLite will enforce the heap limits may
** changes in future releases of SQLite. ** change in future releases of SQLite.
*/ */
SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N);
SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N);
@ -7693,8 +7693,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc. ** ^The entry point is zProc.
** ^(zProc may be 0, in which case SQLite will try to come up with an ** ^(zProc may be 0, in which case SQLite will try to come up with an
** entry point name on its own. It first tries "sqlite3_extension_init". ** entry point name on its own. It first tries "sqlite3_extension_init".
** If that does not work, it constructs a name "sqlite3_X_init" where the ** If that does not work, it constructs a name "sqlite3_X_init" where
** X is consists of the lower-case equivalent of all ASCII alphabetic ** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following ** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^ ** "." and omitting any initial "lib".)^
** ^The sqlite3_load_extension() interface returns ** ^The sqlite3_load_extension() interface returns
@ -7765,7 +7765,7 @@ SQLITE_API int sqlite3_enable_load_extension(sqlite3 *db, int onoff);
** ^(Even though the function prototype shows that xEntryPoint() takes ** ^(Even though the function prototype shows that xEntryPoint() takes
** no arguments and returns void, SQLite invokes xEntryPoint() with three ** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the ** arguments and expects an integer result as if the signature of the
** entry point where as follows: ** entry point were as follows:
** **
** <blockquote><pre> ** <blockquote><pre>
** &nbsp; int xEntryPoint( ** &nbsp; int xEntryPoint(
@ -7929,7 +7929,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The ** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag ** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** is left in its default setting of false, the constraint will always be ** is left in its default setting of false, the constraint will always be
** checked separately in byte code. If the omit flag is change to true, then ** checked separately in byte code. If the omit flag is changed to true, then
** the constraint may or may not be checked in byte code. In other words, ** the constraint may or may not be checked in byte code. In other words,
** when the omit flag is true there is no guarantee that the constraint will ** when the omit flag is true there is no guarantee that the constraint will
** not be checked again using byte code.)^ ** not be checked again using byte code.)^
@ -7955,7 +7955,7 @@ struct sqlite3_module {
** The xBestIndex method may optionally populate the idxFlags field with a ** The xBestIndex method may optionally populate the idxFlags field with a
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is ** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN] ** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
** output to show the idxNum has hex instead of as decimal. Another flag is ** output to show the idxNum as hex instead of as decimal. Another flag is
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will ** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
** return at most one row. ** return at most one row.
** **
@ -8096,7 +8096,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth ** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through ** parameter is an arbitrary client data pointer that is passed through
** into the [xCreate] and [xConnect] methods of the virtual table module ** into the [xCreate] and [xConnect] methods of the virtual table module
** when a new virtual table is be being created or reinitialized. ** when a new virtual table is being created or reinitialized.
** **
** ^The sqlite3_create_module_v2() interface has a fifth parameter which ** ^The sqlite3_create_module_v2() interface has a fifth parameter which
** is a pointer to a destructor for the pClientData. ^SQLite will ** is a pointer to a destructor for the pClientData. ^SQLite will
@ -8261,7 +8261,7 @@ typedef struct sqlite3_blob sqlite3_blob;
** in *ppBlob. Otherwise an [error code] is returned and, unless the error ** in *ppBlob. Otherwise an [error code] is returned and, unless the error
** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided ** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided
** the API is not misused, it is always safe to call [sqlite3_blob_close()] ** the API is not misused, it is always safe to call [sqlite3_blob_close()]
** on *ppBlob after this function it returns. ** on *ppBlob after this function returns.
** **
** This function fails with SQLITE_ERROR if any of the following are true: ** This function fails with SQLITE_ERROR if any of the following are true:
** <ul> ** <ul>
@ -8381,7 +8381,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
** **
** ^Returns the size in bytes of the BLOB accessible via the ** ^Returns the size in bytes of the BLOB accessible via the
** successfully opened [BLOB handle] in its only argument. ^The ** successfully opened [BLOB handle] in its only argument. ^The
** incremental blob I/O routines can only read or overwriting existing ** incremental blob I/O routines can only read or overwrite existing
** blob content; they cannot change the size of a blob. ** blob content; they cannot change the size of a blob.
** **
** This routine only works on a [BLOB handle] which has been created ** This routine only works on a [BLOB handle] which has been created
@ -8531,7 +8531,7 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
** ^The sqlite3_mutex_alloc() routine allocates a new ** ^The sqlite3_mutex_alloc() routine allocates a new
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc() ** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** routine returns NULL if it is unable to allocate the requested ** routine returns NULL if it is unable to allocate the requested
** mutex. The argument to sqlite3_mutex_alloc() must one of these ** mutex. The argument to sqlite3_mutex_alloc() must be one of these
** integer constants: ** integer constants:
** **
** <ul> ** <ul>
@ -8764,7 +8764,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection ** CAPI3REF: Retrieve the mutex for a database connection
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^This interface returns a pointer the [sqlite3_mutex] object that ** ^This interface returns a pointer to the [sqlite3_mutex] object that
** serializes access to the [database connection] given in the argument ** serializes access to the [database connection] given in the argument
** when the [threading mode] is Serialized. ** when the [threading mode] is Serialized.
** ^If the [threading mode] is Single-thread or Multi-thread then this ** ^If the [threading mode] is Single-thread or Multi-thread then this
@ -8887,7 +8887,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** CAPI3REF: SQL Keyword Checking ** CAPI3REF: SQL Keyword Checking
** **
** These routines provide access to the set of SQL language keywords ** These routines provide access to the set of SQL language keywords
** recognized by SQLite. Applications can uses these routines to determine ** recognized by SQLite. Applications can use these routines to determine
** whether or not a specific identifier needs to be escaped (for example, ** whether or not a specific identifier needs to be escaped (for example,
** by enclosing in double-quotes) so as not to confuse the parser. ** by enclosing in double-quotes) so as not to confuse the parser.
** **
@ -9055,7 +9055,7 @@ SQLITE_API void sqlite3_str_reset(sqlite3_str*);
** content of the dynamic string under construction in X. The value ** content of the dynamic string under construction in X. The value
** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X ** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X
** and might be freed or altered by any subsequent method on the same ** and might be freed or altered by any subsequent method on the same
** [sqlite3_str] object. Applications must not used the pointer returned ** [sqlite3_str] object. Applications must not use the pointer returned by
** [sqlite3_str_value(X)] after any subsequent method call on the same ** [sqlite3_str_value(X)] after any subsequent method call on the same
** object. ^Applications may change the content of the string returned ** object. ^Applications may change the content of the string returned
** by [sqlite3_str_value(X)] as long as they do not write into any bytes ** by [sqlite3_str_value(X)] as long as they do not write into any bytes
@ -9141,7 +9141,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE] ** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The ** buffer and where forced to overflow to [sqlite3_malloc()]. The
** returned value includes allocations that overflowed because they ** returned value includes allocations that overflowed because they
** where too large (they were larger than the "sz" parameter to ** were too large (they were larger than the "sz" parameter to
** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because ** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because
** no space was left in the page cache.</dd>)^ ** no space was left in the page cache.</dd>)^
** **
@ -9225,28 +9225,29 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt> ** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt>
** <dd>This parameter returns the number of malloc attempts that were ** <dd>This parameter returns the number of malloc attempts that were
** satisfied using lookaside memory. Only the high-water value is meaningful; ** satisfied using lookaside memory. Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to the amount of ** been satisfied using lookaside memory but failed due to the amount of
** memory requested being larger than the lookaside slot size. ** memory requested being larger than the lookaside slot size.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to all lookaside ** been satisfied using lookaside memory but failed due to all lookaside
** memory already being in use. ** memory already being in use.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt> ** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^ ** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]] ** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt> ** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt>
@ -9255,10 +9256,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** memory used by that pager cache is divided evenly between the attached ** memory used by that pager cache is divided evenly between the attached
** connections.)^ In other words, if none of the pager caches associated ** connections.)^ In other words, if none of the pager caches associated
** with the database connection are shared, this request returns the same ** with the database connection are shared, this request returns the same
** value as DBSTATUS_CACHE_USED. Or, if one or more or the pager caches are ** value as DBSTATUS_CACHE_USED. Or, if one or more of the pager caches are
** shared, the value returned by this call will be smaller than that returned ** shared, the value returned by this call will be smaller than that returned
** by DBSTATUS_CACHE_USED. ^The highwater mark associated with ** by DBSTATUS_CACHE_USED. ^The highwater mark associated with
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0. ** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.</dd>
** **
** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt> ** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -9268,6 +9269,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** schema memory is shared with other database connections due to ** schema memory is shared with other database connections due to
** [shared cache mode] being enabled. ** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt> ** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -9304,7 +9306,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** been written to disk in the middle of a transaction due to the page ** been written to disk in the middle of a transaction due to the page
** cache overflowing. Transactions are more efficient if they are written ** cache overflowing. Transactions are more efficient if they are written
** to disk all at once. When pages spill mid-transaction, that introduces ** to disk all at once. When pages spill mid-transaction, that introduces
** additional overhead. This parameter can be used help identify ** additional overhead. This parameter can be used to help identify
** inefficiencies that can be resolved by increasing the cache size. ** inefficiencies that can be resolved by increasing the cache size.
** </dd> ** </dd>
** **
@ -9784,7 +9786,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being ** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically ** used by the backup operation, then the backup will be automatically
** restarted by the next call to sqlite3_backup_step(). ^If the source ** restarted by the next call to sqlite3_backup_step(). ^If the source
** database is modified by the using the same database connection as is used ** database is modified by using the same database connection as is used
** by the backup operation, then the backup database is automatically ** by the backup operation, then the backup database is automatically
** updated at the same time. ** updated at the same time.
** **
@ -9801,7 +9803,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** and may not be used following a call to sqlite3_backup_finish(). ** and may not be used following a call to sqlite3_backup_finish().
** **
** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no ** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no
** sqlite3_backup_step() errors occurred, regardless or whether or not ** sqlite3_backup_step() errors occurred, regardless of whether or not
** sqlite3_backup_step() completed. ** sqlite3_backup_step() completed.
** ^If an out-of-memory condition or IO error occurred during any prior ** ^If an out-of-memory condition or IO error occurred during any prior
** sqlite3_backup_step() call on the same [sqlite3_backup] object, then ** sqlite3_backup_step() call on the same [sqlite3_backup] object, then
@ -10871,7 +10873,7 @@ SQLITE_API void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^If a write-transaction is open on [database connection] D when the ** ^If a write-transaction is open on [database connection] D when the
** [sqlite3_db_cacheflush(D)] interface invoked, any dirty ** [sqlite3_db_cacheflush(D)] interface is invoked, any dirty
** pages in the pager-cache that are not currently in use are written out ** pages in the pager-cache that are not currently in use are written out
** to disk. A dirty page may be in use if a database cursor created by an ** to disk. A dirty page may be in use if a database cursor created by an
** active SQL statement is reading from it, or if it is page 1 of a database ** active SQL statement is reading from it, or if it is page 1 of a database
@ -15442,8 +15444,8 @@ typedef INT16_TYPE LogEst;
** assuming n is a signed integer type. UMXV(n) is similar for unsigned ** assuming n is a signed integer type. UMXV(n) is similar for unsigned
** integer types. ** integer types.
*/ */
#define SMXV(n) ((((i64)1)<<(sizeof(n)-1))-1) #define SMXV(n) ((((i64)1)<<(sizeof(n)*8-1))-1)
#define UMXV(n) ((((i64)1)<<(sizeof(n)))-1) #define UMXV(n) ((((i64)1)<<(sizeof(n)*8))-1)
/* /*
** Round up a number to the next larger multiple of 8. This is used ** Round up a number to the next larger multiple of 8. This is used
@ -18703,6 +18705,7 @@ struct CollSeq {
#define SQLITE_AFF_INTEGER 0x44 /* 'D' */ #define SQLITE_AFF_INTEGER 0x44 /* 'D' */
#define SQLITE_AFF_REAL 0x45 /* 'E' */ #define SQLITE_AFF_REAL 0x45 /* 'E' */
#define SQLITE_AFF_FLEXNUM 0x46 /* 'F' */ #define SQLITE_AFF_FLEXNUM 0x46 /* 'F' */
#define SQLITE_AFF_DEFER 0x58 /* 'X' - defer computation until later */
#define sqlite3IsNumericAffinity(X) ((X)>=SQLITE_AFF_NUMERIC) #define sqlite3IsNumericAffinity(X) ((X)>=SQLITE_AFF_NUMERIC)
@ -19253,7 +19256,7 @@ struct AggInfo {
** from source tables rather than from accumulators */ ** from source tables rather than from accumulators */
u8 useSortingIdx; /* In direct mode, reference the sorting index rather u8 useSortingIdx; /* In direct mode, reference the sorting index rather
** than the source table */ ** than the source table */
u16 nSortingColumn; /* Number of columns in the sorting index */ u32 nSortingColumn; /* Number of columns in the sorting index */
int sortingIdx; /* Cursor number of the sorting index */ int sortingIdx; /* Cursor number of the sorting index */
int sortingIdxPTab; /* Cursor number of pseudo-table */ int sortingIdxPTab; /* Cursor number of pseudo-table */
int iFirstReg; /* First register in range for aCol[] and aFunc[] */ int iFirstReg; /* First register in range for aCol[] and aFunc[] */
@ -19262,8 +19265,8 @@ struct AggInfo {
Table *pTab; /* Source table */ Table *pTab; /* Source table */
Expr *pCExpr; /* The original expression */ Expr *pCExpr; /* The original expression */
int iTable; /* Cursor number of the source table */ int iTable; /* Cursor number of the source table */
i16 iColumn; /* Column number within the source table */ int iColumn; /* Column number within the source table */
i16 iSorterColumn; /* Column number in the sorting index */ int iSorterColumn; /* Column number in the sorting index */
} *aCol; } *aCol;
int nColumn; /* Number of used entries in aCol[] */ int nColumn; /* Number of used entries in aCol[] */
int nAccumulator; /* Number of columns that show through to the output. int nAccumulator; /* Number of columns that show through to the output.
@ -43874,21 +43877,20 @@ static int unixShmLock(
/* Check that, if this to be a blocking lock, no locks that occur later /* Check that, if this to be a blocking lock, no locks that occur later
** in the following list than the lock being obtained are already held: ** in the following list than the lock being obtained are already held:
** **
** 1. Checkpointer lock (ofst==1). ** 1. Recovery lock (ofst==2).
** 2. Write lock (ofst==0). ** 2. Checkpointer lock (ofst==1).
** 3. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK). ** 3. Write lock (ofst==0).
** 4. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK).
** **
** In other words, if this is a blocking lock, none of the locks that ** In other words, if this is a blocking lock, none of the locks that
** occur later in the above list than the lock being obtained may be ** occur later in the above list than the lock being obtained may be
** held. ** held.
**
** It is not permitted to block on the RECOVER lock.
*/ */
#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG) #if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG)
{ {
u16 lockMask = (p->exclMask|p->sharedMask); u16 lockMask = (p->exclMask|p->sharedMask);
assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || ( assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || (
(ofst!=2) /* not RECOVER */ (ofst!=2 || lockMask==0)
&& (ofst!=1 || lockMask==0 || lockMask==2) && (ofst!=1 || lockMask==0 || lockMask==2)
&& (ofst!=0 || lockMask<3) && (ofst!=0 || lockMask<3)
&& (ofst<3 || lockMask<(1<<ofst)) && (ofst<3 || lockMask<(1<<ofst))
@ -49849,7 +49851,11 @@ static int winHandleLockTimeout(
if( res==WAIT_OBJECT_0 ){ if( res==WAIT_OBJECT_0 ){
ret = TRUE; ret = TRUE;
}else if( res==WAIT_TIMEOUT ){ }else if( res==WAIT_TIMEOUT ){
#if SQLITE_ENABLE_SETLK_TIMEOUT==1
rc = SQLITE_BUSY_TIMEOUT; rc = SQLITE_BUSY_TIMEOUT;
#else
rc = SQLITE_BUSY;
#endif
}else{ }else{
/* Some other error has occurred */ /* Some other error has occurred */
rc = SQLITE_IOERR_LOCK; rc = SQLITE_IOERR_LOCK;
@ -51660,21 +51666,20 @@ static int winShmLock(
/* Check that, if this to be a blocking lock, no locks that occur later /* Check that, if this to be a blocking lock, no locks that occur later
** in the following list than the lock being obtained are already held: ** in the following list than the lock being obtained are already held:
** **
** 1. Checkpointer lock (ofst==1). ** 1. Recovery lock (ofst==2).
** 2. Write lock (ofst==0). ** 2. Checkpointer lock (ofst==1).
** 3. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK). ** 3. Write lock (ofst==0).
** 4. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK).
** **
** In other words, if this is a blocking lock, none of the locks that ** In other words, if this is a blocking lock, none of the locks that
** occur later in the above list than the lock being obtained may be ** occur later in the above list than the lock being obtained may be
** held. ** held.
**
** It is not permitted to block on the RECOVER lock.
*/ */
#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG) #if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG)
{ {
u16 lockMask = (p->exclMask|p->sharedMask); u16 lockMask = (p->exclMask|p->sharedMask);
assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || ( assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || (
(ofst!=2) /* not RECOVER */ (ofst!=2 || lockMask==0)
&& (ofst!=1 || lockMask==0 || lockMask==2) && (ofst!=1 || lockMask==0 || lockMask==2)
&& (ofst!=0 || lockMask<3) && (ofst!=0 || lockMask<3)
&& (ofst<3 || lockMask<(1<<ofst)) && (ofst<3 || lockMask<(1<<ofst))
@ -54963,7 +54968,9 @@ bitvec_set_rehash:
}else{ }else{
memcpy(aiValues, p->u.aHash, sizeof(p->u.aHash)); memcpy(aiValues, p->u.aHash, sizeof(p->u.aHash));
memset(p->u.apSub, 0, sizeof(p->u.apSub)); memset(p->u.apSub, 0, sizeof(p->u.apSub));
p->iDivisor = (p->iSize + BITVEC_NPTR - 1)/BITVEC_NPTR; p->iDivisor = p->iSize/BITVEC_NPTR;
if( (p->iSize%BITVEC_NPTR)!=0 ) p->iDivisor++;
if( p->iDivisor<BITVEC_NBIT ) p->iDivisor = BITVEC_NBIT;
rc = sqlite3BitvecSet(p, i); rc = sqlite3BitvecSet(p, i);
for(j=0; j<BITVEC_NINT; j++){ for(j=0; j<BITVEC_NINT; j++){
if( aiValues[j] ) rc |= sqlite3BitvecSet(p, aiValues[j]); if( aiValues[j] ) rc |= sqlite3BitvecSet(p, aiValues[j]);
@ -58750,6 +58757,9 @@ struct Pager {
Wal *pWal; /* Write-ahead log used by "journal_mode=wal" */ Wal *pWal; /* Write-ahead log used by "journal_mode=wal" */
char *zWal; /* File name for write-ahead log */ char *zWal; /* File name for write-ahead log */
#endif #endif
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT
sqlite3 *dbWal;
#endif
}; };
/* /*
@ -65631,6 +65641,11 @@ static int pagerOpenWal(Pager *pPager){
pPager->fd, pPager->zWal, pPager->exclusiveMode, pPager->fd, pPager->zWal, pPager->exclusiveMode,
pPager->journalSizeLimit, &pPager->pWal pPager->journalSizeLimit, &pPager->pWal
); );
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT
if( rc==SQLITE_OK ){
sqlite3WalDb(pPager->pWal, pPager->dbWal);
}
#endif
} }
pagerFixMaplimit(pPager); pagerFixMaplimit(pPager);
@ -65750,6 +65765,7 @@ SQLITE_PRIVATE int sqlite3PagerWalWriteLock(Pager *pPager, int bLock){
** blocking locks are required. ** blocking locks are required.
*/ */
SQLITE_PRIVATE void sqlite3PagerWalDb(Pager *pPager, sqlite3 *db){ SQLITE_PRIVATE void sqlite3PagerWalDb(Pager *pPager, sqlite3 *db){
pPager->dbWal = db;
if( pagerUseWal(pPager) ){ if( pagerUseWal(pPager) ){
sqlite3WalDb(pPager->pWal, db); sqlite3WalDb(pPager->pWal, db);
} }
@ -68923,7 +68939,6 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int *pCnt){
rc = walIndexReadHdr(pWal, pChanged); rc = walIndexReadHdr(pWal, pChanged);
} }
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT #ifdef SQLITE_ENABLE_SETLK_TIMEOUT
walDisableBlocking(pWal);
if( rc==SQLITE_BUSY_TIMEOUT ){ if( rc==SQLITE_BUSY_TIMEOUT ){
rc = SQLITE_BUSY; rc = SQLITE_BUSY;
*pCnt |= WAL_RETRY_BLOCKED_MASK; *pCnt |= WAL_RETRY_BLOCKED_MASK;
@ -68938,6 +68953,7 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int *pCnt){
** WAL_RETRY this routine will be called again and will probably be ** WAL_RETRY this routine will be called again and will probably be
** right on the second iteration. ** right on the second iteration.
*/ */
(void)walEnableBlocking(pWal);
if( pWal->apWiData[0]==0 ){ if( pWal->apWiData[0]==0 ){
/* This branch is taken when the xShmMap() method returns SQLITE_BUSY. /* This branch is taken when the xShmMap() method returns SQLITE_BUSY.
** We assume this is a transient condition, so return WAL_RETRY. The ** We assume this is a transient condition, so return WAL_RETRY. The
@ -68954,6 +68970,7 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int *pCnt){
rc = SQLITE_BUSY_RECOVERY; rc = SQLITE_BUSY_RECOVERY;
} }
} }
walDisableBlocking(pWal);
if( rc!=SQLITE_OK ){ if( rc!=SQLITE_OK ){
return rc; return rc;
} }
@ -69641,6 +69658,7 @@ SQLITE_PRIVATE int sqlite3WalUndo(Wal *pWal, int (*xUndo)(void *, Pgno), void *p
if( iMax!=pWal->hdr.mxFrame ) walCleanupHash(pWal); if( iMax!=pWal->hdr.mxFrame ) walCleanupHash(pWal);
} }
SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; ) SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; )
pWal->iReCksum = 0;
} }
return rc; return rc;
} }
@ -69688,6 +69706,9 @@ SQLITE_PRIVATE int sqlite3WalSavepointUndo(Wal *pWal, u32 *aWalData){
walCleanupHash(pWal); walCleanupHash(pWal);
} }
SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; ) SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; )
if( pWal->iReCksum>pWal->hdr.mxFrame ){
pWal->iReCksum = 0;
}
} }
return rc; return rc;
@ -75228,6 +75249,13 @@ static SQLITE_NOINLINE int btreeBeginTrans(
(void)sqlite3PagerWalWriteLock(pPager, 0); (void)sqlite3PagerWalWriteLock(pPager, 0);
unlockBtreeIfUnused(pBt); unlockBtreeIfUnused(pBt);
} }
#if defined(SQLITE_ENABLE_SETLK_TIMEOUT)
if( rc==SQLITE_BUSY_TIMEOUT ){
/* If a blocking lock timed out, break out of the loop here so that
** the busy-handler is not invoked. */
break;
}
#endif
}while( (rc&0xFF)==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE && }while( (rc&0xFF)==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&
btreeInvokeBusyHandler(pBt) ); btreeInvokeBusyHandler(pBt) );
sqlite3PagerWalDb(pPager, 0); sqlite3PagerWalDb(pPager, 0);
@ -105039,7 +105067,7 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit(
assert( pCsr->eCurType==CURTYPE_SORTER ); assert( pCsr->eCurType==CURTYPE_SORTER );
assert( sizeof(KeyInfo) + UMXV(pCsr->pKeyInfo->nKeyField)*sizeof(CollSeq*) assert( sizeof(KeyInfo) + UMXV(pCsr->pKeyInfo->nKeyField)*sizeof(CollSeq*)
< 0x7fffffff ); < 0x7fffffff );
szKeyInfo = SZ_KEYINFO(pCsr->pKeyInfo->nKeyField+1); szKeyInfo = SZ_KEYINFO(pCsr->pKeyInfo->nKeyField);
sz = SZ_VDBESORTER(nWorker+1); sz = SZ_VDBESORTER(nWorker+1);
pSorter = (VdbeSorter*)sqlite3DbMallocZero(db, sz + szKeyInfo); pSorter = (VdbeSorter*)sqlite3DbMallocZero(db, sz + szKeyInfo);
@ -110389,7 +110417,9 @@ SQLITE_PRIVATE char sqlite3ExprAffinity(const Expr *pExpr){
pExpr->pLeft->x.pSelect->pEList->a[pExpr->iColumn].pExpr pExpr->pLeft->x.pSelect->pEList->a[pExpr->iColumn].pExpr
); );
} }
if( op==TK_VECTOR ){ if( op==TK_VECTOR
|| (op==TK_FUNCTION && pExpr->affExpr==SQLITE_AFF_DEFER)
){
assert( ExprUseXList(pExpr) ); assert( ExprUseXList(pExpr) );
return sqlite3ExprAffinity(pExpr->x.pList->a[0].pExpr); return sqlite3ExprAffinity(pExpr->x.pList->a[0].pExpr);
} }
@ -110582,7 +110612,9 @@ SQLITE_PRIVATE CollSeq *sqlite3ExprCollSeq(Parse *pParse, const Expr *pExpr){
p = p->pLeft; p = p->pLeft;
continue; continue;
} }
if( op==TK_VECTOR ){ if( op==TK_VECTOR
|| (op==TK_FUNCTION && p->affExpr==SQLITE_AFF_DEFER)
){
assert( ExprUseXList(p) ); assert( ExprUseXList(p) );
p = p->x.pList->a[0].pExpr; p = p->x.pList->a[0].pExpr;
continue; continue;
@ -117322,7 +117354,9 @@ static void findOrCreateAggInfoColumn(
){ ){
struct AggInfo_col *pCol; struct AggInfo_col *pCol;
int k; int k;
int mxTerm = pParse->db->aLimit[SQLITE_LIMIT_COLUMN];
assert( mxTerm <= SMXV(i16) );
assert( pAggInfo->iFirstReg==0 ); assert( pAggInfo->iFirstReg==0 );
pCol = pAggInfo->aCol; pCol = pAggInfo->aCol;
for(k=0; k<pAggInfo->nColumn; k++, pCol++){ for(k=0; k<pAggInfo->nColumn; k++, pCol++){
@ -117340,6 +117374,10 @@ static void findOrCreateAggInfoColumn(
assert( pParse->db->mallocFailed ); assert( pParse->db->mallocFailed );
return; return;
} }
if( k>mxTerm ){
sqlite3ErrorMsg(pParse, "more than %d aggregate terms", mxTerm);
k = mxTerm;
}
pCol = &pAggInfo->aCol[k]; pCol = &pAggInfo->aCol[k];
assert( ExprUseYTab(pExpr) ); assert( ExprUseYTab(pExpr) );
pCol->pTab = pExpr->y.pTab; pCol->pTab = pExpr->y.pTab;
@ -117373,6 +117411,7 @@ fix_up_expr:
if( pExpr->op==TK_COLUMN ){ if( pExpr->op==TK_COLUMN ){
pExpr->op = TK_AGG_COLUMN; pExpr->op = TK_AGG_COLUMN;
} }
assert( k <= SMXV(pExpr->iAgg) );
pExpr->iAgg = (i16)k; pExpr->iAgg = (i16)k;
} }
@ -117457,13 +117496,19 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){
** function that is already in the pAggInfo structure ** function that is already in the pAggInfo structure
*/ */
struct AggInfo_func *pItem = pAggInfo->aFunc; struct AggInfo_func *pItem = pAggInfo->aFunc;
int mxTerm = pParse->db->aLimit[SQLITE_LIMIT_COLUMN];
assert( mxTerm <= SMXV(i16) );
for(i=0; i<pAggInfo->nFunc; i++, pItem++){ for(i=0; i<pAggInfo->nFunc; i++, pItem++){
if( NEVER(pItem->pFExpr==pExpr) ) break; if( NEVER(pItem->pFExpr==pExpr) ) break;
if( sqlite3ExprCompare(0, pItem->pFExpr, pExpr, -1)==0 ){ if( sqlite3ExprCompare(0, pItem->pFExpr, pExpr, -1)==0 ){
break; break;
} }
} }
if( i>=pAggInfo->nFunc ){ if( i>mxTerm ){
sqlite3ErrorMsg(pParse, "more than %d aggregate terms", mxTerm);
i = mxTerm;
assert( i<pAggInfo->nFunc );
}else if( i>=pAggInfo->nFunc ){
/* pExpr is original. Make a new entry in pAggInfo->aFunc[] /* pExpr is original. Make a new entry in pAggInfo->aFunc[]
*/ */
u8 enc = ENC(pParse->db); u8 enc = ENC(pParse->db);
@ -117517,6 +117562,7 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){
*/ */
assert( !ExprHasProperty(pExpr, EP_TokenOnly|EP_Reduced) ); assert( !ExprHasProperty(pExpr, EP_TokenOnly|EP_Reduced) );
ExprSetVVAProperty(pExpr, EP_NoReduce); ExprSetVVAProperty(pExpr, EP_NoReduce);
assert( i <= SMXV(pExpr->iAgg) );
pExpr->iAgg = (i16)i; pExpr->iAgg = (i16)i;
pExpr->pAggInfo = pAggInfo; pExpr->pAggInfo = pAggInfo;
return WRC_Prune; return WRC_Prune;
@ -131975,7 +132021,7 @@ static void concatFuncCore(
int nSep, int nSep,
const char *zSep const char *zSep
){ ){
i64 j, k, n = 0; i64 j, n = 0;
int i; int i;
char *z; char *z;
for(i=0; i<argc; i++){ for(i=0; i<argc; i++){
@ -131989,8 +132035,8 @@ static void concatFuncCore(
} }
j = 0; j = 0;
for(i=0; i<argc; i++){ for(i=0; i<argc; i++){
k = sqlite3_value_bytes(argv[i]); if( sqlite3_value_type(argv[i])!=SQLITE_NULL ){
if( k>0 ){ int k = sqlite3_value_bytes(argv[i]);
const char *v = (const char*)sqlite3_value_text(argv[i]); const char *v = (const char*)sqlite3_value_text(argv[i]);
if( v!=0 ){ if( v!=0 ){
if( j>0 && nSep>0 ){ if( j>0 && nSep>0 ){
@ -145364,7 +145410,7 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){
} }
pE1 = sqlite3CreateColumnExpr(db, pSrc, iLeft, iLeftCol); pE1 = sqlite3CreateColumnExpr(db, pSrc, iLeft, iLeftCol);
sqlite3SrcItemColumnUsed(&pSrc->a[iLeft], iLeftCol); sqlite3SrcItemColumnUsed(&pSrc->a[iLeft], iLeftCol);
if( (pSrc->a[0].fg.jointype & JT_LTORJ)!=0 ){ if( (pSrc->a[0].fg.jointype & JT_LTORJ)!=0 && pParse->nErr==0 ){
/* This branch runs if the query contains one or more RIGHT or FULL /* This branch runs if the query contains one or more RIGHT or FULL
** JOINs. If only a single table on the left side of this join ** JOINs. If only a single table on the left side of this join
** contains the zName column, then this branch is a no-op. ** contains the zName column, then this branch is a no-op.
@ -145380,6 +145426,8 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){
*/ */
ExprList *pFuncArgs = 0; /* Arguments to the coalesce() */ ExprList *pFuncArgs = 0; /* Arguments to the coalesce() */
static const Token tkCoalesce = { "coalesce", 8 }; static const Token tkCoalesce = { "coalesce", 8 };
assert( pE1!=0 );
ExprSetProperty(pE1, EP_CanBeNull);
while( tableAndColumnIndex(pSrc, iLeft+1, i, zName, &iLeft, &iLeftCol, while( tableAndColumnIndex(pSrc, iLeft+1, i, zName, &iLeft, &iLeftCol,
pRight->fg.isSynthUsing)!=0 ){ pRight->fg.isSynthUsing)!=0 ){
if( pSrc->a[iLeft].fg.isUsing==0 if( pSrc->a[iLeft].fg.isUsing==0
@ -145396,8 +145444,14 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){
if( pFuncArgs ){ if( pFuncArgs ){
pFuncArgs = sqlite3ExprListAppend(pParse, pFuncArgs, pE1); pFuncArgs = sqlite3ExprListAppend(pParse, pFuncArgs, pE1);
pE1 = sqlite3ExprFunction(pParse, pFuncArgs, &tkCoalesce, 0); pE1 = sqlite3ExprFunction(pParse, pFuncArgs, &tkCoalesce, 0);
if( pE1 ){
pE1->affExpr = SQLITE_AFF_DEFER;
} }
} }
}else if( (pSrc->a[i+1].fg.jointype & JT_LEFT)!=0 && pParse->nErr==0 ){
assert( pE1!=0 );
ExprSetProperty(pE1, EP_CanBeNull);
}
pE2 = sqlite3CreateColumnExpr(db, pSrc, i+1, iRightCol); pE2 = sqlite3CreateColumnExpr(db, pSrc, i+1, iRightCol);
sqlite3SrcItemColumnUsed(pRight, iRightCol); sqlite3SrcItemColumnUsed(pRight, iRightCol);
pEq = sqlite3PExpr(pParse, TK_EQ, pE1, pE2); pEq = sqlite3PExpr(pParse, TK_EQ, pE1, pE2);
@ -149004,9 +149058,9 @@ static int compoundHasDifferentAffinities(Select *p){
** from 2015-02-09.) ** from 2015-02-09.)
** **
** (3) If the subquery is the right operand of a LEFT JOIN then ** (3) If the subquery is the right operand of a LEFT JOIN then
** (3a) the subquery may not be a join and ** (3a) the subquery may not be a join
** (3b) the FROM clause of the subquery may not contain a virtual ** (**) Was (3b): "the FROM clause of the subquery may not contain
** table and ** a virtual table"
** (**) Was: "The outer query may not have a GROUP BY." This case ** (**) Was: "The outer query may not have a GROUP BY." This case
** is now managed correctly ** is now managed correctly
** (3d) the outer query may not be DISTINCT. ** (3d) the outer query may not be DISTINCT.
@ -149222,7 +149276,7 @@ static int flattenSubquery(
*/ */
if( (pSubitem->fg.jointype & (JT_OUTER|JT_LTORJ))!=0 ){ if( (pSubitem->fg.jointype & (JT_OUTER|JT_LTORJ))!=0 ){
if( pSubSrc->nSrc>1 /* (3a) */ if( pSubSrc->nSrc>1 /* (3a) */
|| IsVirtual(pSubSrc->a[0].pSTab) /* (3b) */ /**** || IsVirtual(pSubSrc->a[0].pSTab) (3b)-omitted */
|| (p->selFlags & SF_Distinct)!=0 /* (3d) */ || (p->selFlags & SF_Distinct)!=0 /* (3d) */
|| (pSubitem->fg.jointype & JT_RIGHT)!=0 /* (26) */ || (pSubitem->fg.jointype & JT_RIGHT)!=0 /* (26) */
){ ){
@ -161722,12 +161776,13 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart(
if( pLevel->iLeftJoin==0 ){ if( pLevel->iLeftJoin==0 ){
/* If a partial index is driving the loop, try to eliminate WHERE clause /* If a partial index is driving the loop, try to eliminate WHERE clause
** terms from the query that must be true due to the WHERE clause of ** terms from the query that must be true due to the WHERE clause of
** the partial index. ** the partial index. This optimization does not work on an outer join,
** as shown by:
** **
** 2019-11-02 ticket 623eff57e76d45f6: This optimization does not work ** 2019-11-02 ticket 623eff57e76d45f6 (LEFT JOIN)
** for a LEFT JOIN. ** 2025-05-29 forum post 7dee41d32506c4ae (RIGHT JOIN)
*/ */
if( pIdx->pPartIdxWhere ){ if( pIdx->pPartIdxWhere && pLevel->pRJ==0 ){
whereApplyPartialIndexConstraints(pIdx->pPartIdxWhere, iCur, pWC); whereApplyPartialIndexConstraints(pIdx->pPartIdxWhere, iCur, pWC);
} }
}else{ }else{
@ -163400,30 +163455,42 @@ static void exprAnalyzeOrTerm(
** 1. The SQLITE_Transitive optimization must be enabled ** 1. The SQLITE_Transitive optimization must be enabled
** 2. Must be either an == or an IS operator ** 2. Must be either an == or an IS operator
** 3. Not originating in the ON clause of an OUTER JOIN ** 3. Not originating in the ON clause of an OUTER JOIN
** 4. The affinities of A and B must be compatible ** 4. The operator is not IS or else the query does not contain RIGHT JOIN
** 5a. Both operands use the same collating sequence OR ** 5. The affinities of A and B must be compatible
** 5b. The overall collating sequence is BINARY ** 6a. Both operands use the same collating sequence OR
** 6b. The overall collating sequence is BINARY
** If this routine returns TRUE, that means that the RHS can be substituted ** If this routine returns TRUE, that means that the RHS can be substituted
** for the LHS anyplace else in the WHERE clause where the LHS column occurs. ** for the LHS anyplace else in the WHERE clause where the LHS column occurs.
** This is an optimization. No harm comes from returning 0. But if 1 is ** This is an optimization. No harm comes from returning 0. But if 1 is
** returned when it should not be, then incorrect answers might result. ** returned when it should not be, then incorrect answers might result.
*/ */
static int termIsEquivalence(Parse *pParse, Expr *pExpr){ static int termIsEquivalence(Parse *pParse, Expr *pExpr, SrcList *pSrc){
char aff1, aff2; char aff1, aff2;
CollSeq *pColl; CollSeq *pColl;
if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; /* (1) */
if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; /* (2) */
if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; /* (3) */
assert( pSrc!=0 );
if( pExpr->op==TK_IS
&& pSrc->nSrc
&& (pSrc->a[0].fg.jointype & JT_LTORJ)!=0
){
return 0; /* (4) */
}
aff1 = sqlite3ExprAffinity(pExpr->pLeft); aff1 = sqlite3ExprAffinity(pExpr->pLeft);
aff2 = sqlite3ExprAffinity(pExpr->pRight); aff2 = sqlite3ExprAffinity(pExpr->pRight);
if( aff1!=aff2 if( aff1!=aff2
&& (!sqlite3IsNumericAffinity(aff1) || !sqlite3IsNumericAffinity(aff2)) && (!sqlite3IsNumericAffinity(aff1) || !sqlite3IsNumericAffinity(aff2))
){ ){
return 0; return 0; /* (5) */
} }
pColl = sqlite3ExprCompareCollSeq(pParse, pExpr); pColl = sqlite3ExprCompareCollSeq(pParse, pExpr);
if( sqlite3IsBinary(pColl) ) return 1; if( !sqlite3IsBinary(pColl)
return sqlite3ExprCollSeqMatch(pParse, pExpr->pLeft, pExpr->pRight); && !sqlite3ExprCollSeqMatch(pParse, pExpr->pLeft, pExpr->pRight)
){
return 0; /* (6) */
}
return 1;
} }
/* /*
@ -163688,8 +163755,8 @@ static void exprAnalyze(
if( op==TK_IS ) pNew->wtFlags |= TERM_IS; if( op==TK_IS ) pNew->wtFlags |= TERM_IS;
pTerm = &pWC->a[idxTerm]; pTerm = &pWC->a[idxTerm];
pTerm->wtFlags |= TERM_COPIED; pTerm->wtFlags |= TERM_COPIED;
assert( pWInfo->pTabList!=0 );
if( termIsEquivalence(pParse, pDup) ){ if( termIsEquivalence(pParse, pDup, pWInfo->pTabList) ){
pTerm->eOperator |= WO_EQUIV; pTerm->eOperator |= WO_EQUIV;
eExtraOp = WO_EQUIV; eExtraOp = WO_EQUIV;
} }
@ -184391,6 +184458,7 @@ SQLITE_API int sqlite3_setlk_timeout(sqlite3 *db, int ms, int flags){
#endif #endif
if( ms<-1 ) return SQLITE_RANGE; if( ms<-1 ) return SQLITE_RANGE;
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT #ifdef SQLITE_ENABLE_SETLK_TIMEOUT
sqlite3_mutex_enter(db->mutex);
db->setlkTimeout = ms; db->setlkTimeout = ms;
db->setlkFlags = flags; db->setlkFlags = flags;
sqlite3BtreeEnterAll(db); sqlite3BtreeEnterAll(db);
@ -184402,6 +184470,7 @@ SQLITE_API int sqlite3_setlk_timeout(sqlite3 *db, int ms, int flags){
} }
} }
sqlite3BtreeLeaveAll(db); sqlite3BtreeLeaveAll(db);
sqlite3_mutex_leave(db->mutex);
#endif #endif
#if !defined(SQLITE_ENABLE_API_ARMOR) && !defined(SQLITE_ENABLE_SETLK_TIMEOUT) #if !defined(SQLITE_ENABLE_API_ARMOR) && !defined(SQLITE_ENABLE_SETLK_TIMEOUT)
UNUSED_PARAMETER(db); UNUSED_PARAMETER(db);
@ -209021,8 +209090,10 @@ static int jsonBlobChangePayloadSize(
nExtra = 1; nExtra = 1;
}else if( szType==13 ){ }else if( szType==13 ){
nExtra = 2; nExtra = 2;
}else{ }else if( szType==14 ){
nExtra = 4; nExtra = 4;
}else{
nExtra = 8;
} }
if( szPayload<=11 ){ if( szPayload<=11 ){
nNeeded = 0; nNeeded = 0;
@ -213407,6 +213478,8 @@ SQLITE_PRIVATE int sqlite3JsonTableFunctions(sqlite3 *db){
#endif #endif
SQLITE_PRIVATE int sqlite3GetToken(const unsigned char*,int*); /* In the SQLite core */ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char*,int*); /* In the SQLite core */
/* #include <stddef.h> */
/* /*
** If building separately, we will need some setup that is normally ** If building separately, we will need some setup that is normally
** found in sqliteInt.h ** found in sqliteInt.h
@ -235449,6 +235522,7 @@ SQLITE_EXTENSION_INIT1
/* #include <string.h> */ /* #include <string.h> */
/* #include <assert.h> */ /* #include <assert.h> */
/* #include <stddef.h> */
#ifndef SQLITE_AMALGAMATION #ifndef SQLITE_AMALGAMATION
@ -257192,7 +257266,7 @@ static void fts5SourceIdFunc(
){ ){
assert( nArg==0 ); assert( nArg==0 );
UNUSED_PARAM2(nArg, apUnused); UNUSED_PARAM2(nArg, apUnused);
sqlite3_result_text(pCtx, "fts5: 2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742", -1, SQLITE_TRANSIENT); sqlite3_result_text(pCtx, "fts5: 2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079", -1, SQLITE_TRANSIENT);
} }
/* /*
@ -258007,6 +258081,7 @@ static int fts5StorageDeleteFromIndex(
for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){ for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){
if( pConfig->abUnindexed[iCol-1]==0 ){ if( pConfig->abUnindexed[iCol-1]==0 ){
sqlite3_value *pVal = 0; sqlite3_value *pVal = 0;
sqlite3_value *pFree = 0;
const char *pText = 0; const char *pText = 0;
int nText = 0; int nText = 0;
const char *pLoc = 0; const char *pLoc = 0;
@ -258023,6 +258098,16 @@ static int fts5StorageDeleteFromIndex(
if( pConfig->bLocale && sqlite3Fts5IsLocaleValue(pConfig, pVal) ){ if( pConfig->bLocale && sqlite3Fts5IsLocaleValue(pConfig, pVal) ){
rc = sqlite3Fts5DecodeLocaleValue(pVal, &pText, &nText, &pLoc, &nLoc); rc = sqlite3Fts5DecodeLocaleValue(pVal, &pText, &nText, &pLoc, &nLoc);
}else{ }else{
if( sqlite3_value_type(pVal)!=SQLITE_TEXT ){
/* Make a copy of the value to work with. This is because the call
** to sqlite3_value_text() below forces the type of the value to
** SQLITE_TEXT, and we may need to use it again later. */
pFree = pVal = sqlite3_value_dup(pVal);
if( pVal==0 ){
rc = SQLITE_NOMEM;
}
}
if( rc==SQLITE_OK ){
pText = (const char*)sqlite3_value_text(pVal); pText = (const char*)sqlite3_value_text(pVal);
nText = sqlite3_value_bytes(pVal); nText = sqlite3_value_bytes(pVal);
if( pConfig->bLocale && pSeek ){ if( pConfig->bLocale && pSeek ){
@ -258030,6 +258115,7 @@ static int fts5StorageDeleteFromIndex(
nLoc = sqlite3_column_bytes(pSeek, iCol + pConfig->nCol); nLoc = sqlite3_column_bytes(pSeek, iCol + pConfig->nCol);
} }
} }
}
if( rc==SQLITE_OK ){ if( rc==SQLITE_OK ){
sqlite3Fts5SetLocale(pConfig, pLoc, nLoc); sqlite3Fts5SetLocale(pConfig, pLoc, nLoc);
@ -258043,6 +258129,7 @@ static int fts5StorageDeleteFromIndex(
} }
sqlite3Fts5ClearLocale(pConfig); sqlite3Fts5ClearLocale(pConfig);
} }
sqlite3_value_free(pFree);
} }
} }
if( rc==SQLITE_OK && p->nTotalRow<1 ){ if( rc==SQLITE_OK && p->nTotalRow<1 ){

View file

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.50.0" #define SQLITE_VERSION "3.50.2"
#define SQLITE_VERSION_NUMBER 3050000 #define SQLITE_VERSION_NUMBER 3050002
#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742" #define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -4079,7 +4079,7 @@ SQLITE_API sqlite3_file *sqlite3_database_file_object(const char*);
** **
** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of ** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of
** database filename D with corresponding journal file J and WAL file W and ** database filename D with corresponding journal file J and WAL file W and
** with N URI parameters key/values pairs in the array P. The result from ** an array P of N URI Key/Value pairs. The result from
** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that ** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that
** is safe to pass to routines like: ** is safe to pass to routines like:
** <ul> ** <ul>
@ -4760,7 +4760,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants, ** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants,
** literals may be replaced by a [parameter] that matches one of following ** literals may be replaced by a [parameter] that matches one of the following
** templates: ** templates:
** **
** <ul> ** <ul>
@ -4805,7 +4805,7 @@ typedef struct sqlite3_context sqlite3_context;
** **
** [[byte-order determination rules]] ^The byte-order of ** [[byte-order determination rules]] ^The byte-order of
** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF) ** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF)
** found in first character, which is removed, or in the absence of a BOM ** found in the first character, which is removed, or in the absence of a BOM
** the byte order is the native byte order of the host ** the byte order is the native byte order of the host
** machine for sqlite3_bind_text16() or the byte order specified in ** machine for sqlite3_bind_text16() or the byte order specified in
** the 6th parameter for sqlite3_bind_text64().)^ ** the 6th parameter for sqlite3_bind_text64().)^
@ -4825,7 +4825,7 @@ typedef struct sqlite3_context sqlite3_context;
** or sqlite3_bind_text16() or sqlite3_bind_text64() then ** or sqlite3_bind_text16() or sqlite3_bind_text64() then
** that parameter must be the byte offset ** that parameter must be the byte offset
** where the NUL terminator would occur assuming the string were NUL ** where the NUL terminator would occur assuming the string were NUL
** terminated. If any NUL characters occurs at byte offsets less than ** terminated. If any NUL characters occur at byte offsets less than
** the value of the fourth parameter then the resulting string value will ** the value of the fourth parameter then the resulting string value will
** contain embedded NULs. The result of expressions involving strings ** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined. ** with embedded NULs is undefined.
@ -5037,7 +5037,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^These routines provide a means to determine the database, table, and ** ^These routines provide a means to determine the database, table, and
** table column that is the origin of a particular result column in ** table column that is the origin of a particular result column in a
** [SELECT] statement. ** [SELECT] statement.
** ^The name of the database or table or column can be returned as ** ^The name of the database or table or column can be returned as
** either a UTF-8 or UTF-16 string. ^The _database_ routines return ** either a UTF-8 or UTF-16 string. ^The _database_ routines return
@ -5606,8 +5606,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** **
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for ** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be ** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of ** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL ** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state. ** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of ** Without this flag, an attacker might be able to modify the schema of
** a database file to include invocations of the function with parameters ** a database file to include invocations of the function with parameters
@ -5638,7 +5638,7 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** [user-defined window functions|available here]. ** [user-defined window functions|available here].
** **
** ^(If the final parameter to sqlite3_create_function_v2() or ** ^(If the final parameter to sqlite3_create_function_v2() or
** sqlite3_create_window_function() is not NULL, then it is destructor for ** sqlite3_create_window_function() is not NULL, then it is the destructor for
** the application data pointer. The destructor is invoked when the function ** the application data pointer. The destructor is invoked when the function
** is deleted, either by being overloaded or when the database connection ** is deleted, either by being overloaded or when the database connection
** closes.)^ ^The destructor is also invoked if the call to ** closes.)^ ^The destructor is also invoked if the call to
@ -6038,7 +6038,7 @@ SQLITE_API unsigned int sqlite3_value_subtype(sqlite3_value*);
** METHOD: sqlite3_value ** METHOD: sqlite3_value
** **
** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value] ** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value]
** object D and returns a pointer to that copy. ^The [sqlite3_value] returned ** object V and returns a pointer to that copy. ^The [sqlite3_value] returned
** is a [protected sqlite3_value] object even if the input is not. ** is a [protected sqlite3_value] object even if the input is not.
** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a ** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
** memory allocation fails. ^If V is a [pointer value], then the result ** memory allocation fails. ^If V is a [pointer value], then the result
@ -6076,7 +6076,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs. ** allocation error occurs.
** **
** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is ** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
** determined by the N parameter on first successful call. Changing the ** determined by the N parameter on the first successful call. Changing the
** value of N in any subsequent call to sqlite3_aggregate_context() within ** value of N in any subsequent call to sqlite3_aggregate_context() within
** the same aggregate function instance will not resize the memory ** the same aggregate function instance will not resize the memory
** allocation.)^ Within the xFinal callback, it is customary to set ** allocation.)^ Within the xFinal callback, it is customary to set
@ -6238,7 +6238,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
** **
** Security Warning: These interfaces should not be exposed in scripting ** Security Warning: These interfaces should not be exposed in scripting
** languages or in other circumstances where it might be possible for an ** languages or in other circumstances where it might be possible for an
** an attacker to invoke them. Any agent that can invoke these interfaces ** attacker to invoke them. Any agent that can invoke these interfaces
** can probably also take control of the process. ** can probably also take control of the process.
** **
** Database connection client data is only available for SQLite ** Database connection client data is only available for SQLite
@ -6352,7 +6352,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** pointed to by the 2nd parameter are taken as the application-defined ** pointed to by the 2nd parameter are taken as the application-defined
** function result. If the 3rd parameter is non-negative, then it ** function result. If the 3rd parameter is non-negative, then it
** must be the byte offset into the string where the NUL terminator would ** must be the byte offset into the string where the NUL terminator would
** appear if the string where NUL terminated. If any NUL characters occur ** appear if the string were NUL terminated. If any NUL characters occur
** in the string at a byte offset that is less than the value of the 3rd ** in the string at a byte offset that is less than the value of the 3rd
** parameter, then the resulting string will contain embedded NULs and the ** parameter, then the resulting string will contain embedded NULs and the
** result of expressions operating on strings with embedded NULs is undefined. ** result of expressions operating on strings with embedded NULs is undefined.
@ -6410,7 +6410,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** string and preferably a string literal. The sqlite3_result_pointer() ** string and preferably a string literal. The sqlite3_result_pointer()
** routine is part of the [pointer passing interface] added for SQLite 3.20.0. ** routine is part of the [pointer passing interface] added for SQLite 3.20.0.
** **
** If these routines are called from within the different thread ** If these routines are called from within a different thread
** than the one containing the application-defined function that received ** than the one containing the application-defined function that received
** the [sqlite3_context] pointer, the results are undefined. ** the [sqlite3_context] pointer, the results are undefined.
*/ */
@ -6816,7 +6816,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name ** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
** for the N-th database on database connection D, or a NULL pointer of N is ** for the N-th database on database connection D, or a NULL pointer if N is
** out of range. An N value of 0 means the main database file. An N of 1 is ** out of range. An N value of 0 means the main database file. An N of 1 is
** the "temp" schema. Larger values of N correspond to various ATTACH-ed ** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** databases. ** databases.
@ -6911,7 +6911,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_READ state means that the database is currently ** <dd>The SQLITE_TXN_READ state means that the database is currently
** in a read transaction. Content has been read from the database file ** in a read transaction. Content has been read from the database file
** but nothing in the database file has changed. The transaction state ** but nothing in the database file has changed. The transaction state
** will advanced to SQLITE_TXN_WRITE if any changes occur and there are ** will be advanced to SQLITE_TXN_WRITE if any changes occur and there are
** no other conflicting concurrent write transactions. The transaction ** no other conflicting concurrent write transactions. The transaction
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or ** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [COMMIT].</dd> ** [COMMIT].</dd>
@ -6920,7 +6920,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_WRITE state means that the database is currently ** <dd>The SQLITE_TXN_WRITE state means that the database is currently
** in a write transaction. Content has been written to the database file ** in a write transaction. Content has been written to the database file
** but has not yet committed. The transaction state will change to ** but has not yet committed. The transaction state will change to
** to SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd> ** SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
*/ */
#define SQLITE_TXN_NONE 0 #define SQLITE_TXN_NONE 0
#define SQLITE_TXN_READ 1 #define SQLITE_TXN_READ 1
@ -7201,7 +7201,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size ** CAPI3REF: Impose A Limit On Heap Size
** **
** These interfaces impose limits on the amount of heap memory that will be ** These interfaces impose limits on the amount of heap memory that will be
** by all database connections within a single process. ** used by all database connections within a single process.
** **
** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the ** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the
** soft limit on the amount of heap memory that may be allocated by SQLite. ** soft limit on the amount of heap memory that may be allocated by SQLite.
@ -7259,7 +7259,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** </ul>)^ ** </ul>)^
** **
** The circumstances under which SQLite will enforce the heap limits may ** The circumstances under which SQLite will enforce the heap limits may
** changes in future releases of SQLite. ** change in future releases of SQLite.
*/ */
SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N);
SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N);
@ -7374,8 +7374,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc. ** ^The entry point is zProc.
** ^(zProc may be 0, in which case SQLite will try to come up with an ** ^(zProc may be 0, in which case SQLite will try to come up with an
** entry point name on its own. It first tries "sqlite3_extension_init". ** entry point name on its own. It first tries "sqlite3_extension_init".
** If that does not work, it constructs a name "sqlite3_X_init" where the ** If that does not work, it constructs a name "sqlite3_X_init" where
** X is consists of the lower-case equivalent of all ASCII alphabetic ** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following ** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^ ** "." and omitting any initial "lib".)^
** ^The sqlite3_load_extension() interface returns ** ^The sqlite3_load_extension() interface returns
@ -7446,7 +7446,7 @@ SQLITE_API int sqlite3_enable_load_extension(sqlite3 *db, int onoff);
** ^(Even though the function prototype shows that xEntryPoint() takes ** ^(Even though the function prototype shows that xEntryPoint() takes
** no arguments and returns void, SQLite invokes xEntryPoint() with three ** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the ** arguments and expects an integer result as if the signature of the
** entry point where as follows: ** entry point were as follows:
** **
** <blockquote><pre> ** <blockquote><pre>
** &nbsp; int xEntryPoint( ** &nbsp; int xEntryPoint(
@ -7610,7 +7610,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The ** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag ** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** is left in its default setting of false, the constraint will always be ** is left in its default setting of false, the constraint will always be
** checked separately in byte code. If the omit flag is change to true, then ** checked separately in byte code. If the omit flag is changed to true, then
** the constraint may or may not be checked in byte code. In other words, ** the constraint may or may not be checked in byte code. In other words,
** when the omit flag is true there is no guarantee that the constraint will ** when the omit flag is true there is no guarantee that the constraint will
** not be checked again using byte code.)^ ** not be checked again using byte code.)^
@ -7636,7 +7636,7 @@ struct sqlite3_module {
** The xBestIndex method may optionally populate the idxFlags field with a ** The xBestIndex method may optionally populate the idxFlags field with a
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is ** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN] ** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
** output to show the idxNum has hex instead of as decimal. Another flag is ** output to show the idxNum as hex instead of as decimal. Another flag is
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will ** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
** return at most one row. ** return at most one row.
** **
@ -7777,7 +7777,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth ** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through ** parameter is an arbitrary client data pointer that is passed through
** into the [xCreate] and [xConnect] methods of the virtual table module ** into the [xCreate] and [xConnect] methods of the virtual table module
** when a new virtual table is be being created or reinitialized. ** when a new virtual table is being created or reinitialized.
** **
** ^The sqlite3_create_module_v2() interface has a fifth parameter which ** ^The sqlite3_create_module_v2() interface has a fifth parameter which
** is a pointer to a destructor for the pClientData. ^SQLite will ** is a pointer to a destructor for the pClientData. ^SQLite will
@ -7942,7 +7942,7 @@ typedef struct sqlite3_blob sqlite3_blob;
** in *ppBlob. Otherwise an [error code] is returned and, unless the error ** in *ppBlob. Otherwise an [error code] is returned and, unless the error
** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided ** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided
** the API is not misused, it is always safe to call [sqlite3_blob_close()] ** the API is not misused, it is always safe to call [sqlite3_blob_close()]
** on *ppBlob after this function it returns. ** on *ppBlob after this function returns.
** **
** This function fails with SQLITE_ERROR if any of the following are true: ** This function fails with SQLITE_ERROR if any of the following are true:
** <ul> ** <ul>
@ -8062,7 +8062,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
** **
** ^Returns the size in bytes of the BLOB accessible via the ** ^Returns the size in bytes of the BLOB accessible via the
** successfully opened [BLOB handle] in its only argument. ^The ** successfully opened [BLOB handle] in its only argument. ^The
** incremental blob I/O routines can only read or overwriting existing ** incremental blob I/O routines can only read or overwrite existing
** blob content; they cannot change the size of a blob. ** blob content; they cannot change the size of a blob.
** **
** This routine only works on a [BLOB handle] which has been created ** This routine only works on a [BLOB handle] which has been created
@ -8212,7 +8212,7 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
** ^The sqlite3_mutex_alloc() routine allocates a new ** ^The sqlite3_mutex_alloc() routine allocates a new
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc() ** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** routine returns NULL if it is unable to allocate the requested ** routine returns NULL if it is unable to allocate the requested
** mutex. The argument to sqlite3_mutex_alloc() must one of these ** mutex. The argument to sqlite3_mutex_alloc() must be one of these
** integer constants: ** integer constants:
** **
** <ul> ** <ul>
@ -8445,7 +8445,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection ** CAPI3REF: Retrieve the mutex for a database connection
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^This interface returns a pointer the [sqlite3_mutex] object that ** ^This interface returns a pointer to the [sqlite3_mutex] object that
** serializes access to the [database connection] given in the argument ** serializes access to the [database connection] given in the argument
** when the [threading mode] is Serialized. ** when the [threading mode] is Serialized.
** ^If the [threading mode] is Single-thread or Multi-thread then this ** ^If the [threading mode] is Single-thread or Multi-thread then this
@ -8568,7 +8568,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** CAPI3REF: SQL Keyword Checking ** CAPI3REF: SQL Keyword Checking
** **
** These routines provide access to the set of SQL language keywords ** These routines provide access to the set of SQL language keywords
** recognized by SQLite. Applications can uses these routines to determine ** recognized by SQLite. Applications can use these routines to determine
** whether or not a specific identifier needs to be escaped (for example, ** whether or not a specific identifier needs to be escaped (for example,
** by enclosing in double-quotes) so as not to confuse the parser. ** by enclosing in double-quotes) so as not to confuse the parser.
** **
@ -8736,7 +8736,7 @@ SQLITE_API void sqlite3_str_reset(sqlite3_str*);
** content of the dynamic string under construction in X. The value ** content of the dynamic string under construction in X. The value
** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X ** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X
** and might be freed or altered by any subsequent method on the same ** and might be freed or altered by any subsequent method on the same
** [sqlite3_str] object. Applications must not used the pointer returned ** [sqlite3_str] object. Applications must not use the pointer returned by
** [sqlite3_str_value(X)] after any subsequent method call on the same ** [sqlite3_str_value(X)] after any subsequent method call on the same
** object. ^Applications may change the content of the string returned ** object. ^Applications may change the content of the string returned
** by [sqlite3_str_value(X)] as long as they do not write into any bytes ** by [sqlite3_str_value(X)] as long as they do not write into any bytes
@ -8822,7 +8822,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE] ** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The ** buffer and where forced to overflow to [sqlite3_malloc()]. The
** returned value includes allocations that overflowed because they ** returned value includes allocations that overflowed because they
** where too large (they were larger than the "sz" parameter to ** were too large (they were larger than the "sz" parameter to
** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because ** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because
** no space was left in the page cache.</dd>)^ ** no space was left in the page cache.</dd>)^
** **
@ -8906,28 +8906,29 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt> ** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt>
** <dd>This parameter returns the number of malloc attempts that were ** <dd>This parameter returns the number of malloc attempts that were
** satisfied using lookaside memory. Only the high-water value is meaningful; ** satisfied using lookaside memory. Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to the amount of ** been satisfied using lookaside memory but failed due to the amount of
** memory requested being larger than the lookaside slot size. ** memory requested being larger than the lookaside slot size.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to all lookaside ** been satisfied using lookaside memory but failed due to all lookaside
** memory already being in use. ** memory already being in use.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt> ** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^ ** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]] ** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt> ** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt>
@ -8936,10 +8937,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** memory used by that pager cache is divided evenly between the attached ** memory used by that pager cache is divided evenly between the attached
** connections.)^ In other words, if none of the pager caches associated ** connections.)^ In other words, if none of the pager caches associated
** with the database connection are shared, this request returns the same ** with the database connection are shared, this request returns the same
** value as DBSTATUS_CACHE_USED. Or, if one or more or the pager caches are ** value as DBSTATUS_CACHE_USED. Or, if one or more of the pager caches are
** shared, the value returned by this call will be smaller than that returned ** shared, the value returned by this call will be smaller than that returned
** by DBSTATUS_CACHE_USED. ^The highwater mark associated with ** by DBSTATUS_CACHE_USED. ^The highwater mark associated with
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0. ** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.</dd>
** **
** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt> ** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -8949,6 +8950,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** schema memory is shared with other database connections due to ** schema memory is shared with other database connections due to
** [shared cache mode] being enabled. ** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt> ** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -8985,7 +8987,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** been written to disk in the middle of a transaction due to the page ** been written to disk in the middle of a transaction due to the page
** cache overflowing. Transactions are more efficient if they are written ** cache overflowing. Transactions are more efficient if they are written
** to disk all at once. When pages spill mid-transaction, that introduces ** to disk all at once. When pages spill mid-transaction, that introduces
** additional overhead. This parameter can be used help identify ** additional overhead. This parameter can be used to help identify
** inefficiencies that can be resolved by increasing the cache size. ** inefficiencies that can be resolved by increasing the cache size.
** </dd> ** </dd>
** **
@ -9465,7 +9467,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being ** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically ** used by the backup operation, then the backup will be automatically
** restarted by the next call to sqlite3_backup_step(). ^If the source ** restarted by the next call to sqlite3_backup_step(). ^If the source
** database is modified by the using the same database connection as is used ** database is modified by using the same database connection as is used
** by the backup operation, then the backup database is automatically ** by the backup operation, then the backup database is automatically
** updated at the same time. ** updated at the same time.
** **
@ -9482,7 +9484,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** and may not be used following a call to sqlite3_backup_finish(). ** and may not be used following a call to sqlite3_backup_finish().
** **
** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no ** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no
** sqlite3_backup_step() errors occurred, regardless or whether or not ** sqlite3_backup_step() errors occurred, regardless of whether or not
** sqlite3_backup_step() completed. ** sqlite3_backup_step() completed.
** ^If an out-of-memory condition or IO error occurred during any prior ** ^If an out-of-memory condition or IO error occurred during any prior
** sqlite3_backup_step() call on the same [sqlite3_backup] object, then ** sqlite3_backup_step() call on the same [sqlite3_backup] object, then
@ -10552,7 +10554,7 @@ SQLITE_API void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^If a write-transaction is open on [database connection] D when the ** ^If a write-transaction is open on [database connection] D when the
** [sqlite3_db_cacheflush(D)] interface invoked, any dirty ** [sqlite3_db_cacheflush(D)] interface is invoked, any dirty
** pages in the pager-cache that are not currently in use are written out ** pages in the pager-cache that are not currently in use are written out
** to disk. A dirty page may be in use if a database cursor created by an ** to disk. A dirty page may be in use if a database cursor created by an
** active SQL statement is reading from it, or if it is page 1 of a database ** active SQL statement is reading from it, or if it is page 1 of a database

View file

@ -9,6 +9,7 @@
#include "api/api_chat_participants.h" #include "api/api_chat_participants.h"
#include "api/api_text_entities.h" #include "api/api_text_entities.h"
#include "ayu/utils/ayu_mapper.h"
#include "ayu/ui/message_history/history_inner.h" #include "ayu/ui/message_history/history_inner.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "core/application.h" #include "core/application.h"
@ -118,7 +119,10 @@ void GenerateItems(
}; };
const auto text = QString::fromStdString(message.text); const auto text = QString::fromStdString(message.text);
addSimpleTextMessage(Ui::Text::WithEntities(text)); auto textAndEntities = Ui::Text::WithEntities(text);
const auto entities = AyuMapper::deserializeTextWithEntities(message.textEntities);
textAndEntities.entities = Api::EntitiesFromMTP(&history->session(), entities.v);
addSimpleTextMessage(std::move(textAndEntities));
} }
} // namespace MessageHistory } // namespace MessageHistory

View file

@ -855,12 +855,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) {
tr::ayu_HideChannelReactions(), tr::ayu_HideChannelReactions(),
st::settingsButtonNoIcon st::settingsButtonNoIcon
)->toggleOn( )->toggleOn(
rpl::single(!settings->hideChannelReactions) rpl::single(!settings->showChannelReactions)
)->toggledValue( )->toggledValue(
) | rpl::filter( ) | rpl::filter(
[=](bool enabled) [=](bool enabled)
{ {
return (!enabled != settings->hideChannelReactions); return (!enabled != settings->showChannelReactions);
}) | start_with_next( }) | start_with_next(
[=](bool enabled) [=](bool enabled)
{ {
@ -874,12 +874,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) {
tr::ayu_HideGroupReactions(), tr::ayu_HideGroupReactions(),
st::settingsButtonNoIcon st::settingsButtonNoIcon
)->toggleOn( )->toggleOn(
rpl::single(!settings->hideGroupReactions) rpl::single(!settings->showGroupReactions)
)->toggledValue( )->toggledValue(
) | rpl::filter( ) | rpl::filter(
[=](bool enabled) [=](bool enabled)
{ {
return (!enabled != settings->hideGroupReactions); return (!enabled != settings->showGroupReactions);
}) | start_with_next( }) | start_with_next(
[=](bool enabled) [=](bool enabled)
{ {

View file

@ -6,9 +6,14 @@
// Copyright @Radolyn, 2025 // Copyright @Radolyn, 2025
#include "ayu_mapper.h" #include "ayu_mapper.h"
#include "apiwrap.h"
#include "api/api_text_entities.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "main/main_session.h"
#include "mtproto/connection_abstract.h"
#include "mtproto/details/mtproto_dump_to_text.h"
namespace AyuMapper { namespace AyuMapper {
@ -39,17 +44,56 @@ constexpr auto kMessageFlagHasTTL = 0x02000000;
constexpr auto kMessageFlagInvertMedia = 0x08000000; constexpr auto kMessageFlagInvertMedia = 0x08000000;
constexpr auto kMessageFlagHasSavedPeer = 0x10000000; constexpr auto kMessageFlagHasSavedPeer = 0x10000000;
template<typename MTPObject>
std::vector<char> serializeObject(MTPObject object) {
mtpBuffer buffer;
object.write(buffer);
const auto from = reinterpret_cast<char*>(buffer.data());
const auto end = from + buffer.size() * sizeof(mtpPrime);
std::vector<char> entities(from, end);
return entities;
}
template<typename MTPObject>
MTPObject deserializeObject(std::vector<char> serialized) {
gsl::span<char> span(serialized.data(), serialized.size());
auto from = reinterpret_cast<const mtpPrime*>(span.data());
const auto end = from + span.size() / sizeof(mtpPrime);
MTPObject data;
if (!data.read(from, end)) {
LOG(("AyuMapper: Failed to deserialize object"));
}
return data;
}
std::pair<std::string, std::vector<char>> serializeTextWithEntities(not_null<HistoryItem*> item) { std::pair<std::string, std::vector<char>> serializeTextWithEntities(not_null<HistoryItem*> item) {
if (item->emptyText()) { if (item->emptyText()) {
return std::make_pair("", std::vector<char>()); return std::make_pair("", std::vector<char>());
} }
auto textWithEntities = item->originalText(); auto textWithEntities = item->originalText();
std::vector<char> entities; // todo: implement writing to buffer
std::vector<char> entities;
if (!textWithEntities.entities.empty()) {
const auto mtpEntities = Api::EntitiesToMTP(
&item->history()->session(),
textWithEntities.entities,
Api::ConvertOption::WithLocal);
entities = serializeObject(mtpEntities);
}
return std::make_pair(textWithEntities.text.toStdString(), entities); return std::make_pair(textWithEntities.text.toStdString(), entities);
} }
MTPVector<MTPMessageEntity> deserializeTextWithEntities(std::vector<char> serialized) {
return deserializeObject<MTPVector<MTPMessageEntity>>(serialized);
}
int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item) { int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item) {
int flags = 0; int flags = 0;

View file

@ -8,7 +8,14 @@
namespace AyuMapper { namespace AyuMapper {
template<typename MTPObject>
[[nodiscard]] MTPObject deserializeObject(std::vector<char> serialized);
template<typename MTPObject>
[[nodiscard]] std::vector<char> serializeObject(MTPObject object);
std::pair<std::string, std::vector<char>> serializeTextWithEntities(not_null<HistoryItem*> item); std::pair<std::string, std::vector<char>> serializeTextWithEntities(not_null<HistoryItem*> item);
[[nodiscard]] MTPVector<MTPMessageEntity> deserializeTextWithEntities(std::vector<char> serialized);
int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item); int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item);
} // namespace AyuMapper } // namespace AyuMapper

View file

@ -14,19 +14,21 @@
std::unordered_set<ID> default_developers = { std::unordered_set<ID> default_developers = {
963080346, 1282540315, 1374434073, 168769611, 963080346, 1282540315, 1374434073, 168769611,
1773117711, 5330087923, 666154369, 139303278, 1773117711, 5330087923, 139303278, 1752394339,
668557709, 1348136086, 6288255532, 7453676178, 668557709, 1348136086, 6288255532, 7453676178,
880708503, 2135966128, 7818249287,
// ------------------------------------------- // -------------------------------------------
778327202, 238292700, 1795176335, 6247153446, 778327202, 238292700, 1795176335, 6247153446,
1752394339, 7745305003, 1183312839, 497855299, 1183312839, 497855299
623054735
}; };
std::unordered_set<ID> default_channels = { std::unordered_set<ID> default_channels = {
1233768168, 1524581881, 1571726392, 1632728092, 1233768168, 1524581881, 1571726392, 1632728092,
1172503281, 1877362358, 1905581924, 1794457129, 1172503281, 1877362358, 1905581924, 1794457129,
1434550607, 1947958814, 1815864846, 2130395384, 1434550607, 1947958814, 1815864846, 2130395384,
1976430343, 1754537498, 1725670701, 1976430343, 1754537498, 1725670701, 2401498637,
2685666919, 2562664432, 2564770112, 2331068091,
1559501352, 2641258043
}; };
void RCManager::start() { void RCManager::start() {

View file

@ -320,6 +320,22 @@ QString formatDateTime(const QDateTime &date) {
return datePart + getLocalizedAt() + timePart; return datePart + getLocalizedAt() + timePart;
} }
QString formatMessageTime(const QTime &time) {
const auto &settings = AyuSettings::getInstance();
const auto format =
settings.showMessageSeconds
? (QLocale().timeFormat(QLocale::ShortFormat).contains("AP")
? "h:mm:ss AP"
: "HH:mm:ss")
: QLocale().timeFormat(QLocale::ShortFormat);
return QLocale().toString(
time,
format
);
}
int getMediaSizeBytes(not_null<HistoryItem*> message) { int getMediaSizeBytes(not_null<HistoryItem*> message) {
if (!message->media()) { if (!message->media()) {
return -1; return -1;

View file

@ -37,6 +37,7 @@ void readHistory(not_null<HistoryItem*> message);
QString formatTTL(int time); QString formatTTL(int time);
QString formatDateTime(const QDateTime &date); QString formatDateTime(const QDateTime &date);
QString formatMessageTime(const QTime &time);
QString getDCName(int dc); QString getDCName(int dc);

View file

@ -111,6 +111,13 @@ void AddProxyFromClipboard(
QGuiApplication::clipboard()->text()); QGuiApplication::clipboard()->text());
const auto isSingle = maybeUrls.size() == 1; const auto isSingle = maybeUrls.size() == 1;
enum class Result {
Success,
Failed,
Unsupported,
Invalid,
};
const auto proceedUrl = [=](const auto &local) { const auto proceedUrl = [=](const auto &local) {
const auto command = base::StringViewMid( const auto command = base::StringViewMid(
local, local,
@ -146,6 +153,11 @@ void AddProxyFromClipboard(
match->captured(1), match->captured(1),
qthelp::UrlParamNameTransform::ToLower); qthelp::UrlParamNameTransform::ToLower);
const auto proxy = ProxyDataFromFields(type, fields); const auto proxy = ProxyDataFromFields(type, fields);
if (!proxy) {
return (proxy.status() == ProxyData::Status::Unsupported)
? Result::Unsupported
: Result::Invalid;
}
const auto contains = controller->contains(proxy); const auto contains = controller->contains(proxy);
const auto toast = (contains const auto toast = (contains
? tr::lng_proxy_add_from_clipboard_existing_toast ? tr::lng_proxy_add_from_clipboard_existing_toast
@ -158,19 +170,29 @@ void AddProxyFromClipboard(
} }
break; break;
} }
return true; return Result::Success;
} }
return false; return Result::Failed;
}; };
auto success = false; auto success = Result::Failed;
for (const auto &maybeUrl : maybeUrls) { for (const auto &maybeUrl : maybeUrls) {
success |= proceedUrl(Core::TryConvertUrlToLocal(maybeUrl)); const auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));
if (success != Result::Success) {
success = result;
}
} }
if (!success) { if (success != Result::Success) {
if (success == Result::Failed) {
show->showToast( show->showToast(
tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
} else {
show->showBox(Ui::MakeInformBox(
(success == Result::Unsupported
? tr::lng_proxy_unsupported(tr::now)
: tr::lng_proxy_invalid(tr::now))));
}
} }
} }

View file

@ -361,9 +361,14 @@ void CreateModerateMessagesBox(
}); });
} }
if (allCanBan) { if (allCanBan) {
auto ownedWrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( const auto peer = items.front()->history()->peer;
auto ownedWrap = peer->isMonoforum()
? nullptr
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner, inner,
object_ptr<Ui::VerticalLayout>(inner)); object_ptr<Ui::VerticalLayout>(inner));
auto computeRestrictions = Fn<ChatRestrictions()>();
const auto wrap = ownedWrap.data();
Ui::AddSkip(inner); Ui::AddSkip(inner);
Ui::AddSkip(inner); Ui::AddSkip(inner);
@ -371,7 +376,9 @@ void CreateModerateMessagesBox(
object_ptr<Ui::Checkbox>( object_ptr<Ui::Checkbox>(
box, box,
rpl::conditional( rpl::conditional(
ownedWrap->toggledValue(), (ownedWrap
? ownedWrap->toggledValue()
: rpl::single(false) | rpl::type_erased()),
tr::lng_restrict_user( tr::lng_restrict_user(
lt_count, lt_count,
rpl::single(participants.size()) | tr::to_count()), rpl::single(participants.size()) | tr::to_count()),
@ -390,7 +397,9 @@ void CreateModerateMessagesBox(
Ui::AddSkip(inner); Ui::AddSkip(inner);
Ui::AddSkip(inner); Ui::AddSkip(inner);
const auto wrap = inner->add(std::move(ownedWrap)); if (ownedWrap) {
inner->add(std::move(ownedWrap));
const auto container = wrap->entity(); const auto container = wrap->entity();
wrap->toggle(false, anim::type::instant); wrap->toggle(false, anim::type::instant);
@ -468,7 +477,6 @@ void CreateModerateMessagesBox(
using Flag = ChatRestriction; using Flag = ChatRestriction;
using Flags = ChatRestrictions; using Flags = ChatRestrictions;
const auto peer = items.front()->history()->peer;
const auto chat = peer->asChat(); const auto chat = peer->asChat();
const auto channel = peer->asChannel(); const auto channel = peer->asChannel();
const auto defaultRestrictions = chat const auto defaultRestrictions = chat
@ -507,6 +515,7 @@ void CreateModerateMessagesBox(
prepareFlags, prepareFlags,
disabledMessages, disabledMessages,
{ .isForum = peer->isForum() }); { .isForum = peer->isForum() });
computeRestrictions = getRestrictions;
std::move(changes) | rpl::start_with_next([=] { std::move(changes) | rpl::start_with_next([=] {
ban->setChecked(true); ban->setChecked(true);
}, ban->lifetime()); }, ban->lifetime());
@ -514,12 +523,15 @@ void CreateModerateMessagesBox(
Ui::AddDivider(container); Ui::AddDivider(container);
Ui::AddSkip(container); Ui::AddSkip(container);
container->add(std::move(checkboxes)); container->add(std::move(checkboxes));
}
// Handle confirmation manually. // Handle confirmation manually.
confirms->events() | rpl::start_with_next([=] { confirms->events() | rpl::start_with_next([=] {
if (ban->checked() && controller->collectRequests) { if (ban->checked() && controller->collectRequests) {
const auto kick = !wrap->toggled(); const auto kick = !wrap || !wrap->toggled();
const auto restrictions = getRestrictions(); const auto restrictions = computeRestrictions
? computeRestrictions()
: ChatRestrictions();
const auto request = [=]( const auto request = [=](
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<ChannelData*> channel) { not_null<ChannelData*> channel) {
@ -532,10 +544,15 @@ void CreateModerateMessagesBox(
nullptr, nullptr,
nullptr); nullptr);
} else { } else {
channel->session().api().chatParticipants().kick( const auto block = channel->isMonoforum()
channel, ? channel->monoforumBroadcast()
: channel.get();
if (block) {
block->session().api().chatParticipants().kick(
block,
peer, peer,
{ channel->restrictions(), 0 }); { block->restrictions(), 0 });
}
} }
}; };
sequentiallyRequest(request, controller->collectRequests()); sequentiallyRequest(request, controller->collectRequests());

View file

@ -127,6 +127,7 @@ constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
constexpr auto kResaleGiftsPerPage = 50; constexpr auto kResaleGiftsPerPage = 50;
constexpr auto kFiltersCount = 4; constexpr auto kFiltersCount = 4;
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
using namespace HistoryView; using namespace HistoryView;
using namespace Info::PeerGifts; using namespace Info::PeerGifts;
@ -220,6 +221,33 @@ struct GiftDetails {
bool byStars = false; bool byStars = false;
}; };
struct SessionResalePrices {
explicit SessionResalePrices(not_null<Main::Session*> session)
: api(std::make_unique<Api::PremiumGiftCodeOptions>(session->user())) {
}
std::unique_ptr<Api::PremiumGiftCodeOptions> api;
base::flat_map<QString, int> prices;
std::vector<Fn<void()>> waiting;
rpl::lifetime requestLifetime;
crl::time lastReceived = 0;
};
[[nodiscard]] not_null<SessionResalePrices*> ResalePrices(
not_null<Main::Session*> session) {
static auto result = base::flat_map<
not_null<Main::Session*>,
std::unique_ptr<SessionResalePrices>>();
if (const auto i = result.find(session); i != end(result)) {
return i->second.get();
}
const auto i = result.emplace(
session,
std::make_unique<SessionResalePrices>(session)).first;
session->lifetime().add([session] { result.remove(session); });
return i->second.get();
}
class PeerRow final : public PeerListRow { class PeerRow final : public PeerListRow {
public: public:
using PeerListRow::PeerListRow; using PeerListRow::PeerListRow;
@ -4381,6 +4409,55 @@ void ShowUniqueGiftWearBox(
})); }));
} }
void PreloadUniqueGiftResellPrices(not_null<Main::Session*> session) {
const auto entry = ResalePrices(session);
const auto now = crl::now();
const auto makeRequest = entry->prices.empty()
|| (now - entry->lastReceived >= kResellPriceCacheLifetime);
if (!makeRequest || entry->requestLifetime) {
return;
}
const auto finish = [=] {
entry->requestLifetime.destroy();
entry->lastReceived = crl::now();
for (const auto &callback : base::take(entry->waiting)) {
callback();
}
};
entry->requestLifetime = entry->api->requestStarGifts(
) | rpl::start_with_error_done(finish, [=] {
const auto &gifts = entry->api->starGifts();
entry->prices.reserve(gifts.size());
for (auto &gift : gifts) {
if (!gift.resellTitle.isEmpty() && gift.starsResellMin > 0) {
entry->prices[gift.resellTitle] = gift.starsResellMin;
}
}
finish();
});
}
void InvokeWithUniqueGiftResellPrice(
not_null<Main::Session*> session,
const QString &title,
Fn<void(int)> callback) {
PreloadUniqueGiftResellPrices(session);
const auto finish = [=] {
const auto entry = ResalePrices(session);
Assert(entry->lastReceived != 0);
const auto i = entry->prices.find(title);
callback((i != end(entry->prices)) ? i->second : 0);
};
const auto entry = ResalePrices(session);
if (entry->lastReceived) {
finish();
} else {
entry->waiting.push_back(finish);
}
}
void UpdateGiftSellPrice( void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,
@ -4422,15 +4499,13 @@ void UpdateGiftSellPrice(
}).send(); }).send();
} }
void ShowUniqueGiftSellBox( void UniqueGiftSellBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId, Data::SavedStarGiftId savedId,
int price,
Settings::GiftWearBoxStyleOverride st) { Settings::GiftWearBoxStyleOverride st) {
if (ShowResaleGiftLater(show, unique)) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_gift_sell_title()); box->setTitle(tr::lng_gift_sell_title());
box->setStyle(st.box ? *st.box : st::upgradeGiftBox); box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth); box->setWidth(st::boxWideWidth);
@ -4462,7 +4537,7 @@ void ShowUniqueGiftSellBox(
wrap, wrap,
st::editTagField, st::editTagField,
rpl::single(QString()), rpl::single(QString()),
QString::number(priceNow ? priceNow : minimal), QString::number(priceNow ? priceNow : price ? price : minimal),
limit); limit);
const auto field = owned.data(); const auto field = owned.data();
wrap->widthValue() | rpl::start_with_next([=](int width) { wrap->widthValue() | rpl::start_with_next([=](int width) {
@ -4548,7 +4623,21 @@ void ShowUniqueGiftSellBox(
button->moveToLeft(padding.left(), padding.top()); button->moveToLeft(padding.left(), padding.top());
} }
}, box->lifetime()); }, box->lifetime());
})); }
void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId,
Settings::GiftWearBoxStyleOverride st) {
if (ShowResaleGiftLater(show, unique)) {
return;
}
const auto session = &show->session();
const auto &title = unique->title;
InvokeWithUniqueGiftResellPrice(session, title, [=](int price) {
show->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st));
});
} }
void GiftReleasedByHandler(not_null<PeerData*> peer) { void GiftReleasedByHandler(not_null<PeerData*> peer) {

View file

@ -21,6 +21,7 @@ class SavedStarGiftId;
} // namespace Data } // namespace Data
namespace Main { namespace Main {
class Session;
class SessionShow; class SessionShow;
} // namespace Main } // namespace Main
@ -71,6 +72,8 @@ void ShowUniqueGiftWearBox(
const Data::UniqueGift &gift, const Data::UniqueGift &gift,
Settings::GiftWearBoxStyleOverride st); Settings::GiftWearBoxStyleOverride st);
void PreloadUniqueGiftResellPrices(not_null<Main::Session*> session);
void UpdateGiftSellPrice( void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,

View file

@ -768,12 +768,14 @@ void StickerSetBox::updateButtons() {
&st::menuIconManage); &st::menuIconManage);
}); });
}(); }();
const auto addPackOwner = [=](const std::shared_ptr<base::unique_qptr<Ui::PopupMenu>> &menu) const auto addPackIdActions = [=](const std::shared_ptr<base::unique_qptr<Ui::PopupMenu>> &menu)
{ {
if (type == Data::StickersType::Stickers || type == Data::StickersType::Emoji) { if (type == Data::StickersType::Stickers || type == Data::StickersType::Emoji) {
const auto &settings = AyuSettings::getInstance();
const auto weak = Ui::MakeWeak(this); const auto weak = Ui::MakeWeak(this);
const auto session = _session; const auto session = _session;
const auto innerId = _inner->setId() >> 32; const auto setId = _inner->setId();
const auto innerId = setId >> 32;
(*menu)->addAction( (*menu)->addAction(
tr::ayu_MessageDetailsPackOwnerPC(tr::now), tr::ayu_MessageDetailsPackOwnerPC(tr::now),
@ -816,6 +818,26 @@ void StickerSetBox::updateButtons() {
}); });
}, },
&st::menuIconProfile); &st::menuIconProfile);
if (settings.showPeerId != 0) {
(*menu)->addAction(
tr::ayu_ContextCopyID(tr::now),
[weak, session, setId]
{
if (!weak) {
return;
}
const auto strongInner = weak.data();
if (!strongInner) {
return;
}
QGuiApplication::clipboard()->setText(QString::number(setId));
strongInner->showToast(tr::ayu_IDCopiedToast(tr::now));
},
&st::menuIconCopy);
}
} }
}; };
if (_inner->notInstalled()) { if (_inner->notInstalled()) {
@ -865,7 +887,7 @@ void StickerSetBox::updateButtons() {
: tr::lng_stickers_share_pack)(tr::now), : tr::lng_stickers_share_pack)(tr::now),
[=] { share(); closeBox(); }, [=] { share(); closeBox(); },
&st::menuIconShare); &st::menuIconShare);
addPackOwner(menu); addPackIdActions(menu);
(*menu)->popup(QCursor::pos()); (*menu)->popup(QCursor::pos());
return true; return true;
}); });
@ -918,7 +940,7 @@ void StickerSetBox::updateButtons() {
archive, archive,
&st::menuIconArchive); &st::menuIconArchive);
} }
addPackOwner(menu); addPackIdActions(menu);
(*menu)->popup(QCursor::pos()); (*menu)->popup(QCursor::pos());
return true; return true;
}); });
@ -1486,6 +1508,16 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
QGuiApplication::clipboard()->setMimeData(data.release()); QGuiApplication::clipboard()->setMimeData(data.release());
} }
}, &st::menuIconCopy); }, &st::menuIconCopy);
const auto &settings = AyuSettings::getInstance();
if (settings.showPeerId != 0) {
_menu->addAction(tr::ayu_ContextCopyID(tr::now),
[=]
{
QGuiApplication::clipboard()->setText(QString::number(_pack[index]->id));
},
&st::menuIconCopy);
}
} }
} else if (details.type != SendMenu::Type::Disabled) { } else if (details.type != SendMenu::Type::Disabled) {
const auto document = _pack[index]; const auto document = _pack[index];

View file

@ -425,8 +425,6 @@ void TransferGift(
Data::SavedStarGiftId savedId, Data::SavedStarGiftId savedId,
Fn<void(Payments::CheckoutResult)> done, Fn<void(Payments::CheckoutResult)> done,
bool skipPaymentForm = false) { bool skipPaymentForm = false) {
Expects(to->isUser());
const auto session = &window->session(); const auto session = &window->session();
const auto weak = base::make_weak(window); const auto weak = base::make_weak(window);
auto formDone = [=]( auto formDone = [=](

View file

@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04;
constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kReactionsCountEmojiProperty = 0x05;
constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kDocumentFilenameTooltipProperty = 0x06;
constexpr auto kPhoneNumberLinkProperty = 0x07; constexpr auto kPhoneNumberLinkProperty = 0x07;
constexpr auto kTodoListItemIdProperty = 0x08;
namespace Ui { namespace Ui {
class Show; class Show;

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 = 5016003; constexpr auto AppVersion = 5016004;
constexpr auto AppVersionStr = "5.16.3"; constexpr auto AppVersionStr = "5.16.4";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -32,8 +32,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
namespace { namespace {
constexpr auto kMs = crl::time(1000);
constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);
const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
[[nodiscard]] bool TooEarlyForRequest(crl::time received) { [[nodiscard]] bool TooEarlyForRequest(crl::time received) {
return (received > 0) && (received + kRequestTimeLimit > crl::now()); return (received > 0) && (received + kRequestTimeLimit > crl::now());
} }
@ -77,17 +80,21 @@ void SponsoredMessages::clear() {
void SponsoredMessages::clearOldRequests() { void SponsoredMessages::clearOldRequests() {
const auto now = crl::now(); const auto now = crl::now();
const auto clear = [&](auto &requests) {
while (true) { while (true) {
const auto i = ranges::find_if(_requests, [&](const auto &value) { const auto i = ranges::find_if(requests, [&](const auto &value) {
const auto &request = value.second; const auto &request = value.second;
return !request.requestId return !request.requestId
&& (request.lastReceived + kRequestTimeLimit <= now); && (request.lastReceived + kRequestTimeLimit <= now);
}); });
if (i == end(_requests)) { if (i == end(requests)) {
break; break;
} }
_requests.erase(i); requests.erase(i);
} }
};
clear(_requests);
clear(_requestsForVideo);
} }
SponsoredMessages::AppendResult SponsoredMessages::append( SponsoredMessages::AppendResult SponsoredMessages::append(
@ -241,6 +248,16 @@ bool SponsoredMessages::canHaveFor(not_null<History*> history) const {
return false; return false;
} }
bool SponsoredMessages::canHaveFor(not_null<HistoryItem*> item) const {
const auto &settings = AyuSettings::getInstance();
if (settings.disableAds) {
return false;
}
return item->history()->peer->isBroadcast()
&& item->isRegular();
}
bool SponsoredMessages::isTopBarFor(not_null<History*> history) const { bool SponsoredMessages::isTopBarFor(not_null<History*> history) const {
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
if (settings.disableAds) { if (settings.disableAds) {
@ -291,6 +308,78 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
}).send(); }).send();
} }
void SponsoredMessages::requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done) {
Expects(done != nullptr);
if (!canHaveFor(item)) {
done({});
return;
}
const auto peer = item->history()->peer;
auto &request = _requestsForVideo[peer];
if (TooEarlyForRequest(request.lastReceived)) {
auto prepared = prepareForVideo(peer);
if (prepared.list.empty()
|| prepared.state.itemIndex < prepared.list.size()
|| prepared.state.leftTillShow > 0) {
done(std::move(prepared));
return;
}
}
request.callbacks.push_back(std::move(done));
if (request.requestId) {
return;
}
{
const auto it = _dataForVideo.find(peer);
if (it != end(_dataForVideo)) {
auto &list = it->second;
// Don't rebuild currently displayed messages.
const auto proj = [](const Entry &e) {
return e.item != nullptr;
};
if (ranges::any_of(list.entries, proj)) {
return;
}
}
}
const auto finish = [=] {
const auto i = _requestsForVideo.find(peer);
if (i != end(_requestsForVideo)) {
for (const auto &callback : base::take(i->second.callbacks)) {
callback(prepareForVideo(peer));
}
}
};
using Flag = MTPmessages_GetSponsoredMessages::Flag;
request.requestId = _session->api().request(
MTPmessages_GetSponsoredMessages(
MTP_flags(Flag::f_msg_id),
peer->input,
MTP_int(item->id.bare))
).done([=](const MTPmessages_sponsoredMessages &result) {
parseForVideo(peer, result);
finish();
}).fail([=] {
_requestsForVideo.remove(peer);
finish();
}).send();
}
void SponsoredMessages::updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state) {
if (state.initial()) {
return;
}
const auto i = _dataForVideo.find(_session->data().peer(itemId.peer));
if (i != end(_dataForVideo)) {
i->second.state = state;
}
}
void SponsoredMessages::parse( void SponsoredMessages::parse(
not_null<History*> history, not_null<History*> history,
const MTPmessages_sponsoredMessages &list) { const MTPmessages_sponsoredMessages &list) {
@ -306,12 +395,9 @@ void SponsoredMessages::parse(
_session->data().processChats(data.vchats()); _session->data().processChats(data.vchats());
const auto &messages = data.vmessages().v; const auto &messages = data.vmessages().v;
auto &list = _data.emplace(history, List()).first->second; auto &list = _data.emplace(history).first->second;
list.entries.clear(); list.entries.clear();
list.received = crl::now(); list.received = crl::now();
for (const auto &message : messages) {
append(history, list, message);
}
if (const auto postsBetween = data.vposts_between()) { if (const auto postsBetween = data.vposts_between()) {
list.postsBetween = postsBetween->v; list.postsBetween = postsBetween->v;
list.state = State::InjectToMiddle; list.state = State::InjectToMiddle;
@ -320,10 +406,66 @@ void SponsoredMessages::parse(
? State::AppendToEnd ? State::AppendToEnd
: State::AppendToTopBar; : State::AppendToTopBar;
} }
for (const auto &message : messages) {
append([=] {
return &_data[history].entries;
}, history, message);
}
}, [](const MTPDmessages_sponsoredMessagesEmpty &) { }, [](const MTPDmessages_sponsoredMessagesEmpty &) {
}); });
} }
void SponsoredMessages::parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list) {
auto &request = _requestsForVideo[peer];
request.lastReceived = crl::now();
request.requestId = 0;
if (!_clearTimer.isActive()) {
_clearTimer.callOnce(kRequestTimeLimit * 2);
}
list.match([&](const MTPDmessages_sponsoredMessages &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
const auto history = _session->data().history(peer);
const auto &messages = data.vmessages().v;
auto &list = _dataForVideo.emplace(peer).first->second;
list.entries.clear();
list.received = crl::now();
list.startDelay = data.vstart_delay().value_or_empty() * kMs;
list.betweenDelay = data.vbetween_delay().value_or_empty() * kMs;
for (const auto &message : messages) {
append([=] {
return &_dataForVideo[peer].entries;
}, history, message);
}
}, [](const MTPDmessages_sponsoredMessagesEmpty &) {
});
}
SponsoredForVideo SponsoredMessages::prepareForVideo(
not_null<PeerData*> peer) {
const auto &settings = AyuSettings::getInstance();
if (settings.disableAds) {
return {};
}
const auto i = _dataForVideo.find(peer);
if (i == end(_dataForVideo) || i->second.entries.empty()) {
return {};
}
return SponsoredForVideo{
.list = i->second.entries | ranges::views::transform(
&Entry::sponsored
) | ranges::to_vector,
.startDelay = i->second.startDelay,
.betweenDelay = i->second.betweenDelay,
.state = i->second.state,
};
}
FullMsgId SponsoredMessages::fillTopBar( FullMsgId SponsoredMessages::fillTopBar(
not_null<History*> history, not_null<History*> history,
not_null<Ui::RpWidget*> widget) { not_null<Ui::RpWidget*> widget) {
@ -373,8 +515,8 @@ rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {
} }
void SponsoredMessages::append( void SponsoredMessages::append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history, not_null<History*> history,
List &list,
const MTPSponsoredMessage &message) { const MTPSponsoredMessage &message) {
const auto &data = message.data(); const auto &data = message.data();
const auto randomId = data.vrandom_id().v; const auto randomId = data.vrandom_id().v;
@ -385,14 +527,14 @@ void SponsoredMessages::append(
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) { data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) { if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) { tlPhoto->match([&](const MTPDphoto &data) {
mediaPhoto = history->owner().processPhoto(data); mediaPhoto = _session->data().processPhoto(data);
}, [](const MTPDphotoEmpty &) { }, [](const MTPDphotoEmpty &) {
}); });
} }
}, [&](const MTPDmessageMediaDocument &media) { }, [&](const MTPDmessageMediaDocument &media) {
if (const auto tlDocument = media.vdocument()) { if (const auto tlDocument = media.vdocument()) {
tlDocument->match([&](const MTPDdocument &data) { tlDocument->match([&](const MTPDdocument &data) {
const auto d = history->owner().processDocument( const auto d = _session->data().processDocument(
data, data,
media.valt_documents()); media.valt_documents());
if (d->isVideoFile() if (d->isVideoFile()
@ -413,7 +555,7 @@ void SponsoredMessages::append(
.link = qs(data.vurl()), .link = qs(data.vurl()),
.buttonText = qs(data.vbutton_text()), .buttonText = qs(data.vbutton_text()),
.photoId = data.vphoto() .photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id ? _session->data().processPhoto(*data.vphoto())->id
: PhotoId(0), : PhotoId(0),
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0), .mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0), .mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
@ -449,25 +591,24 @@ void SponsoredMessages::append(
.link = from.link, .link = from.link,
.sponsorInfo = std::move(sponsorInfo), .sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo), .additionalInfo = std::move(additionalInfo),
.durationMin = data.vmin_display_duration().value_or_empty() * kMs,
.durationMax = data.vmax_display_duration().value_or_empty() * kMs,
}; };
list.entries.push_back({ const auto itemId = FullMsgId(
.sponsored = std::move(sharedMessage),
});
auto &entry = list.entries.back();
const auto itemId = entry.itemFullId = FullMsgId(
history->peer->id, history->peer->id,
_session->data().nextLocalMessageId()); _session->data().nextLocalMessageId());
const auto list = entries();
list->push_back({
.itemFullId = itemId,
.sponsored = std::move(sharedMessage),
});
auto &entry = list->back();
const auto fileOrigin = FileOrigin(); // No way to refresh in ads. const auto fileOrigin = FileOrigin(); // No way to refresh in ads.
static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
const auto preloaded = [=] { const auto preloaded = [=] {
const auto i = _data.find(history); const auto list = entries();
if (i == end(_data)) { const auto j = ranges::find(*list, itemId, &Entry::itemFullId);
return; if (j == end(*list)) {
}
auto &entries = i->second.entries;
const auto j = ranges::find(entries, itemId, &Entry::itemFullId);
if (j == end(entries)) {
return; return;
} }
auto &entry = *j; auto &entry = *j;
@ -565,7 +706,11 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
if (!entryPtr) { if (!entryPtr) {
return {}; return {};
} }
const auto &data = entryPtr->sponsored; return lookupDetails(entryPtr->sponsored);
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const SponsoredMessage &data) const {
return { return {
.info = Prepare(data), .info = Prepare(data),
.link = data.link, .link = data.link,

View file

@ -67,10 +67,12 @@ struct SponsoredMessage {
QByteArray randomId; QByteArray randomId;
SponsoredFrom from; SponsoredFrom from;
TextWithEntities textWithEntities; TextWithEntities textWithEntities;
History *history = nullptr; not_null<History*> history;
QString link; QString link;
TextWithEntities sponsorInfo; TextWithEntities sponsorInfo;
TextWithEntities additionalInfo; TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
}; };
struct SponsoredMessageDetails { struct SponsoredMessageDetails {
@ -92,6 +94,23 @@ struct SponsoredReportAction {
Fn<void(Data::SponsoredReportResult)>)> callback; Fn<void(Data::SponsoredReportResult)>)> callback;
}; };
struct SponsoredForVideoState {
int itemIndex = 0;
crl::time leftTillShow = 0;
[[nodiscard]] bool initial() const {
return !itemIndex && !leftTillShow;
}
};
struct SponsoredForVideo {
std::vector<SponsoredMessage> list;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
class SponsoredMessages final { class SponsoredMessages final {
public: public:
enum class AppendResult { enum class AppendResult {
@ -111,10 +130,18 @@ public:
~SponsoredMessages(); ~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const; [[nodiscard]] bool canHaveFor(not_null<History*> history) const;
[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const; [[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
void request(not_null<History*> history, Fn<void()> done); void request(not_null<History*> history, Fn<void()> done);
void requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done);
void updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state);
void clearItems(not_null<History*> history); void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const;
[[nodiscard]] Details lookupDetails( [[nodiscard]] Details lookupDetails(
const Api::SponsoredSearchResult &data) const; const Api::SponsoredSearchResult &data) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
@ -166,18 +193,35 @@ private:
int postsBetween = 0; int postsBetween = 0;
State state = State::None; State state = State::None;
}; };
struct ListForVideo {
std::vector<Entry> entries;
crl::time received = 0;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
struct Request { struct Request {
mtpRequestId requestId = 0; mtpRequestId requestId = 0;
crl::time lastReceived = 0; crl::time lastReceived = 0;
}; };
struct RequestForVideo {
std::vector<Fn<void(SponsoredForVideo)>> callbacks;
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void parse( void parse(
not_null<History*> history, not_null<History*> history,
const MTPmessages_sponsoredMessages &list); const MTPmessages_sponsoredMessages &list);
void parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list);
void append( void append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history, not_null<History*> history,
List &list,
const MTPSponsoredMessage &message); const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests(); void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const; const Entry *find(const FullMsgId &fullId) const;
@ -189,6 +233,9 @@ private:
base::flat_map<not_null<History*>, Request> _requests; base::flat_map<not_null<History*>, Request> _requests;
base::flat_map<RandomId, Request> _viewRequests; base::flat_map<RandomId, Request> _viewRequests;
base::flat_map<not_null<PeerData*>, ListForVideo> _dataForVideo;
base::flat_map<not_null<PeerData*>, RequestForVideo> _requestsForVideo;
rpl::event_stream<FullMsgId> _itemRemoved; rpl::event_stream<FullMsgId> _itemRemoved;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -72,7 +72,7 @@ namespace {
| (data.is_send_audios() ? Flag::SendMusic : Flag()) | (data.is_send_audios() ? Flag::SendMusic : Flag())
| (data.is_send_voices() ? Flag::SendVoiceMessages : Flag()) | (data.is_send_voices() ? Flag::SendVoiceMessages : Flag())
| (data.is_send_docs() ? Flag::SendFiles : Flag()) | (data.is_send_docs() ? Flag::SendFiles : Flag())
| (data.is_send_messages() ? Flag::SendOther : Flag()) | (data.is_send_plain() ? Flag::SendOther : Flag())
| (data.is_embed_links() ? Flag::EmbedLinks : Flag()) | (data.is_embed_links() ? Flag::EmbedLinks : Flag())
| (data.is_change_info() ? Flag::ChangeInfo : Flag()) | (data.is_change_info() ? Flag::ChangeInfo : Flag())
| (data.is_invite_users() ? Flag::AddParticipants : Flag()) | (data.is_invite_users() ? Flag::AddParticipants : Flag())
@ -142,7 +142,7 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) {
| ((flags & R::SendMusic) ? Flag::f_send_audios : Flag()) | ((flags & R::SendMusic) ? Flag::f_send_audios : Flag())
| ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag()) | ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag())
| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag()) | ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())
| ((flags & R::SendOther) ? Flag::f_send_messages : Flag()) | ((flags & R::SendOther) ? Flag::f_send_plain : Flag())
| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag()) | ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())

View file

@ -92,7 +92,8 @@ MTPInputReplyTo ReplyToForMTP(
: Flag()) : Flag())
| (quoteEntities.v.isEmpty() | (quoteEntities.v.isEmpty()
? Flag() ? Flag()
: Flag::f_quote_entities)), : Flag::f_quote_entities)
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())),
MTP_int(replyTo.messageId ? replyTo.messageId.msg : 0), MTP_int(replyTo.messageId ? replyTo.messageId.msg : 0),
MTP_int(replyTo.topicRootId), MTP_int(replyTo.topicRootId),
(external (external
@ -103,7 +104,8 @@ MTPInputReplyTo ReplyToForMTP(
MTP_int(replyTo.quoteOffset), MTP_int(replyTo.quoteOffset),
(replyToMonoforumPeerId (replyToMonoforumPeerId
? history->owner().peer(replyToMonoforumPeerId)->input ? history->owner().peer(replyToMonoforumPeerId)->input
: MTPInputPeer())); : MTPInputPeer()),
MTP_int(replyTo.todoItemId));
} else if (history->peer->amMonoforumAdmin() } else if (history->peer->amMonoforumAdmin()
&& replyTo.monoforumPeerId) { && replyTo.monoforumPeerId) {
const auto replyToMonoforumPeer = replyTo.monoforumPeerId const auto replyToMonoforumPeer = replyTo.monoforumPeerId

View file

@ -172,6 +172,19 @@ inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) {
Q_DECLARE_METATYPE(FullMsgId); Q_DECLARE_METATYPE(FullMsgId);
struct MessageHighlightId {
TextWithEntities quote;
int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] bool empty() const {
return quote.empty() && !todoItemId;
}
[[nodiscard]] friend inline bool operator==(
const MessageHighlightId &a,
const MessageHighlightId &b) = default;
};
struct FullReplyTo { struct FullReplyTo {
FullMsgId messageId; FullMsgId messageId;
TextWithEntities quote; TextWithEntities quote;
@ -179,7 +192,11 @@ struct FullReplyTo {
MsgId topicRootId = 0; MsgId topicRootId = 0;
PeerId monoforumPeerId = 0; PeerId monoforumPeerId = 0;
int quoteOffset = 0; int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
}
[[nodiscard]] bool replying() const { [[nodiscard]] bool replying() const {
return messageId || (storyId && storyId.peer); return messageId || (storyId && storyId.peer);
} }

View file

@ -96,17 +96,6 @@ Thread *SavedMessages::activeSubsectionThread() const {
return _activeSubsectionSublist; return _activeSubsectionSublist;
} }
Dialogs::UnreadState SavedMessages::unreadStateWithParentMuted() const {
auto result = _chatsList.unreadState();
if (_owningHistory->muted()) {
result.chatsMuted = result.chats;
result.marksMuted = result.marks;
result.messagesMuted = result.messages;
result.reactionsMuted = result.reactions;
}
return result;
}
SavedMessages::~SavedMessages() { SavedMessages::~SavedMessages() {
clear(); clear();
} }
@ -458,6 +447,9 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {
if (ranges::contains(_lastSublists, not_null(raw))) { if (ranges::contains(_lastSublists, not_null(raw))) {
reorderLastSublists(); reorderLastSublists();
} }
if (_activeSubsectionSublist == raw) {
_activeSubsectionSublist = nullptr;
}
_sublistDestroyed.fire(raw); _sublistDestroyed.fire(raw);
session().changes().sublistUpdated( session().changes().sublistUpdated(

View file

@ -84,8 +84,6 @@ public:
void saveActiveSubsectionThread(not_null<Thread*> thread); void saveActiveSubsectionThread(not_null<Thread*> thread);
Thread *activeSubsectionThread() const; Thread *activeSubsectionThread() const;
[[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const;
[[nodiscard]] rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();
private: private:

View file

@ -217,7 +217,28 @@ void SavedSublist::applyItemRemoved(MsgId id) {
if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { if (const auto chatListItem = _chatListMessage.value_or(nullptr)) {
if (chatListItem->id == id) { if (chatListItem->id == id) {
_chatListMessage = std::nullopt; _chatListMessage = std::nullopt;
crl::on_main(this, [=] {
// We didn't yet update _list here.
if (_chatListMessage.has_value()) {
return;
} else if (_skippedAfter == 0) {
if (!_list.empty()) {
applyMaybeLast(owner().message(
owningHistory()->peer,
_list.front()));
return;
} else if (_skippedBefore == 0) {
setLastServerMessage(nullptr);
updateChatListExistence();
return;
}
}
if (_parent->parentChat()) {
requestChatListMessage(); requestChatListMessage();
} else {
loadAround(0);
}
});
} }
} }
} }
@ -1110,6 +1131,10 @@ void SavedSublist::loadAround(MsgId id) {
_list.clear(); _list.clear();
if (processMessagesIsEmpty(result)) { if (processMessagesIsEmpty(result)) {
_fullCount = _skippedBefore = _skippedAfter = 0; _fullCount = _skippedBefore = _skippedAfter = 0;
if (!_parent->parentChat() && !_chatListMessage) {
setLastServerMessage(nullptr);
updateChatListExistence();
}
} else if (id) { } else if (id) {
Assert(!_list.empty()); Assert(!_list.empty());
if (_list.front() <= id) { if (_list.front() <= id) {
@ -1117,6 +1142,11 @@ void SavedSublist::loadAround(MsgId id) {
} else if (_list.back() >= id) { } else if (_list.back() >= id) {
_skippedBefore = 0; _skippedBefore = 0;
} }
} else if (!_parent->parentChat() && !_chatListMessage) {
Assert(!_list.empty());
applyMaybeLast(owner().message(
owningHistory()->peer,
_list.front()));
} }
checkReadTillEnd(); checkReadTillEnd();
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h" #include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "core/application.h" #include "core/application.h"
#include "data/components/top_peers.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
@ -425,10 +426,7 @@ void Stories::parseAndApply(const MTPPeerStories &stories) {
}; };
if (result.peer->isSelf() if (result.peer->isSelf()
|| (result.peer->isChannel() && result.peer->asChannel()->amIn()) || (result.peer->isChannel() && result.peer->asChannel()->amIn())
|| (result.peer->isUser() || result.peer->isUser()) {
&& (result.peer->asUser()->isBot()
|| result.peer->asUser()->isContact()))
|| result.peer->isServiceUser()) {
const auto hidden = result.peer->hasStoriesHidden(); const auto hidden = result.peer->hasStoriesHidden();
using List = StorySourcesList; using List = StorySourcesList;
add(hidden ? List::Hidden : List::NotHidden); add(hidden ? List::Hidden : List::NotHidden);
@ -1197,7 +1195,11 @@ void Stories::toggleHidden(
bool hidden, bool hidden,
std::shared_ptr<Ui::Show> show) { std::shared_ptr<Ui::Show> show) {
const auto peer = _owner->peer(peerId); const auto peer = _owner->peer(peerId);
const auto justRemove = peer->isServiceUser() && hidden; const auto byHints = peer->isUser()
&& !peer->asUser()->isBot()
&& !peer->asUser()->isContact()
&& !peer->asUser()->isServiceUser();
const auto justRemove = (byHints || peer->isServiceUser()) && hidden;
if (peer->hasStoriesHidden() != hidden) { if (peer->hasStoriesHidden() != hidden) {
if (!justRemove) { if (!justRemove) {
peer->setStoriesHidden(hidden); peer->setStoriesHidden(hidden);
@ -1206,6 +1208,9 @@ void Stories::toggleHidden(
peer->input, peer->input,
MTP_bool(hidden) MTP_bool(hidden)
)).send(); )).send();
if (byHints) {
peer->session().topPeers().remove(peer);
}
} }
const auto name = peer->shortName(); const auto name = peer->shortName();

View file

@ -244,6 +244,17 @@ dialogsEmptyLabel: FlatLabel(defaultFlatLabel) {
align: align(top); align: align(top);
textFg: windowSubTextFg; textFg: windowSubTextFg;
} }
dialogEmptyButton: RoundButton(defaultActiveButton) {
}
dialogEmptyButtonSkip: 12px;
dialogEmptyButtonLabel: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(boxFontSize semibold);
}
minWidth: 32px;
align: align(top);
textFg: windowFg;
}
dialogsMenuToggle: IconButton { dialogsMenuToggle: IconButton {
width: 40px; width: 40px;

View file

@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/text/text_options.h" #include "ui/text/text_options.h"
#include "ui/dynamic_thumbnails.h" #include "ui/dynamic_thumbnails.h"
#include "ui/vertical_list.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
@ -58,8 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/options.h" #include "base/options.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "lottie/lottie_icon.h" #include "lottie/lottie_icon.h"
#include "mainwindow.h" #include "settings/settings_common.h"
#include "mainwidget.h"
#include "storage/storage_account.h" #include "storage/storage_account.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -80,6 +80,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_filters.h" #include "api/api_chat_filters.h"
#include "base/qt/qt_common_adapters.h" #include "base/qt/qt_common_adapters.h"
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_chat.h" // popupMenuExpandedSeparator
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_color_indices.h" #include "styles/style_color_indices.h"
@ -3081,6 +3082,11 @@ void InnerWidget::clearSelection() {
} }
void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) { void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) {
const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)
|| (_searchState.tab == ChatSearchTab::PublicPosts);
if (!globalSearch && _searchState.inChat) {
return;
}
const auto all = session().settings().supportAllSearchResults(); const auto all = session().settings().supportAllSearchResults();
const auto text = all ? "Only one from chat" : "Show all messages"; const auto text = all ? "Only one from chat" : "Show all messages";
menu->addAction(text, [=] { menu->addAction(text, [=] {
@ -3091,9 +3097,11 @@ void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) {
void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) { void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) {
const auto folder = session().data().folderLoaded(Data::Folder::kId); const auto folder = session().data().folderLoaded(Data::Folder::kId);
const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)
|| (_searchState.tab == ChatSearchTab::PublicPosts);
if (!folder if (!folder
|| !folder->chatsList()->fullSize().current() || !folder->chatsList()->fullSize().current()
|| _searchState.inChat) { || (!globalSearch && _searchState.inChat)) {
return; return;
} }
const auto skip = session().settings().skipArchiveInSearch(); const auto skip = session().settings().skipArchiveInSearch();
@ -3263,16 +3271,13 @@ void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) {
refresh(); refresh();
}); });
Menu::FillSponsored( Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu), Ui::Menu::CreateAddActionCallback(_menu),
_controller->uiShow(), _controller->uiShow(),
Menu::SponsoredPhrases::Search, Menu::SponsoredPhrases::Search,
session().sponsoredMessages().lookupDetails(entry->sponsored->data), session().sponsoredMessages().lookupDetails(entry->sponsored->data),
session().sponsoredMessages().createReportCallback( session().sponsoredMessages().createReportCallback(
entry->sponsored->data.randomId, entry->sponsored->data.randomId,
remove), remove));
false,
false);
QObject::connect(_menu.get(), &QObject::destroyed, [=] { QObject::connect(_menu.get(), &QObject::destroyed, [=] {
if (_peerSearchMenu >= 0 if (_peerSearchMenu >= 0
&& _peerSearchMenu < _peerSearchResults.size()) { && _peerSearchMenu < _peerSearchResults.size()) {
@ -3811,7 +3816,7 @@ void InnerWidget::itemRemoved(not_null<const HistoryItem*> item) {
} }
bool InnerWidget::uniqueSearchResults() const { bool InnerWidget::uniqueSearchResults() const {
return _controller->uniqueChatsInSearchResults(); return _controller->uniqueChatsInSearchResults(_searchState);
} }
bool InnerWidget::hasHistoryInResults(not_null<History*> history) const { bool InnerWidget::hasHistoryInResults(not_null<History*> history) const {
@ -3869,7 +3874,8 @@ void InnerWidget::searchReceived(
? _searchState.inChat ? _searchState.inChat
: Key(_openedForum->history()); : Key(_openedForum->history());
if (inject if (inject
&& (!_searchState.inChat && (globalSearch
|| !_searchState.inChat
|| inject->history() == _searchState.inChat.history())) { || inject->history() == _searchState.inChat.history())) {
Assert(_searchResults.empty()); Assert(_searchResults.empty());
Assert(!toPreview); Assert(!toPreview);
@ -4082,9 +4088,18 @@ void InnerWidget::refreshEmpty() {
if (state == EmptyState::None) { if (state == EmptyState::None) {
_emptyState = state; _emptyState = state;
_empty.destroy(); _empty.destroy();
_emptyList.destroy();
_emptyButton.destroy();
return; return;
} else if (_emptyState == state) { } else if (_emptyState == state) {
_empty->setVisible(_state == WidgetState::Default); _empty->setVisible(_state == WidgetState::Default);
if (_emptyList) {
_emptyList->setVisible(_state == WidgetState::Default);
_empty->setVisible(!_emptyList->isVisible());
}
if (_emptyButton) {
_emptyButton->setVisible(_state == WidgetState::Default);
}
return; return;
} }
_emptyState = state; _emptyState = state;
@ -4115,7 +4130,6 @@ void InnerWidget::refreshEmpty() {
return result; return result;
}); });
_empty.create(this, std::move(full), st::dialogsEmptyLabel); _empty.create(this, std::move(full), st::dialogsEmptyLabel);
resizeEmpty();
_empty->overrideLinkClickHandler([=] { _empty->overrideLinkClickHandler([=] {
if (_emptyState == EmptyState::NoContacts) { if (_emptyState == EmptyState::NoContacts) {
_controller->showAddContact(); _controller->showAddContact();
@ -4127,6 +4141,58 @@ void InnerWidget::refreshEmpty() {
} }
}); });
_empty->setVisible(_state == WidgetState::Default); _empty->setVisible(_state == WidgetState::Default);
if (state == EmptyState::NoContacts) {
const auto isListVisible = _state == WidgetState::Default;
_emptyList.create(this);
_emptyList->setVisible(isListVisible);
auto icon = ::Settings::CreateLottieIcon(
_emptyList,
{
.name = u"no_chats"_q,
.sizeOverride = Size(st::changePhoneIconSize),
});
_emptyList->add(
object_ptr<Ui::CenterWrap<>>(_emptyList, std::move(icon.widget)));
Ui::AddSkip(_emptyList);
_emptyList->add(
object_ptr<Ui::FlatLabel>(
_emptyList,
tr::lng_no_conversations(),
st::dialogEmptyButtonLabel));
if (_state == WidgetState::Default) {
icon.animate(anim::repeat::once);
}
_emptyButton.create(
this,
tr::lng_no_conversations_button(),
st::dialogEmptyButton);
_emptyButton->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
_emptyButton->setVisible(isListVisible);
_emptyButton->setClickedCallback([=, window = _controller] {
window->show(PrepareContactsBox(window));
});
geometryValue() | rpl::start_with_next([=](const QRect &r) {
const auto top = r.height()
- _emptyButton->height()
- st::dialogEmptyButtonSkip;
_emptyButton->moveToLeft(st::dialogEmptyButtonSkip, top);
}, _emptyButton->lifetime());
geometryValue() | rpl::start_with_next([=](const QRect &r) {
const auto bottom = _emptyButton
? (_emptyButton->height() + st::dialogEmptyButtonSkip)
: 0;
_emptyList->moveToLeft(
0,
((r.height() - bottom) - _emptyList->height()) / 2);
}, _emptyList->lifetime());
_empty->setVisible(!_emptyList->isVisible());
}
resizeEmpty();
} }
void InnerWidget::resizeEmpty() { void InnerWidget::resizeEmpty() {
@ -4135,6 +4201,13 @@ void InnerWidget::resizeEmpty() {
_empty->resizeToWidth(width() - 2 * skip); _empty->resizeToWidth(width() - 2 * skip);
_empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2);
} }
if (_emptyList) {
_emptyList->resizeToWidth(width());
}
if (_emptyButton) {
const auto skip = st::dialogEmptyButtonSkip;
_emptyButton->resizeToWidth(width() - 2 * skip);
}
if (_searchEmpty) { if (_searchEmpty) {
_searchEmpty->resizeToWidth(width()); _searchEmpty->resizeToWidth(width());
_searchEmpty->move(0, searchedOffset()); _searchEmpty->move(0, searchedOffset());

View file

@ -43,6 +43,8 @@ namespace Ui {
class IconButton; class IconButton;
class PopupMenu; class PopupMenu;
class FlatLabel; class FlatLabel;
class VerticalLayout;
class RoundButton;
struct ScrollToRequest; struct ScrollToRequest;
namespace Controls { namespace Controls {
enum class QuickDialogAction; enum class QuickDialogAction;
@ -619,6 +621,8 @@ private:
object_ptr<SearchEmpty> _searchEmpty = { nullptr }; object_ptr<SearchEmpty> _searchEmpty = { nullptr };
SearchState _searchEmptyState; SearchState _searchEmptyState;
object_ptr<Ui::FlatLabel> _empty = { nullptr }; object_ptr<Ui::FlatLabel> _empty = { nullptr };
object_ptr<Ui::VerticalLayout> _emptyList = { nullptr };
object_ptr<Ui::RoundButton> _emptyButton = { nullptr };
Ui::DraggingScrollManager _draggingScroll; Ui::DraggingScrollManager _draggingScroll;

View file

@ -120,6 +120,10 @@ void MainList::unreadStateChanged(
const auto notify = !useClouded || wasState.known; const auto notify = !useClouded || wasState.known;
const auto notifier = unreadStateChangeNotifier(notify); const auto notifier = unreadStateChangeNotifier(notify);
_unreadState += nowState - wasState; _unreadState += nowState - wasState;
if (_unreadState.chatsMuted > _unreadState.chats
|| _unreadState.messagesMuted > _unreadState.messages) {
[[maybe_unused]] int a = 0;
}
if (updateCloudUnread) { if (updateCloudUnread) {
// Assert(nowState.known); // Assert(nowState.known);
_cloudUnreadState += nowState - wasState; _cloudUnreadState += nowState - wasState;
@ -145,6 +149,10 @@ void MainList::unreadEntryChanged(
} else { } else {
_unreadState -= state; _unreadState -= state;
} }
if (_unreadState.chatsMuted > _unreadState.chats
|| _unreadState.messagesMuted > _unreadState.messages) {
[[maybe_unused]] int a = 0;
}
if (updateCloudUnread) { if (updateCloudUnread) {
if (added) { if (added) {
_cloudUnreadState += state; _cloudUnreadState += state;

View file

@ -903,10 +903,7 @@ void Widget::chosenRow(const ChosenRow &row) {
} else if (const auto topic = row.key.topic()) { } else if (const auto topic = row.key.topic()) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow( controller()->showInNewWindow(
Window::SeparateId(topic), Window::SeparateId(topic),
@ -973,15 +970,12 @@ void Widget::chosenRow(const ChosenRow &row) {
return; return;
} else if (history) { } else if (history) {
const auto peer = history->peer; const auto peer = history->peer;
const auto showAtMsgId = controller()->uniqueChatsInSearchResults() const auto showAtMsgId = controller()->uniqueChatsInSearchResults(
? ShowAtUnreadMsgId _searchState
: row.message.fullId.msg; ) ? ShowAtUnreadMsgId : row.message.fullId.msg;
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow(peer, showAtMsgId); controller()->showInNewWindow(peer, showAtMsgId);
} else { } else {

View file

@ -1167,8 +1167,15 @@ Chat ParseChat(const MTPChat &data) {
result.colorIndex = (color && color->data().vcolor()) result.colorIndex = (color && color->data().vcolor())
? color->data().vcolor()->v ? color->data().vcolor()->v
: PeerColorIndex(result.bareId); : PeerColorIndex(result.bareId);
result.isMonoforum = data.is_monoforum();
result.isBroadcast = data.is_broadcast(); result.isBroadcast = data.is_broadcast();
result.isSupergroup = data.is_megagroup(); result.isSupergroup = data.is_megagroup();
result.hasMonoforumAdminRights = data.is_broadcast()
&& (data.is_creator()
|| (data.vadmin_rights()
&& data.vadmin_rights()->data().is_manage_direct_messages()));
result.monoforumLinkId
= data.vlinked_monoforum_id().value_or_empty();
result.title = ParseString(data.vtitle()); result.title = ParseString(data.vtitle());
if (const auto username = data.vusername()) { if (const auto username = data.vusername()) {
result.username = ParseString(*username); result.username = ParseString(*username);
@ -1188,15 +1195,6 @@ Chat ParseChat(const MTPChat &data) {
return result; return result;
} }
std::map<PeerId, Chat> ParseChatsList(const MTPVector<MTPChat> &data) {
auto result = std::map<PeerId, Chat>();
for (const auto &chat : data.v) {
auto parsed = ParseChat(chat);
result.emplace(parsed.id(), std::move(parsed));
}
return result;
}
Utf8String ContactInfo::name() const { Utf8String ContactInfo::name() const {
return firstName.isEmpty() return firstName.isEmpty()
? (lastName.isEmpty() ? (lastName.isEmpty()
@ -1273,6 +1271,20 @@ std::map<PeerId, Peer> ParsePeersLists(
auto parsed = ParseChat(chat); auto parsed = ParseChat(chat);
result.emplace(parsed.id(), Peer{ std::move(parsed) }); result.emplace(parsed.id(), Peer{ std::move(parsed) });
} }
for (auto &[peerId, parsed] : result) {
if (const auto chat = std::get_if<Chat>(&parsed.data)) {
if (chat->isMonoforum) {
const auto i = result.find(
PeerId(ChannelId(chat->monoforumLinkId)));
if (i != end(result)) {
chat->isMonoforumAdmin
= i->second.chat()->hasMonoforumAdminRights;
chat->isMonoforumOfPublicBroadcast
= !i->second.chat()->username.isEmpty();
}
}
}
}
return result; return result;
} }
@ -2191,7 +2203,13 @@ const DialogInfo *DialogsInfo::item(int index) const {
DialogInfo::Type DialogTypeFromChat(const Chat &chat) { DialogInfo::Type DialogTypeFromChat(const Chat &chat) {
using Type = DialogInfo::Type; using Type = DialogInfo::Type;
return chat.username.isEmpty() return (chat.isMonoforum && !chat.isMonoforumAdmin)
? Type::Personal
: (chat.isMonoforumAdmin && chat.isMonoforumOfPublicBroadcast)
? Type::PublicSupergroup
: chat.isMonoforumAdmin
? Type::PrivateSupergroup
: chat.username.isEmpty()
? (chat.isBroadcast ? (chat.isBroadcast
? Type::PrivateChannel ? Type::PrivateChannel
: chat.isSupergroup : chat.isSupergroup
@ -2252,6 +2270,11 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) {
info.migratedToChannelId = peer.chat() info.migratedToChannelId = peer.chat()
? peer.chat()->migratedToChannelId ? peer.chat()->migratedToChannelId
: 0; : 0;
info.isMonoforum = peer.chat()
&& peer.chat()->isMonoforum;
info.monoforumBroadcastInput = peer.chat()
? peer.chat()->monoforumBroadcastInput
: MTPInputPeer(MTP_inputPeerEmpty());
} }
info.topMessageId = fields.vtop_message().v; info.topMessageId = fields.vtop_message().v;
const auto messageIt = messages.find(MessageId{ const auto messageIt = messages.find(MessageId{
@ -2290,6 +2313,10 @@ DialogInfo DialogInfoFromChat(const Chat &data) {
result.topMessageId = 0; result.topMessageId = 0;
result.type = DialogTypeFromChat(data); result.type = DialogTypeFromChat(data);
result.migratedToChannelId = data.migratedToChannelId; result.migratedToChannelId = data.migratedToChannelId;
result.isMonoforum = data.isMonoforum;
if (data.isMonoforumAdmin) {
result.monoforumBroadcastInput = data.monoforumBroadcastInput;
}
return result; return result;
} }
@ -2424,7 +2451,8 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) {
} }
Unexpected("Type in ApiWrap::onlyMyMessages."); Unexpected("Type in ApiWrap::onlyMyMessages.");
}(); }();
dialog.onlyMyMessages = ((settings.fullChats & setting) != setting); dialog.onlyMyMessages = (dialog.type != DialogType::Personal)
&& ((settings.fullChats & setting) != setting);
ranges::sort(dialog.splits); ranges::sort(dialog.splits);
} }

View file

@ -319,14 +319,19 @@ struct Chat {
Utf8String title; Utf8String title;
Utf8String username; Utf8String username;
uint8 colorIndex = 0; uint8 colorIndex = 0;
bool isMonoforum = false;
bool isBroadcast = false; bool isBroadcast = false;
bool isSupergroup = false; bool isSupergroup = false;
bool isMonoforumAdmin = false;
bool hasMonoforumAdminRights = false;
bool isMonoforumOfPublicBroadcast = false;
BareId monoforumLinkId = 0;
MTPInputPeer input = MTP_inputPeerEmpty(); MTPInputPeer input = MTP_inputPeerEmpty();
MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();
}; };
Chat ParseChat(const MTPChat &data); Chat ParseChat(const MTPChat &data);
std::map<PeerId, Chat> ParseChatsList(const MTPVector<MTPChat> &data);
struct Peer { struct Peer {
PeerId id() const; PeerId id() const;
@ -952,12 +957,15 @@ struct DialogInfo {
MTPInputPeer migratedFromInput = MTP_inputPeerEmpty(); MTPInputPeer migratedFromInput = MTP_inputPeerEmpty();
ChannelId migratedToChannelId = 0; ChannelId migratedToChannelId = 0;
MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();
// User messages splits which contained that dialog. // User messages splits which contained that dialog.
std::vector<int> splits; std::vector<int> splits;
// Filled after the whole dialogs list is accumulated. // Filled after the whole dialogs list is accumulated.
bool onlyMyMessages = false; bool onlyMyMessages = false;
bool isLeftChannel = false; bool isLeftChannel = false;
bool isMonoforum = false;
QString relativePath; QString relativePath;
// Filled when requesting dialog messages. // Filled when requesting dialog messages.

View file

@ -1370,7 +1370,7 @@ void ApiWrap::appendSinglePeerDialogs(Data::DialogsInfo &&info) {
if (isSupergroupType(info.type) && !migratedRequestId) { if (isSupergroupType(info.type) && !migratedRequestId) {
migratedRequestId = requestSinglePeerMigrated(info); migratedRequestId = requestSinglePeerMigrated(info);
continue; continue;
} else if (isChannelType(info.type)) { } else if (isChannelType(info.type) || info.isMonoforum) {
continue; continue;
} }
for (auto i = last; i != 0; --i) { for (auto i = last; i != 0; --i) {
@ -1642,6 +1642,9 @@ void ApiWrap::requestChatMessages(
const auto realPeerInput = (splitIndex >= 0) const auto realPeerInput = (splitIndex >= 0)
? _chatProcess->info.input ? _chatProcess->info.input
: _chatProcess->info.migratedFromInput; : _chatProcess->info.migratedFromInput;
const auto outgoingInput = _chatProcess->info.isMonoforum
? _chatProcess->info.monoforumBroadcastInput
: MTP_inputPeerSelf();
const auto realSplitIndex = (splitIndex >= 0) const auto realSplitIndex = (splitIndex >= 0)
? splitIndex ? splitIndex
: (splitsCount + splitIndex); : (splitsCount + splitIndex);
@ -1650,7 +1653,7 @@ void ApiWrap::requestChatMessages(
MTP_flags(MTPmessages_Search::Flag::f_from_id), MTP_flags(MTPmessages_Search::Flag::f_from_id),
realPeerInput, realPeerInput,
MTP_string(), // query MTP_string(), // query
MTP_inputPeerSelf(), outgoingInput,
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id

View file

@ -111,7 +111,8 @@ std::optional<MTPMessageReplyHeader> PrepareLogReply(
MTP_int(topId), MTP_int(topId),
MTPstring(), // quote_text MTPstring(), // quote_text
MTPVector<MTPMessageEntity>(), // quote_entities MTPVector<MTPMessageEntity>(), // quote_entities
MTPint()); // quote_offset MTPint(), // quote_offset
MTPint()); // todo_item_id
} }
} }
return {}; return {};

View file

@ -2380,7 +2380,7 @@ Dialogs::UnreadState History::chatListUnreadState() const {
return AdjustedForumUnreadState(forum->topicsList()->unreadState()); return AdjustedForumUnreadState(forum->topicsList()->unreadState());
} else if (const auto monoforum = peer->monoforum()) { } else if (const auto monoforum = peer->monoforum()) {
return AdjustedForumUnreadState( return AdjustedForumUnreadState(
monoforum->unreadStateWithParentMuted()); withMyMuted(monoforum->chatsList()->unreadState()));;
} }
return computeUnreadState(); return computeUnreadState();
} }
@ -2395,7 +2395,7 @@ Dialogs::BadgesState History::chatListBadgesState() const {
} else if (const auto monoforum = peer->monoforum()) { } else if (const auto monoforum = peer->monoforum()) {
return adjustBadgesStateByFolder( return adjustBadgesStateByFolder(
Dialogs::BadgesForUnread( Dialogs::BadgesForUnread(
monoforum->unreadStateWithParentMuted(), withMyMuted(monoforum->chatsList()->unreadState()),
Dialogs::CountInBadge::Chats, Dialogs::CountInBadge::Chats,
Dialogs::IncludeInBadge::All)); Dialogs::IncludeInBadge::All));
} }
@ -2429,8 +2429,8 @@ Dialogs::UnreadState History::computeUnreadState() const {
result.mentions = unreadMentions().has() ? 1 : 0; result.mentions = unreadMentions().has() ? 1 : 0;
const auto peer = this->peer.get(); const auto peer = this->peer.get();
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
const auto hideReactions = (peer->isChannel() && !peer->isMegagroup() && !settings.hideChannelReactions) const auto hideReactions = (peer->isChannel() && !peer->isMegagroup() && !settings.showChannelReactions)
|| (peer->isMegagroup() && !settings.hideGroupReactions); || (peer->isMegagroup() && !settings.showGroupReactions);
result.reactions = hideReactions ? 0 : (unreadReactions().has() ? 1 : 0); result.reactions = hideReactions ? 0 : (unreadReactions().has() ? 1 : 0);
result.messagesMuted = muted ? result.messages : 0; result.messagesMuted = muted ? result.messages : 0;
result.chatsMuted = muted ? result.chats : 0; result.chatsMuted = muted ? result.chats : 0;
@ -2440,6 +2440,16 @@ Dialogs::UnreadState History::computeUnreadState() const {
return result; return result;
} }
Dialogs::UnreadState History::withMyMuted(Dialogs::UnreadState state) const {
if (muted()) {
state.chatsMuted = state.chats;
state.marksMuted = state.marks;
state.messagesMuted = state.messages;
state.reactionsMuted = state.reactions;
}
return state;
}
void History::allowChatListMessageResolve() { void History::allowChatListMessageResolve() {
if (_flags & Flag::ResolveChatListMessage) { if (_flags & Flag::ResolveChatListMessage) {
return; return;
@ -3368,7 +3378,8 @@ bool History::isForum() const {
void History::monoforumChanged(Data::SavedMessages *old) { void History::monoforumChanged(Data::SavedMessages *old) {
if (inChatList()) { if (inChatList()) {
notifyUnreadStateChange(old notifyUnreadStateChange(old
? AdjustedForumUnreadState(old->chatsList()->unreadState()) ? AdjustedForumUnreadState(
withMyMuted(old->chatsList()->unreadState()))
: computeUnreadState()); : computeUnreadState());
} }
@ -3378,9 +3389,9 @@ void History::monoforumChanged(Data::SavedMessages *old) {
monoforum->chatsList()->unreadStateChanges( monoforum->chatsList()->unreadStateChanges(
) | rpl::filter([=] { ) | rpl::filter([=] {
return (_flags & Flag::IsMonoforumAdmin) && inChatList(); return (_flags & Flag::IsMonoforumAdmin) && inChatList();
}) | rpl::map( }) | rpl::map([=](const Dialogs::UnreadState &was) {
AdjustedForumUnreadState return AdjustedForumUnreadState(withMyMuted(was));
) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { }) | rpl::start_with_next([=](const Dialogs::UnreadState &old) {
notifyUnreadStateChange(old); notifyUnreadStateChange(old);
}, monoforum->lifetime()); }, monoforum->lifetime());

View file

@ -602,6 +602,8 @@ private:
[[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder( [[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder(
Dialogs::BadgesState state) const; Dialogs::BadgesState state) const;
[[nodiscard]] Dialogs::UnreadState computeUnreadState() const; [[nodiscard]] Dialogs::UnreadState computeUnreadState() const;
[[nodiscard]] Dialogs::UnreadState withMyMuted(
Dialogs::UnreadState state) const;
void setFolderPointer(Data::Folder *folder); void setFolderPointer(Data::Folder *folder);
void hasUnreadMentionChanged(bool has) override; void hasUnreadMentionChanged(bool has) override;

View file

@ -674,10 +674,10 @@ void HistoryInner::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
}); });
if (!selected.text.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}; };
@ -1221,8 +1221,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
if (markingAsViewed && item->hasUnwatchedEffect()) { if (markingAsViewed && item->hasUnwatchedEffect()) {
const auto peer = item->history()->peer; const auto peer = item->history()->peer;
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
const auto hide = (!settings.hideChannelReactions && peer->isChannel() && !peer->isMegagroup()) || const auto hide = (!settings.showChannelReactions && peer->isChannel() && !peer->isMegagroup()) ||
(!settings.hideGroupReactions && peer->isMegagroup()); (!settings.showGroupReactions && peer->isMegagroup());
if (!hide) { if (!hide) {
startEffects.emplace(view); startEffects.emplace(view);
} else { } else {
@ -2409,6 +2409,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto linkUserpicPeerId = (link && _dragStateUserpic) const auto linkUserpicPeerId = (link && _dragStateUserpic)
? link->property(kPeerLinkPeerIdProperty).toULongLong() ? link->property(kPeerLinkPeerIdProperty).toULongLong()
: 0; : 0;
const auto todoListTaskId = link
? link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto session = &this->session(); const auto session = &this->session();
_whoReactedMenuLifetime.destroy(); _whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty() if (!clickedReaction.empty()
@ -2777,20 +2780,21 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto selected = selectedQuote(item); const auto selected = selectedQuote(item);
auto text = (selected auto text = (selected
? tr::lng_context_quote_and_reply ? tr::lng_context_quote_and_reply
: todoListTaskId
? tr::lng_context_reply_to_task
: tr::lng_context_reply_msg)( : tr::lng_context_reply_msg)(
tr::now, tr::now,
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
const auto replyToItem = selected.item ? selected.item : item; const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId(); const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
const auto quoteOffset = selected.offset;
_menu->addAction(std::move(text), [=] { _menu->addAction(std::move(text), [=] {
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = itemId, .messageId = itemId,
.quote = quote, .quote = selected.highlight.quote,
.quoteOffset = quoteOffset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = todoListTaskId,
}); });
if (!quote.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}, &st::menuIconReply); }, &st::menuIconReply);
@ -2809,7 +2813,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
Window::PeerMenuAddTodoListTasks(_controller, item); Window::PeerMenuAddTodoListTasks(_controller, item);
} }
}), }),
&st::menuIconCreateTodoList); &st::menuIconAdd);
}; };
const auto lnkPhoto = link const auto lnkPhoto = link
? reinterpret_cast<PhotoData*>( ? reinterpret_cast<PhotoData*>(
@ -2955,11 +2959,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
: nullptr; : nullptr;
if (sponsored) { if (sponsored) {
Menu::FillSponsored( Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu), Ui::Menu::CreateAddActionCallback(_menu),
controller->uiShow(), controller->uiShow(),
sponsored->fullId(), sponsored->fullId());
false);
} }
if (isUponSelected > 0) { if (isUponSelected > 0) {
addReplyAction(item); addReplyAction(item);

View file

@ -964,12 +964,26 @@ void HistoryItem::updateServiceDependent(bool force) {
} }
if (!dependent->lnk) { if (!dependent->lnk) {
auto todoItemId = 0;
if (const auto done = Get<HistoryServiceTodoCompletions>()) {
const auto &items = !done->completed.empty()
? done->completed
: done->incompleted;
if (items.size() == 1) {
todoItemId = items.front();
}
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
if (append->list.size() == 1) {
todoItemId = append->list.front().id;
}
}
dependent->lnk = JumpToMessageClickHandler( dependent->lnk = JumpToMessageClickHandler(
(dependent->peerId (dependent->peerId
? _history->owner().peer(dependent->peerId) ? _history->owner().peer(dependent->peerId)
: _history->peer), : _history->peer),
dependent->msgId, dependent->msgId,
fullId()); fullId(),
{ .todoItemId = todoItemId });
} }
auto gotDependencyItem = false; auto gotDependencyItem = false;
if (!dependent->msg) { if (!dependent->msg) {
@ -1316,14 +1330,8 @@ void HistoryItem::setCommentsItemId(FullMsgId id) {
void HistoryItem::setServiceText(PreparedServiceText &&prepared) { void HistoryItem::setServiceText(PreparedServiceText &&prepared) {
auto text = std::move(prepared.text); auto text = std::move(prepared.text);
const auto &settings = AyuSettings::getInstance();
if (date() > 0) { if (date() > 0) {
const auto timeString = QString(" (%1)").arg(QLocale().toString( const auto timeString = QString(" (%1)").arg(formatMessageTime(base::unixtime::parse(_date).time()));
base::unixtime::parse(_date),
settings.showMessageSeconds
? QLocale::system().timeFormat(QLocale::LongFormat).remove(" t")
: QLocale::system().timeFormat(QLocale::ShortFormat)
));
if (!text.text.isEmpty() && !text.text.contains(timeString)) { if (!text.text.isEmpty() && !text.text.contains(timeString)) {
text = text.append(timeString); text = text.append(timeString);
} }
@ -1858,7 +1866,10 @@ bool HistoryItem::isAyuNoForwards() const {
} }
bool HistoryItem::canLookupMessageAuthor() const { bool HistoryItem::canLookupMessageAuthor() const {
return isRegular() && _history->amMonoforumAdmin() && _from->isChannel(); return isRegular()
&& !isService()
&& _history->amMonoforumAdmin()
&& _from->isChannel();
} }
bool HistoryItem::skipNotification() const { bool HistoryItem::skipNotification() const {
@ -4392,6 +4403,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) {
: replyTo.monoforumPeerId : replyTo.monoforumPeerId
? replyTo.monoforumPeerId ? replyTo.monoforumPeerId
: PeerId(); : PeerId();
config.reply.todoItemId = replyTo.todoItemId;
const auto replyToTop = replyTo.topicRootId const auto replyToTop = replyTo.topicRootId
? replyTo.topicRootId ? replyTo.topicRootId
: LookupReplyToTop(_history, to); : LookupReplyToTop(_history, to);

View file

@ -390,6 +390,7 @@ ReplyFields ReplyFieldsFromMTP(
= data.vreply_to_top_id().value_or(result.messageId.bare); = data.vreply_to_top_id().value_or(result.messageId.bare);
result.topicPost = data.is_forum_topic() ? 1 : 0; result.topicPost = data.is_forum_topic() ? 1 : 0;
} }
result.todoItemId = data.vtodo_item_id().value_or_empty();
if (const auto header = data.vreply_from()) { if (const auto header = data.vreply_from()) {
const auto &data = header->data(); const auto &data = header->data();
result.externalPostAuthor result.externalPostAuthor
@ -704,7 +705,8 @@ auto ReplyMarkupClickHandler::getUrlButton() const
-> const HistoryMessageMarkupButton* { -> const HistoryMessageMarkupButton* {
if (const auto button = getButton()) { if (const auto button = getButton()) {
using Type = HistoryMessageMarkupButton::Type; using Type = HistoryMessageMarkupButton::Type;
if (button->type == Type::Url || button->type == Type::Auth || button->type == Type::Callback) { if (button->type == Type::Url || button->type == Type::Auth || button->type == Type::Callback ||
button->type == Type::WebView || button->type == Type::SimpleWebView) {
return button; return button;
} }
} }

View file

@ -277,6 +277,7 @@ struct ReplyFields {
MsgId messageId = 0; MsgId messageId = 0;
MsgId topMessageId = 0; MsgId topMessageId = 0;
StoryId storyId = 0; StoryId storyId = 0;
int todoItemId = 0;
uint32 quoteOffset : 30 = 0; uint32 quoteOffset : 30 = 0;
uint32 manualQuote : 1 = 0; uint32 manualQuote : 1 = 0;
uint32 topicPost : 1 = 0; uint32 topicPost : 1 = 0;

View file

@ -722,22 +722,19 @@ 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, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return JumpToMessageClickHandler( return JumpToMessageClickHandler(
item->history()->peer, item->history()->peer,
item->id, item->id,
returnToId, returnToId,
std::move(highlightPart), std::move(highlight));
highlightPartOffsetHint);
} }
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId, FullMsgId returnToId,
TextWithEntities highlightPart, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=] {
const auto separate = Core::App().separateWindowFor(peer); const auto separate = Core::App().separateWindowFor(peer);
const auto controller = separate const auto controller = separate
@ -747,8 +744,7 @@ ClickHandlerPtr JumpToMessageClickHandler(
auto params = Window::SectionShow{ auto params = Window::SectionShow{
Window::SectionShow::Way::Forward Window::SectionShow::Way::Forward
}; };
params.highlightPart = highlightPart; params.highlight = highlight;
params.highlightPartOffsetHint = highlightPartOffsetHint;
params.origin = Window::SectionShow::OriginMessage{ params.origin = Window::SectionShow::OriginMessage{
returnToId returnToId
}; };
@ -910,7 +906,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
| Flag::f_quote_offset)) | Flag::f_quote_offset))
| (quoteEntities.v.empty() | (quoteEntities.v.empty()
? Flag() ? Flag()
: Flag::f_quote_entities)), : Flag::f_quote_entities)
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())),
MTP_int(replyTo.messageId.msg), MTP_int(replyTo.messageId.msg),
peerToMTP(externalPeerId), peerToMTP(externalPeerId),
MTPMessageFwdHeader(), // reply_from MTPMessageFwdHeader(), // reply_from
@ -918,7 +915,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
MTP_int(replyToTop), MTP_int(replyToTop),
MTP_string(replyTo.quote.text), MTP_string(replyTo.quote.text),
quoteEntities, quoteEntities,
MTP_int(replyTo.quoteOffset)); MTP_int(replyTo.quoteOffset),
MTP_int(replyTo.todoItemId));
} }
return MTPMessageReplyHeader(); return MTPMessageReplyHeader();
} }
@ -1173,8 +1171,8 @@ void CheckReactionNotificationSchedule(
} }
const auto peer = item->history()->peer; const auto peer = item->history()->peer;
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
if ((peer->isChannel() && !peer->isMegagroup() && !settings.hideChannelReactions) if ((peer->isChannel() && !peer->isMegagroup() && !settings.showChannelReactions)
|| (peer->isMegagroup() && !settings.hideGroupReactions)) { || (peer->isMegagroup() && !settings.showGroupReactions)) {
item->markEffectWatched(); item->markEffectWatched();
return; return;
} }

View file

@ -229,13 +229,11 @@ private:
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( [[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(
not_null<Data::Story*> story); not_null<Data::Story*> story);
ClickHandlerPtr JumpToStoryClickHandler( ClickHandlerPtr JumpToStoryClickHandler(

View file

@ -65,6 +65,7 @@ Ui::ChatPaintHighlight ElementHighlighter::state(
if (item->fullId() == _highlighted.itemId) { if (item->fullId() == _highlighted.itemId) {
auto result = _animation.state(); auto result = _animation.state();
result.range = _highlighted.part; result.range = _highlighted.part;
result.todoItemId = _highlighted.todoListId;
return result; return result;
} }
return {}; return {};
@ -82,19 +83,27 @@ ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
const auto i = ranges::find(group->items, item); const auto i = ranges::find(group->items, item);
if (i != end(group->items)) { if (i != end(group->items)) {
const auto index = int(i - begin(group->items)); const auto index = int(i - begin(group->items));
if (quote.text.empty()) { if (quote.highlight.empty()) {
return { leaderId, AddGroupItemSelection({}, index) }; return { leaderId, AddGroupItemSelection({}, index) };
} else if (const auto leaderView = _viewForItem(leader)) { } else if (const auto leaderView = _viewForItem(leader)) {
return { leaderId, leaderView->selectionFromQuote(quote) }; return {
leaderId,
leaderView->selectionFromQuote(quote),
quote.highlight.todoItemId,
};
} }
} }
return { leaderId }; return { leaderId, {}, quote.highlight.todoItemId };
} else if (quote.text.empty()) { } else if (quote.highlight.quote.empty()) {
return { item->fullId() }; return { item->fullId(), {}, quote.highlight.todoItemId };
} else if (const auto view = _viewForItem(item)) { } else if (const auto view = _viewForItem(item)) {
return { item->fullId(), view->selectionFromQuote(quote) }; return {
item->fullId(),
view->selectionFromQuote(quote),
quote.highlight.todoItemId,
};
} }
return { item->fullId() }; return { item->fullId(), {}, quote.highlight.todoItemId };
} }
void ElementHighlighter::highlight(Highlight data) { void ElementHighlighter::highlight(Highlight data) {
@ -108,7 +117,7 @@ void ElementHighlighter::highlight(Highlight data) {
} }
} }
_highlighted = data; _highlighted = data;
_animation.start(!data.part.empty() _animation.start((!data.part.empty() || data.todoListId)
&& !IsSubGroupSelection(data.part)); && !IsSubGroupSelection(data.part));
repaintHighlightedItem(view); repaintHighlightedItem(view);

View file

@ -65,6 +65,7 @@ private:
struct Highlight { struct Highlight {
FullMsgId itemId; FullMsgId itemId;
TextSelection part; TextSelection part;
int todoListId = 0;
explicit operator bool() const { explicit operator bool() const {
return itemId.operator bool(); return itemId.operator bool();

View file

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_drafts.h" #include "data/data_drafts.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_web_page.h" #include "data/data_web_page.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_photo.h" #include "data/data_photo.h"
@ -1575,12 +1576,21 @@ int HistoryWidget::itemTopForHighlight(
const auto heightLeft = (visibleAreaHeight - viewHeight); const auto heightLeft = (visibleAreaHeight - viewHeight);
if (heightLeft >= 0) { if (heightLeft >= 0) {
return std::max(itemTop - (heightLeft / 2), 0); return std::max(itemTop - (heightLeft / 2), 0);
} else if (const auto sel = itemHighlight(item).range } else if (const auto highlight = itemHighlight(item)
; !sel.empty() && !IsSubGroupSelection(sel)) { ; (!highlight.range.empty() || highlight.todoItemId)
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height; const auto single = st::messageTextStyle.font->height;
const auto begin = HistoryView::FindViewY(view, sel.from) - single; const auto todoy = sel.empty()
const auto end = HistoryView::FindViewY(view, sel.to, begin + single) ? HistoryView::FindViewTaskY(view, highlight.todoItemId)
+ 2 * single; : 0;
const auto begin = sel.empty()
? (todoy - 4 * single)
: HistoryView::FindViewY(view, sel.from) - single;
const auto end = sel.empty()
? (todoy + 4 * single)
: (HistoryView::FindViewY(view, sel.to, begin + single)
+ 2 * single);
auto result = itemTop; auto result = itemTop;
if (end > visibleAreaHeight) { if (end > visibleAreaHeight) {
result = std::max(result, itemTop + end - visibleAreaHeight); result = std::max(result, itemTop + end - visibleAreaHeight);
@ -5797,8 +5807,7 @@ void HistoryWidget::switchToSearch(QString query) {
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,
@ -6907,8 +6916,7 @@ int HistoryWidget::countInitialScrollTop() {
enqueueMessageHighlight({ enqueueMessageHighlight({
item, item,
base::take(_showAtMsgParams.highlightPart), base::take(_showAtMsgParams.highlight),
base::take(_showAtMsgParams.highlightPartOffsetHint),
}); });
const auto result = itemTopForHighlight(view); const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result); createUnreadBarIfBelowVisibleArea(result);
@ -7670,12 +7678,7 @@ void HistoryWidget::editDraftOptions() {
void HistoryWidget::jumpToReply(FullReplyTo to) { void HistoryWidget::jumpToReply(FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }
@ -8718,7 +8721,7 @@ void HistoryWidget::clearFieldText(
void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage(FullReplyTo id) {
if (const auto item = session().data().message(id.messageId)) { if (const auto item = session().data().message(id.messageId)) {
if (CanSendReply(item) && !base::IsCtrlPressed()) { if (CanSendReply(item) && !base::IsCtrlPressed()) {
replyToMessage(item, id.quote, id.quoteOffset); replyToMessage(item, id);
} else if (item->allowsForward()) { } else if (item->allowsForward()) {
const auto show = controller()->uiShow(); const auto show = controller()->uiShow();
HistoryView::Controls::ShowReplyToChatBox(show, id); HistoryView::Controls::ShowReplyToChatBox(show, id);
@ -8731,16 +8734,12 @@ void HistoryWidget::replyToMessage(FullReplyTo id) {
void HistoryWidget::replyToMessage( void HistoryWidget::replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote, FullReplyTo fields) {
int quoteOffset) {
if (isJoinChannel()) { if (isJoinChannel()) {
return; return;
} }
_processingReplyTo = { fields.messageId = item->fullId();
.messageId = item->fullId(), _processingReplyTo = fields;
.quote = quote,
.quoteOffset = quoteOffset,
};
_processingReplyItem = item; _processingReplyItem = item;
processReply(); processReply();
} }
@ -9429,11 +9428,24 @@ void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
.session = &session(), .session = &session(),
.repaint = [=] { updateField(); }, .repaint = [=] { updateField(); },
}); });
const auto text = [&] {
const auto media = _replyTo.todoItemId ? item->media() : nullptr;
if (const auto todolist = media ? media->todolist() : nullptr) {
const auto i = ranges::find(
todolist->items,
_replyTo.todoItemId,
&TodoListItem::id);
if (i != end(todolist->items)) {
return i->text;
}
}
return (_editMsgId || _replyTo.quote.empty())
? item->inReplyText()
: _replyTo.quote;
}();
_replyEditMsgText.setMarkedText( _replyEditMsgText.setMarkedText(
st::defaultTextStyle, st::defaultTextStyle,
((_editMsgId || _replyTo.quote.empty()) text,
? item->inReplyText()
: _replyTo.quote),
Ui::DialogTextOptions(), Ui::DialogTextOptions(),
context); context);
if (fieldOrDisabledShown() || isRecording()) { if (fieldOrDisabledShown() || isRecording()) {
@ -9519,10 +9531,9 @@ void HistoryWidget::updateReplyToName() {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo;
const auto replyToQuote = _replyTo && !_replyTo.quote.empty();
_replyToName.setMarkedText( _replyToName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, to, replyToQuote), HistoryView::Reply::ComposePreviewName(_history, to, _replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} }

View file

@ -205,8 +205,7 @@ public:
void replyToMessage(FullReplyTo id); void replyToMessage(FullReplyTo id);
void replyToMessage( void replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote = {}, FullReplyTo fields = {});
int quoteOffset = 0);
void editMessage( void editMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const TextSelection &selection); const TextSelection &selection);

View file

@ -92,6 +92,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
// AyuGram includes
#include "ayu/ayu_settings.h"
namespace HistoryView { namespace HistoryView {
namespace { namespace {
@ -119,6 +123,13 @@ using SetHistoryArgs = ComposeControls::SetHistoryArgs;
using VoiceRecordBar = Controls::VoiceRecordBar; using VoiceRecordBar = Controls::VoiceRecordBar;
using ForwardPanel = Controls::ForwardPanel; using ForwardPanel = Controls::ForwardPanel;
#define SWITCH_BUTTON(button, show_v) \
if (show_v) { \
(button)->show(); \
} else { \
(button)->hide(); \
}
} // namespace } // namespace
const ChatHelpers::PauseReason kDefaultPanelsLevel const ChatHelpers::PauseReason kDefaultPanelsLevel
@ -492,10 +503,9 @@ void FieldHeader::setShownMessage(HistoryItem *item) {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto replyTo = _replyTo.current(); const auto replyTo = _replyTo.current();
const auto quote = replyTo && !replyTo.quote.empty();
_shownMessageName.setMarkedText( _shownMessageName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, item, quote), HistoryView::Reply::ComposePreviewName(_history, item, replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} else { } else {
@ -1595,6 +1605,14 @@ void ComposeControls::init() {
updateAttachBotsMenu(); updateAttachBotsMenu();
}, _wrap->lifetime()); }, _wrap->lifetime());
AyuSettings::get_historyUpdateReactive() | rpl::start_with_next([=]
{
updateSendButtonType();
updateControlsVisibility();
updateControlsGeometry(_wrap->size());
orderControls();
}, _wrap->lifetime());
orderControls(); orderControls();
} }
@ -1604,6 +1622,11 @@ void ComposeControls::orderControls() {
} }
bool ComposeControls::showRecordButton() const { bool ComposeControls::showRecordButton() const {
const auto &settings = AyuSettings::getInstance();
if (!settings.showMicrophoneButtonInMessageField) {
return false;
}
return (_recordAvailability != Webrtc::RecordAvailability::None) return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState() && !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar() && !_voiceRecordBar->isRecordingByAnotherBar()
@ -2686,17 +2709,19 @@ void ComposeControls::updateControlsGeometry(QSize size) {
// (_attachToggle|_replaceMedia) (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel // (_attachToggle|_replaceMedia) (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel
// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_botCommandStart) _tabbedSelectorToggle _send // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_botCommandStart) _tabbedSelectorToggle _send
const auto &settings = AyuSettings::getInstance();
const auto fieldWidth = size.width() const auto fieldWidth = size.width()
- _attachToggle->width() - (settings.showAttachButtonInMessageField ? _attachToggle->width() : 0)
- (_sendAs ? _sendAs->width() : 0) - (_sendAs ? _sendAs->width() : 0)
- st::historySendRight - st::historySendRight
- _send->width() - _send->width()
- _tabbedSelectorToggle->width() - (settings.showEmojiButtonInMessageField ? _tabbedSelectorToggle->width() : 0)
- (_likeShown ? _like->width() : 0) - (_likeShown ? _like->width() : 0)
- (_botCommandShown ? _botCommandStart->width() : 0) - (_botCommandShown && settings.showCommandsButtonInMessageField ? _botCommandStart->width() : 0)
- (_silent ? _silent->width() : 0) - (_silent ? _silent->width() : 0)
- (_scheduled ? _scheduled->width() : 0) - (_scheduled ? _scheduled->width() : 0)
- (_ttlInfo ? _ttlInfo->width() : 0); - (_ttlInfo && settings.showAutoDeleteButtonInMessageField ? _ttlInfo->width() : 0);
{ {
const auto oldFieldHeight = _field->height(); const auto oldFieldHeight = _field->height();
_field->resizeToWidth(fieldWidth); _field->resizeToWidth(fieldWidth);
@ -2713,8 +2738,10 @@ void ComposeControls::updateControlsGeometry(QSize size) {
if (_replaceMedia) { if (_replaceMedia) {
_replaceMedia->moveToLeft(left, buttonsTop); _replaceMedia->moveToLeft(left, buttonsTop);
} }
if (settings.showAttachButtonInMessageField) {
_attachToggle->moveToLeft(left, buttonsTop); _attachToggle->moveToLeft(left, buttonsTop);
left += _attachToggle->width(); left += _attachToggle->width();
}
if (_sendAs) { if (_sendAs) {
_sendAs->moveToLeft(left, buttonsTop); _sendAs->moveToLeft(left, buttonsTop);
left += _sendAs->width(); left += _sendAs->width();
@ -2731,8 +2758,10 @@ void ComposeControls::updateControlsGeometry(QSize size) {
auto right = st::historySendRight; auto right = st::historySendRight;
_send->moveToRight(right, buttonsTop); _send->moveToRight(right, buttonsTop);
right += _send->width(); right += _send->width();
if (settings.showEmojiButtonInMessageField) {
_tabbedSelectorToggle->moveToRight(right, buttonsTop); _tabbedSelectorToggle->moveToRight(right, buttonsTop);
right += _tabbedSelectorToggle->width(); right += _tabbedSelectorToggle->width();
}
if (_like) { if (_like) {
using Type = Controls::WriteRestrictionType; using Type = Controls::WriteRestrictionType;
if (_writeRestriction.current().type == Type::PremiumRequired) { if (_writeRestriction.current().type == Type::PremiumRequired) {
@ -2746,7 +2775,7 @@ void ComposeControls::updateControlsGeometry(QSize size) {
} }
if (_botCommandStart) { if (_botCommandStart) {
_botCommandStart->moveToRight(right, buttonsTop); _botCommandStart->moveToRight(right, buttonsTop);
if (_botCommandShown) { if (_botCommandShown && settings.showCommandsButtonInMessageField) {
right += _botCommandStart->width(); right += _botCommandStart->width();
} }
} }
@ -2758,7 +2787,7 @@ void ComposeControls::updateControlsGeometry(QSize size) {
_scheduled->moveToRight(right, buttonsTop); _scheduled->moveToRight(right, buttonsTop);
right += _scheduled->width(); right += _scheduled->width();
} }
if (_ttlInfo) { if (_ttlInfo && settings.showAutoDeleteButtonInMessageField) {
_ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop); _ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop);
} }
@ -2769,14 +2798,16 @@ void ComposeControls::updateControlsGeometry(QSize size) {
} }
void ComposeControls::updateControlsVisibility() { void ComposeControls::updateControlsVisibility() {
const auto &settings = AyuSettings::getInstance();
if (_botCommandStart) { if (_botCommandStart) {
_botCommandStart->setVisible(_botCommandShown); SWITCH_BUTTON(_botCommandStart, _botCommandShown && settings.showCommandsButtonInMessageField);
} }
if (_like) { if (_like) {
_like->setVisible(_likeShown); _like->setVisible(_likeShown);
} }
if (_ttlInfo) { if (_ttlInfo) {
_ttlInfo->show(); SWITCH_BUTTON(_ttlInfo, settings.showAutoDeleteButtonInMessageField);
} }
if (_sendAs) { if (_sendAs) {
_sendAs->show(); _sendAs->show();
@ -2785,11 +2816,12 @@ void ComposeControls::updateControlsVisibility() {
_replaceMedia->show(); _replaceMedia->show();
_attachToggle->hide(); _attachToggle->hide();
} else { } else {
_attachToggle->show(); SWITCH_BUTTON(_attachToggle, settings.showAttachButtonInMessageField);
} }
if (_scheduled) { if (_scheduled) {
_scheduled->setVisible(!isEditingMessage()); _scheduled->setVisible(!isEditingMessage());
} }
SWITCH_BUTTON(_tabbedSelectorToggle, settings.showEmojiButtonInMessageField);
} }
bool ComposeControls::updateLikeShown() { bool ComposeControls::updateLikeShown() {

View file

@ -718,8 +718,7 @@ void DraftOptionsBox(
state->link = args.usedLink; state->link = args.usedLink;
state->quote = SelectedQuote{ state->quote = SelectedQuote{
replyItem, replyItem,
draft.reply.quote, { draft.reply.quote, draft.reply.quoteOffset },
draft.reply.quoteOffset,
}; };
state->forward = std::move(args.forward); state->forward = std::move(args.forward);
state->webpage = draft.webpage; state->webpage = draft.webpage;
@ -783,7 +782,7 @@ void DraftOptionsBox(
box->setTitle(hasLink box->setTitle(hasLink
? tr::lng_link_options_header() ? tr::lng_link_options_header()
: hasReply : hasReply
? (state->quote.current().text.empty() ? (state->quote.current().highlight.quote.empty()
? tr::lng_reply_options_header() ? tr::lng_reply_options_header()
: tr::lng_reply_options_quote()) : tr::lng_reply_options_quote())
: (forwardCount == 1) : (forwardCount == 1)
@ -807,10 +806,12 @@ void DraftOptionsBox(
auto result = draft.reply; auto result = draft.reply;
if (const auto current = state->quote.current()) { if (const auto current = state->quote.current()) {
result.messageId = current.item->fullId(); result.messageId = current.item->fullId();
result.quote = current.text; result.quote = current.highlight.quote;
result.quoteOffset = current.offset; result.quoteOffset = current.highlight.quoteOffset;
// result.todoItemId = current.highlight.todoItemId;
} else { } else {
result.quote = {}; result.quote = {};
// result.todoItemId = 0;
} }
return result; return result;
}; };
@ -1112,7 +1113,7 @@ void DraftOptionsBox(
state->quote.value(), state->quote.value(),
state->shown.value() state->shown.value()
) | rpl::map([=](const SelectedQuote &quote, Section shown) { ) | rpl::map([=](const SelectedQuote &quote, Section shown) {
return (quote.text.empty() || shown != Section::Reply) return (quote.highlight.quote.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();

View file

@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes // AyuGram includes
#include "ayu/ayu_settings.h" #include "ayu/ayu_settings.h"
#include "ayu/features/messageshot/message_shot.h" #include "ayu/features/messageshot/message_shot.h"
#include "ayu/utils/telegram_helpers.h"
#include "core/ui_integration.h" #include "core/ui_integration.h"
#include "styles/style_ayu_icons.h" #include "styles/style_ayu_icons.h"
@ -428,12 +429,7 @@ void BottomInfo::layoutDateText() {
: QString(); : QString();
const auto author = _data.author; const auto author = _data.author;
const auto prefix = !author.isEmpty() ? u", "_q : QString(); const auto prefix = !author.isEmpty() ? u", "_q : QString();
const auto date = edited + QLocale().toString( const auto date = edited + formatMessageTime(_data.date.time());
_data.date.time(),
settings.showMessageSeconds
? QLocale::system().timeFormat(QLocale::LongFormat).remove(" t")
: QLocale::system().timeFormat(QLocale::ShortFormat)
);
const auto afterAuthor = prefix + date; const auto afterAuthor = prefix + date;
const auto afterAuthorWidth = st::msgDateFont->width(afterAuthor); const auto afterAuthorWidth = st::msgDateFont->width(afterAuthor);
const auto authorWidth = st::msgDateFont->width(author); const auto authorWidth = st::msgDateFont->width(author);
@ -494,12 +490,9 @@ void BottomInfo::layoutDateText() {
const auto author = _data.author; const auto author = _data.author;
const auto prefix = !author.isEmpty() ? (_data.flags & Data::Flag::Edited ? u" "_q : u", "_q) : QString(); const auto prefix = !author.isEmpty() ? (_data.flags & Data::Flag::Edited ? u" "_q : u", "_q) : QString();
const auto date = TextWithEntities{}.append(edited).append(QLocale().toString( const auto date = TextWithEntities{}
_data.date.time(), .append(edited)
settings.showMessageSeconds .append(formatMessageTime(_data.date.time()));
? QLocale::system().timeFormat(QLocale::LongFormat).remove(" t")
: QLocale::system().timeFormat(QLocale::ShortFormat)
));
const auto afterAuthor = TextWithEntities{}.append(prefix).append(date); const auto afterAuthor = TextWithEntities{}.append(prefix).append(date);
const auto afterAuthorWidth = st::msgDateFont->width(afterAuthor.text); const auto afterAuthorWidth = st::msgDateFont->width(afterAuthor.text);

View file

@ -123,12 +123,10 @@ rpl::producer<Ui::MessageBarContent> RootViewContent(
ChatMemento::ChatMemento( ChatMemento::ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId, MsgId highlightId,
const TextWithEntities &highlightPart, MessageHighlightId highlight)
int highlightPartOffsetHint)
: _id(id) : _id(id)
, _highlightPart(highlightPart) , _highlightId(highlightId)
, _highlightPartOffsetHint(highlightPartOffsetHint) , _highlight(std::move(highlight)) {
, _highlightId(highlightId) {
if (highlightId || _id.sublist) { if (highlightId || _id.sublist) {
_list.setAroundPosition({ _list.setAroundPosition({
.fullId = FullMsgId(_id.history->peer->id, highlightId), .fullId = FullMsgId(_id.history->peer->id, highlightId),
@ -884,12 +882,7 @@ void ChatWidget::setupComposeControls() {
_composeControls->jumpToItemRequests( _composeControls->jumpToItemRequests(
) | rpl::start_with_next([=](FullReplyTo to) { ) | rpl::start_with_next([=](FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
}, lifetime()); }, lifetime());
@ -1047,8 +1040,9 @@ void ChatWidget::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_inner->replyToMessageRequestNotify({ _inner->replyToMessageRequestNotify({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = selected.highlight.todoItemId,
}); });
}; };
return result; return result;
@ -2648,8 +2642,7 @@ void ChatWidget::restoreState(not_null<ChatMemento*> memento) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::Forward, Window::SectionShow::Way::Forward,
anim::type::instant); anim::type::instant);
params.highlightPart = memento->highlightPart(); params.highlight = memento->highlight();
params.highlightPartOffsetHint = memento->highlightPartOffsetHint();
showAtPosition(Data::MessagePosition{ showAtPosition(Data::MessagePosition{
.fullId = FullMsgId(_peer->id, highlight), .fullId = FullMsgId(_peer->id, highlight),
.date = TimeId(0), .date = TimeId(0),
@ -3452,8 +3445,7 @@ bool ChatWidget::searchInChatEmbedded(
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,

View file

@ -461,8 +461,7 @@ public:
explicit ChatMemento( explicit ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId = 0, MsgId highlightId = 0,
const TextWithEntities &highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
struct Comments { struct Comments {
}; };
@ -511,20 +510,16 @@ public:
[[nodiscard]] MsgId highlightId() const { [[nodiscard]] MsgId highlightId() const {
return _highlightId; return _highlightId;
} }
[[nodiscard]] const TextWithEntities &highlightPart() const { [[nodiscard]] const MessageHighlightId &highlight() const {
return _highlightPart; return _highlight;
}
[[nodiscard]] int highlightPartOffsetHint() const {
return _highlightPartOffsetHint;
} }
private: private:
void setupTopicViewer(); void setupTopicViewer();
ChatViewId _id; ChatViewId _id;
const TextWithEntities _highlightPart;
const int _highlightPartOffsetHint = 0;
const MsgId _highlightId = 0; const MsgId _highlightId = 0;
const MessageHighlightId _highlight;
ListMemento _list; ListMemento _list;
std::shared_ptr<Data::RepliesList> _replies; std::shared_ptr<Data::RepliesList> _replies;
QVector<FullMsgId> _replyReturns; QVector<FullMsgId> _replyReturns;

View file

@ -644,8 +644,13 @@ bool AddReplyToMessageAction(
return false; return false;
} }
const auto todoListTaskId = request.link
? request.link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto &quote = request.quote; const auto &quote = request.quote;
auto text = (quote.text.empty() auto text = (todoListTaskId
? tr::lng_context_reply_to_task
: quote.highlight.quote.empty()
? tr::lng_context_reply_msg ? tr::lng_context_reply_msg
: tr::lng_context_quote_and_reply)( : tr::lng_context_quote_and_reply)(
tr::now, tr::now,
@ -653,8 +658,9 @@ bool AddReplyToMessageAction(
menu->addAction(std::move(text), [=, itemId = item->fullId()] { menu->addAction(std::move(text), [=, itemId = item->fullId()] {
list->replyToMessageRequestNotify({ list->replyToMessageRequestNotify({
.messageId = itemId, .messageId = itemId,
.quote = quote.text, .quote = quote.highlight.quote,
.quoteOffset = quote.offset, .quoteOffset = quote.highlight.quoteOffset,
.todoItemId = todoListTaskId,
}, base::IsCtrlPressed()); }, base::IsCtrlPressed());
}, &st::menuIconReply); }, &st::menuIconReply);
return true; return true;
@ -680,7 +686,7 @@ bool AddTodoListAction(
if (const auto item = controller->session().data().message(itemId)) { if (const auto item = controller->session().data().message(itemId)) {
Window::PeerMenuAddTodoListTasks(controller, item); Window::PeerMenuAddTodoListTasks(controller, item);
} }
}, &st::menuIconCreateTodoList); }, &st::menuIconAdd);
return true; return true;
} }

View file

@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_saved_sublist.h" #include "data/data_saved_sublist.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_forum.h" #include "data/data_forum.h"
#include "data/data_forum_topic.h" #include "data/data_forum_topic.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
@ -1360,9 +1361,18 @@ void Element::validateText() {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
if (!done->completed.empty() && !done->incompleted.empty()) { if (!done->completed.empty() && !done->incompleted.empty()) {
const auto todoItemId = (done->incompleted.size() == 1)
? done->incompleted.front()
: 0;
setServicePreMessage( setServicePreMessage(
item->composeTodoIncompleted(done), item->composeTodoIncompleted(done),
done->lnk); JumpToMessageClickHandler(
(done->peerId
? history()->owner().peer(done->peerId)
: history()->peer),
done->msgId,
item->fullId(),
{ .todoItemId = todoItemId }));
} else { } else {
setServicePreMessage({}); setServicePreMessage({});
} }
@ -2205,7 +2215,7 @@ SelectedQuote Element::FindSelectedQuote(
++i; ++i;
} }
} }
return { item, result, modified.from, overflown }; return { item, { result, modified.from }, overflown };
} }
TextSelection Element::FindSelectionFromQuote( TextSelection Element::FindSelectionFromQuote(
@ -2213,17 +2223,18 @@ TextSelection Element::FindSelectionFromQuote(
const SelectedQuote &quote) { const SelectedQuote &quote) {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { const auto &rich = quote.highlight.quote;
if (rich.empty()) {
return {}; return {};
} }
const auto &original = quote.item->originalText(); const auto &original = quote.item->originalText();
if (quote.offset == kSearchQueryOffsetHint) { if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {
return ApplyModificationsFrom( return ApplyModificationsFrom(
FindSearchQueryHighlight(original.text, quote.text.text), FindSearchQueryHighlight(original.text, rich.text),
text); text);
} }
const auto length = int(original.text.size()); const auto length = int(original.text.size());
const auto qlength = int(quote.text.text.size()); const auto qlength = int(rich.text.size());
const auto checkAt = [&](int offset) { const auto checkAt = [&](int offset) {
return TextSelection{ return TextSelection{
uint16(offset), uint16(offset),
@ -2234,7 +2245,7 @@ TextSelection Element::FindSelectionFromQuote(
if (offset > length - qlength) { if (offset > length - qlength) {
return TextSelection(); return TextSelection();
} }
const auto i = original.text.indexOf(quote.text.text, offset); const auto i = original.text.indexOf(rich.text, offset);
return (i >= 0) ? checkAt(i) : TextSelection(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findOneBefore = [&](int offset) { const auto findOneBefore = [&](int offset) {
@ -2243,7 +2254,7 @@ TextSelection Element::FindSelectionFromQuote(
} }
const auto end = std::min(offset + qlength - 1, length); const auto end = std::min(offset + qlength - 1, length);
const auto from = end - length - 1; const auto from = end - length - 1;
const auto i = original.text.lastIndexOf(quote.text.text, from); const auto i = original.text.lastIndexOf(rich.text, from);
return (i >= 0) ? checkAt(i) : TextSelection(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findAfter = [&](int offset) { const auto findAfter = [&](int offset) {
@ -2281,7 +2292,7 @@ TextSelection Element::FindSelectionFromQuote(
? before ? before
: after; : after;
}; };
auto result = findTwoWays(quote.offset); auto result = findTwoWays(quote.highlight.quoteOffset);
if (result.empty()) { if (result.empty()) {
return {}; return {};
} }
@ -2468,6 +2479,70 @@ int FindViewY(not_null<Element*> view, uint16 symbol, int yfrom) {
return origin.y() + (yfrom + ytill) / 2; return origin.y() + (yfrom + ytill) / 2;
} }
int FindViewTaskY(not_null<Element*> view, int taskId, int yfrom) {
auto request = HistoryView::StateRequest();
request.flags = Ui::Text::StateRequest::Flag::LookupLink;
const auto single = st::messageTextStyle.font->height;
const auto inner = view->innerGeometry();
const auto origin = inner.topLeft();
const auto top = 0;
const auto bottom = view->height();
if (origin.y() < top
|| origin.y() + inner.height() > bottom
|| inner.height() <= 0) {
return yfrom;
}
const auto media = view->data()->media();
const auto todolist = media ? media->todolist() : nullptr;
if (!todolist) {
return yfrom;
}
const auto &items = todolist->items;
const auto indexOf = [&](int id) -> int {
return ranges::find(items, id, &TodoListItem::id) - begin(items);
};
const auto index = indexOf(taskId);
const auto count = int(items.size());
if (index == count) {
return yfrom;
}
yfrom = std::max(yfrom - origin.y(), 0);
auto ytill = inner.height() - 1;
const auto middle = (yfrom + ytill) / 2;
const auto fory = [&](int y) {
const auto state = view->textState(origin + QPoint(0, y), request);
const auto &link = state.link;
const auto id = link
? link->property(kTodoListItemIdProperty).toInt()
: -1;
const auto index = (id >= 0) ? indexOf(id) : int(items.size());
return (index < count) ? index : (y < middle) ? -1 : count;
};
auto indexfrom = fory(yfrom);
auto indextill = fory(ytill);
if ((yfrom >= ytill) || (indexfrom >= index)) {
return origin.y() + yfrom;
} else if (indextill <= index) {
return origin.y() + ytill;
}
while (ytill - yfrom >= 2 * single) {
const auto middle = (yfrom + ytill) / 2;
const auto found = fory(middle);
if (found == index
|| indexfrom > found
|| indextill < found) {
return origin.y() + middle;
} else if (found < index) {
yfrom = middle;
indexfrom = found;
} else {
ytill = middle;
indextill = found;
}
}
return origin.y() + (yfrom + ytill) / 2;
}
Window::SessionController *ExtractController(const ClickContext &context) { Window::SessionController *ExtractController(const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) { if (const auto controller = my.sessionWindow.get()) {

View file

@ -357,12 +357,11 @@ struct TopicButton {
struct SelectedQuote { struct SelectedQuote {
HistoryItem *item = nullptr; HistoryItem *item = nullptr;
TextWithEntities text; MessageHighlightId highlight;
int offset = 0;
bool overflown = false; bool overflown = false;
explicit operator bool() const { explicit operator bool() const {
return item && !text.empty(); return item && !highlight.quote.empty();
} }
friend inline bool operator==(SelectedQuote, SelectedQuote) = default; friend inline bool operator==(SelectedQuote, SelectedQuote) = default;
}; };
@ -748,6 +747,11 @@ private:
uint16 symbol, uint16 symbol,
int yfrom = 0); int yfrom = 0);
[[nodiscard]] int FindViewTaskY(
not_null<Element*> view,
int taskId,
int yfrom = 0);
[[nodiscard]] Window::SessionController *ExtractController( [[nodiscard]] Window::SessionController *ExtractController(
const ClickContext &context); const ClickContext &context);

View file

@ -720,12 +720,21 @@ std::optional<int> ListWidget::scrollTopForView(
const auto heightLeft = (available - height); const auto heightLeft = (available - height);
if (heightLeft >= 0) { if (heightLeft >= 0) {
return std::max(top - (heightLeft / 2), 0); return std::max(top - (heightLeft / 2), 0);
} else if (const auto sel = _highlighter.state(view->data()).range } else if (const auto highlight = _highlighter.state(view->data())
; !sel.empty() && !IsSubGroupSelection(sel)) { ; (!highlight.range.empty() || highlight.todoItemId)
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height; const auto single = st::messageTextStyle.font->height;
const auto begin = HistoryView::FindViewY(view, sel.from) - single; const auto todoy = sel.empty()
const auto end = HistoryView::FindViewY(view, sel.to, begin + single) ? HistoryView::FindViewTaskY(view, highlight.todoItemId)
+ 2 * single; : 0;
const auto begin = sel.empty()
? (todoy - 4 * single)
: HistoryView::FindViewY(view, sel.from) - single;
const auto end = sel.empty()
? (todoy + 4 * single)
: (HistoryView::FindViewY(view, sel.to, begin + single)
+ 2 * single);
auto result = top; auto result = top;
if (end > available) { if (end > available) {
result = std::max(result, top + end - available); result = std::max(result, top + end - available);
@ -822,10 +831,9 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
void ListWidget::highlightMessage( void ListWidget::highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight) {
int partOffsetHint) {
if (const auto view = viewForItem(itemId)) { if (const auto view = viewForItem(itemId)) {
_highlighter.highlight({ view->data(), part, partOffsetHint }); _highlighter.highlight({ view->data(), highlight });
} }
} }
@ -903,11 +911,8 @@ bool ListWidget::showAtPositionNow(
} }
if (position != Data::MaxMessagePosition if (position != Data::MaxMessagePosition
&& position != Data::UnreadMessagePosition) { && position != Data::UnreadMessagePosition) {
const auto hasHighlight = !params.highlightPart.empty(); const auto hasHighlight = !params.highlight.empty();
highlightMessage( highlightMessage(position.fullId, params.highlight);
position.fullId,
params.highlightPart,
params.highlightPartOffsetHint);
if (hasHighlight) { if (hasHighlight) {
// We may want to scroll to a different part of the message. // We may want to scroll to a different part of the message.
scrollTop = scrollTopForPosition(position); scrollTop = scrollTopForPosition(position);

View file

@ -314,8 +314,7 @@ public:
bool isBelowPosition(Data::MessagePosition position) const; bool isBelowPosition(Data::MessagePosition position) const;
void highlightMessage( void highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight);
int partOffsetHint);
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,

View file

@ -490,6 +490,8 @@ void Message::initPaidInformation() {
refreshSuggestedInfo(item, suggest, replyData); refreshSuggestedInfo(item, suggest, replyData);
} }
return; return;
} else if (!item->history()->peer->isUser()) {
return;
} }
const auto media = this->media(); const auto media = this->media();
const auto mine = PaidInformation{ const auto mine = PaidInformation{
@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote(
const SelectedQuote &quote) const { const SelectedQuote &quote) const {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { if (quote.highlight.quote.empty()) {
return {}; return {};
} }
const auto item = quote.item; const auto item = quote.item;

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_story.h" #include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/view/history_view_item_preview.h" #include "history/view/history_view_item_preview.h"
#include "history/history.h" #include "history/history.h"
@ -42,6 +43,85 @@ namespace {
constexpr auto kNonExpandedLinesLimit = 5; constexpr auto kNonExpandedLinesLimit = 5;
[[nodiscard]] QImage MakeTaskImage() {
const auto diameter = st::normalFont->ascent;
const auto line = st::historyPollRadio.thickness;
const auto size = 2 * line + diameter;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
PainterHighQualityEnabler hq(p);
p.setOpacity(st::historyPollRadioOpacity);
const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(
QMarginsF(line / 2., line / 2., line / 2., line / 2.));
auto pen = QPen(QColor(255, 255, 255));
pen.setWidth(line);
p.setPen(pen);
p.drawEllipse(rect);
p.end();
return result;
}
[[nodiscard]] QImage MakeTaskDoneImage() {
const auto white = QColor(255, 255, 255);
const auto black = QColor(0, 0, 0);
const auto diameter = st::normalFont->ascent;
const auto line = st::historyPollRadio.thickness;
const auto size = 2 * line + diameter;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.fill(black);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
PainterHighQualityEnabler hq(p);
const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(
QMarginsF(line / 2., line / 2., line / 2., line / 2.));
auto pen = QPen(white);
pen.setWidth(line);
p.setPen(pen);
p.setBrush(white);
p.drawEllipse(rect);
const auto &icon = st::historyPollInChoiceRight;
icon.paint(
p,
line + (diameter - icon.width()) / 2,
line + (diameter - icon.height()) / 2,
size,
black);
p.end();
return style::colorizeImage(result, white);
}
[[nodiscard]] TextWithEntities TaskDoneIcon(
not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskDoneImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
[[nodiscard]] TextWithEntities TaskIcon(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
} // namespace } // namespace
void ValidateBackgroundEmoji( void ValidateBackgroundEmoji(
@ -197,6 +277,22 @@ void Reply::update(
const auto item = view->data(); const auto item = view->data();
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto message = data->resolvedMessage.get(); const auto message = data->resolvedMessage.get();
const auto messageMedia = (message && fields.todoItemId)
? message->media()
: nullptr;
const auto messageTodoList = messageMedia
? messageMedia->todolist()
: nullptr;
const auto taskIndex = messageTodoList
? int(ranges::find(
messageTodoList->items,
fields.todoItemId,
&TodoListItem::id) - begin(messageTodoList->items))
: -1;
const auto task = (taskIndex >= 0
&& taskIndex < messageTodoList->items.size())
? &messageTodoList->items[taskIndex]
: nullptr;
const auto story = data->resolvedStory.get(); const auto story = data->resolvedStory.get();
const auto externalMedia = fields.externalMedia.get(); const auto externalMedia = fields.externalMedia.get();
if (!_externalSender) { if (!_externalSender) {
@ -214,7 +310,6 @@ void Reply::update(
_hiddenSenderColorIndexPlusOne = (!_colorPeer && message) _hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
? (message->originalHiddenSenderInfo()->colorIndex + 1) ? (message->originalHiddenSenderInfo()->colorIndex + 1)
: 0; : 0;
const auto hasPreview = (story && story->hasReplyPreview()) const auto hasPreview = (story && story->hasReplyPreview())
|| (message || (message
&& message->media() && message->media()
@ -229,8 +324,13 @@ void Reply::update(
&& !fields.quote.empty(); && !fields.quote.empty();
_hasQuoteIcon = hasQuoteIcon ? 1 : 0; _hasQuoteIcon = hasQuoteIcon ? 1 : 0;
const auto session = &view->history()->session();
const auto text = (!_displaying && data->unavailable()) const auto text = (!_displaying && data->unavailable())
? TextWithEntities() ? TextWithEntities()
: task
? Ui::Text::Colorized(task->completionDate
? TaskDoneIcon(session)
: TaskIcon(session)).append(task->text)
: (message && (fields.quote.empty() || !fields.manualQuote)) : (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText() ? message->inReplyText()
: !fields.quote.empty() : !fields.quote.empty()
@ -288,10 +388,11 @@ void Reply::setLinkFrom(
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto externalChannelId = peerToChannel(fields.externalPeerId); const auto externalChannelId = peerToChannel(fields.externalPeerId);
const auto messageId = fields.messageId; const auto messageId = fields.messageId;
const auto quote = fields.manualQuote const auto highlight = MessageHighlightId{
? fields.quote .quote = fields.manualQuote ? fields.quote : TextWithEntities(),
: TextWithEntities(); .quoteOffset = int(fields.quoteOffset),
const auto quoteOffset = fields.quoteOffset; .todoItemId = fields.todoItemId,
};
const auto returnToId = view->data()->fullId(); const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) { const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
@ -314,8 +415,7 @@ void Reply::setLinkFrom(
channel, channel,
messageId, messageId,
returnToId, returnToId,
quote, highlight
quoteOffset
)->onClick(context); )->onClick(context);
} else { } else {
controller->showPeerInfo(channel); controller->showPeerInfo(channel);
@ -336,7 +436,7 @@ void Reply::setLinkFrom(
const auto message = data->resolvedMessage.get(); const auto message = data->resolvedMessage.get();
const auto story = data->resolvedStory.get(); const auto story = data->resolvedStory.get();
_link = message _link = message
? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset) ? JumpToMessageClickHandler(message, returnToId, highlight)
: story : story
? JumpToStoryClickHandler(story) ? JumpToStoryClickHandler(story)
: (data->external() : (data->external()
@ -873,18 +973,28 @@ TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName( TextWithEntities Reply::ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote) { const FullReplyTo &replyTo) {
const auto sender = [&] { const auto sender = [&] {
if (const auto from = to->displayFrom()) { if (const auto from = to->displayFrom()) {
return not_null(from); return not_null(from);
} }
return to->author(); return to->author();
}(); }();
if (const auto media = replyTo.todoItemId ? to->media() : nullptr) {
if (const auto todolist = media->todolist()) {
return tr::lng_preview_reply_to_task(
tr::now,
lt_title,
todolist->title,
Ui::Text::WithEntities);
}
}
const auto toPeer = to->history()->peer; const auto toPeer = to->history()->peer;
const auto displayAsExternal = (to->history() != history); const auto displayAsExternal = (to->history() != history);
const auto groupNameAdded = displayAsExternal const auto groupNameAdded = displayAsExternal
&& (toPeer != sender) && (toPeer != sender)
&& (toPeer->isChat() || toPeer->isMegagroup()); && (toPeer->isChat() || toPeer->isMegagroup());
const auto quote = replyTo && !replyTo.quote.empty();
const auto shorten = groupNameAdded || quote; const auto shorten = groupNameAdded || quote;
auto nameFull = TextWithEntities(); auto nameFull = TextWithEntities();

View file

@ -110,7 +110,7 @@ public:
[[nodiscard]] static TextWithEntities ComposePreviewName( [[nodiscard]] static TextWithEntities ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote); const FullReplyTo &replyTo);
private: private:
[[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry(

View file

@ -438,12 +438,8 @@ void ScheduledWidget::setupComposeControls() {
if (item->isScheduled() && item->history() == _history) { if (item->isScheduled() && item->history() == _history) {
showAtPosition(item->position()); showAtPosition(item->position());
} else { } else {
JumpToMessageClickHandler( const auto highlight = to.highlight();
item, JumpToMessageClickHandler(item, {}, highlight)->onClick({});
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }
}, lifetime()); }, lifetime());

View file

@ -463,9 +463,6 @@ QSize Service::performCountCurrentSize(int newWidth) {
const auto media = this->media(); const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed(); const auto mediaDisplayed = media && media->isDisplayed();
auto contentWidth = newWidth; auto contentWidth = newWidth;
if (mediaDisplayed && media->hideServiceText()) {
newHeight += media->resizeGetHeight(newWidth) + marginBottom();
} else if (!text().isEmpty()) {
if (delegate()->elementChatMode() == ElementChatMode::Wide) { if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
} }
@ -473,7 +470,9 @@ QSize Service::performCountCurrentSize(int newWidth) {
if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {
contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;
} }
if (mediaDisplayed && media->hideServiceText()) {
newHeight += media->resizeGetHeight(newWidth) + marginBottom();
} else if (!text().isEmpty()) {
auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0); auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0);
newHeight += (contentWidth >= maxWidth()) newHeight += (contentWidth >= maxWidth())
? minHeight() ? minHeight()

View file

@ -432,10 +432,14 @@ void SubsectionTabs::setupSlider(
.session = &session(), .session = &session(),
}), }),
}, paused); }, paused);
slider->setActiveSectionFast(activeIndex);
const auto ignoreActiveScroll = (scrollSavingIndex >= 0);
slider->setActiveSectionFast(activeIndex, ignoreActiveScroll);
_sectionsSlice = _slice; _sectionsSlice = _slice;
if (scrollSavingIndex >= 0) { Assert(slider->sectionsCount() == _slice.size());
if (ignoreActiveScroll) {
Assert(scrollSavingIndex < slider->sectionsCount());
const auto position = scrollSavingShift const auto position = scrollSavingShift
+ slider->lookupSectionPosition(scrollSavingIndex); + slider->lookupSectionPosition(scrollSavingIndex);
if (vertical) { if (vertical) {
@ -702,6 +706,8 @@ void SubsectionTabs::refreshSlice() {
if (_slice != slice) { if (_slice != slice) {
_slice = std::move(slice); _slice = std::move(slice);
_refreshed.fire({}); _refreshed.fire({});
Assert((!_horizontal && !_vertical)
|| (_slice.size() == _sectionsSlice.size()));
} }
}); });
const auto push = [&](not_null<Data::Thread*> thread) { const auto push = [&](not_null<Data::Thread*> thread) {

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler( ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) { const Task &task) {
const auto id = task.id; const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id); toggleCompletion(id);
})); }));
result->setProperty(kTodoListItemIdProperty, id);
return result;
} }
void TodoList::startToggleAnimation(Task &task) { void TodoList::startToggleAnimation(Task &task) {
@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) {
if (i == end(_tasks)) { if (i == end(_tasks)) {
return; return;
} }
const auto selected = (i->completionDate != 0); const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now(); i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) { if (!selected) {
i->setCompletedBy(_parent->history()->session().user()); i->setCompletedBy(_parent->history()->session().user());
} }
const auto parentMedia = _parent->data()->media();
const auto baseList = parentMedia ? parentMedia->todolist() : nullptr;
if (baseList) {
const auto j = ranges::find(baseList->items, id, &TodoListItem::id);
if (j != end(baseList->items)) {
j->completionDate = i->completionDate;
j->completedBy = i->completedBy;
}
history()->owner().updateDependentMessages(_parent->data());
}
startToggleAnimation(*i); startToggleAnimation(*i);
repaint(); repaint();
@ -467,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw, paintw,
width(), width(),
context); context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) { if (was) {
heavy = true; heavy = true;
} else if (!task.userpic.null()) { } else if (!task.userpic.null()) {
@ -561,6 +577,33 @@ int TodoList::paintTask(
return height; return height;
} }
void TodoList::appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const {
if (context.highlight.todoItemId != id
|| context.highlight.collapsion <= 0.) {
return;
}
const auto to = context.highlightInterpolateTo;
const auto toProgress = (1. - context.highlight.collapsion);
if (toProgress >= 1.) {
context.highlightPathCache->addRect(to);
} else if (toProgress <= 0.) {
context.highlightPathCache->addRect(0, top, width(), height);
} else {
const auto lerp = [=](int from, int to) {
return from + (to - from) * toProgress;
};
context.highlightPathCache->addRect(
lerp(0, to.x()),
lerp(top, to.y()),
lerp(width(), to.width()),
lerp(height, to.height()));
}
}
void TodoList::paintRadio( void TodoList::paintRadio(
Painter &p, Painter &p,
const Task &task, const Task &task,

View file

@ -117,6 +117,11 @@ private:
int top, int top,
int paintw, int paintw,
const PaintContext &context) const; const PaintContext &context) const;
void appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const;
void radialAnimationCallback() const; void radialAnimationCallback() const;

View file

@ -835,12 +835,12 @@ InlineListData InlineListDataFromMessage(not_null<Element*> view) {
using Flag = InlineListData::Flag; using Flag = InlineListData::Flag;
const auto item = view->data(); const auto item = view->data();
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
if (!settings.hideChannelReactions if (!settings.showChannelReactions
&& item->history()->peer->isChannel() && item->history()->peer->isChannel()
&& !item->history()->peer->isMegagroup()) { && !item->history()->peer->isMegagroup()) {
return InlineListData(); return InlineListData();
} }
if (!settings.hideGroupReactions if (!settings.showGroupReactions
&& item->history()->peer->isMegagroup()) { && item->history()->peer->isMegagroup()) {
return InlineListData(); return InlineListData();
} }

View file

@ -1189,11 +1189,6 @@ bool AdjustMenuGeometryForSelector(
not_null<Ui::PopupMenu*> menu, not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition, QPoint desiredPosition,
not_null<Selector*> selector) { not_null<Selector*> selector) {
const auto &settings = AyuSettings::getInstance();
if (!AyuUi::needToShowItem(settings.showReactionsPanelInContextMenu)) {
return false;
}
const auto useTransparency = selector->useTransparency(); const auto useTransparency = selector->useTransparency();
const auto extend = useTransparency const auto extend = useTransparency
? st::reactStripExtend ? st::reactStripExtend
@ -1362,6 +1357,12 @@ AttachSelectorResult AttachSelectorToMenu(
return AttachSelectorResult::Skipped; return AttachSelectorResult::Skipped;
} }
const auto peer = item->history()->peer;
if ((peer->isChannel() && !peer->isMegagroup() && !settings.showChannelReactions)
|| (peer->isMegagroup() && !settings.showGroupReactions)) {
return AttachSelectorResult::Skipped;
}
const auto result = AttachSelectorToMenu( const auto result = AttachSelectorToMenu(
menu, menu,
desiredPosition, desiredPosition,

View file

@ -731,8 +731,8 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
manageGroupReactions: IconButton(defaultIconButton) { manageGroupReactions: IconButton(defaultIconButton) {
width: 24px; width: 24px;
height: 36px; height: 36px;
icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }}; icon: icon{{ "menu/add", historyComposeIconFg }};
iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }}; iconOver: icon{{ "menu/add", historyComposeIconFgOver }};
} }
manageGroupReactionsField: InputField(defaultInputField) { manageGroupReactionsField: InputField(defaultInputField) {
textMargins: margins(1px, 12px, 24px, 8px); textMargins: margins(1px, 12px, 24px, 8px);

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_premium.h" #include "api/api_premium.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "boxes/star_gift_box.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_credits.h" #include "data/data_credits.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -380,6 +381,7 @@ void InnerWidget::loadMore() {
_entries.clear(); _entries.clear();
} }
_entries.reserve(_entries.size() + data.vgifts().v.size()); _entries.reserve(_entries.size() + data.vgifts().v.size());
auto hasUnique = false;
for (const auto &gift : data.vgifts().v) { for (const auto &gift : data.vgifts().v) {
if (auto parsed = Api::FromTL(_peer, gift)) { if (auto parsed = Api::FromTL(_peer, gift)) {
auto descriptor = DescriptorForGift(_peer, *parsed); auto descriptor = DescriptorForGift(_peer, *parsed);
@ -387,10 +389,15 @@ void InnerWidget::loadMore() {
.gift = std::move(*parsed), .gift = std::move(*parsed),
.descriptor = std::move(descriptor), .descriptor = std::move(descriptor),
}); });
hasUnique = (parsed->info.unique != nullptr);
} }
} }
refreshButtons(); refreshButtons();
refreshAbout(); refreshAbout();
if (hasUnique) {
Ui::PreloadUniqueGiftResellPrices(&_peer->session());
}
}).fail([=] { }).fail([=] {
_loadMoreRequestId = 0; _loadMoreRequestId = 0;
_allLoaded = true; _allLoaded = true;

View file

@ -949,7 +949,20 @@ QString FormatCountDecimal(int64 number) {
} }
QString FormatExactCountDecimal(float64 number) { QString FormatExactCountDecimal(float64 number) {
return QLocale().toString(number, 'f', QLocale::FloatingPointShortest); const auto locale = QLocale();
if (qFuzzyCompare(number, base::SafeRound(number))) {
return locale.toString(int64(base::SafeRound(number)));
}
// Somehow using QLocale::FloatingPointShortest sometimes produces
// "0.8500000000000001" on some systems / locales,
// so I want to stick to 6 digits max (default third argument value).
auto result = locale.toString(number, 'f');
const auto zero = locale.zeroDigit();
while (result.endsWith(zero)) {
result.chop(1);
}
return result;
} }
ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) { ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) {

View file

@ -1087,3 +1087,34 @@ mediaviewSponsoredButton: RoundButton(defaultActiveButton) {
ripple: universalRippleAnimation; ripple: universalRippleAnimation;
} }
mediaSponsoredSkip: 16px;
mediaSponsoredShift: 16px;
mediaSponsoredPadding: margins(12px, 8px, 8px, 8px);
mediaSponsoredThumb: 48px;
mediaSponsoredCloseTwice: 3px;
mediaSponsoredCloseSmall: 3px;
mediaSponsoredCloseSize: 11px;
mediaSponsoredCloseCorner: 6px;
mediaSponsoredCloseFull: 64px;
mediaSponsoredCloseStroke: 2px;
mediaSponsoredCloseRipple: 36px;
mediaSponsoredCloseDiameter: 24px;
mediaSponsoredCloseFont: font(12px bold);
mediaSponsoredAbout: RoundButton(defaultActiveButton) {
textFg: windowActiveTextFg;
textFgOver: windowActiveTextFg;
textBg: lightButtonBgOver;
textBgOver: lightButtonBgOver;
width: -12px;
height: 18px;
radius: 9px;
textTop: 0px;
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgRipple;
}
}

View file

@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/view/media_view_pip.h" #include "media/view/media_view_pip.h"
#include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_raster.h"
#include "media/view/media_view_overlay_opengl.h" #include "media/view/media_view_overlay_opengl.h"
#include "media/view/media_view_playback_sponsored.h"
#include "media/stories/media_stories_share.h" #include "media/stories/media_stories_share.h"
#include "media/stories/media_stories_view.h" #include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_document.h"
@ -339,6 +340,7 @@ struct OverlayWidget::Streamed {
Streaming::Instance instance; Streaming::Instance instance;
std::unique_ptr<PlaybackControls> controls; std::unique_ptr<PlaybackControls> controls;
std::unique_ptr<PlaybackSponsored> sponsored;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker; std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
bool ready = false; bool ready = false;
@ -1617,7 +1619,11 @@ void OverlayWidget::fillContextMenuActions(
if (const auto window = findWindow()) { if (const auto window = findWindow()) {
const auto show = window->uiShow(); const auto show = window->uiShow();
const auto fullId = _message->fullId(); const auto fullId = _message->fullId();
Menu::FillSponsored(_body, addAction, show, fullId, true); Menu::FillSponsored(
addAction,
show,
fullId,
{ .dark = true, .skipInfo = true });
} }
return; return;
} }
@ -3981,7 +3987,11 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {
&& !_streamed->instance.player().finished())) { && !_streamed->instance.player().finished())) {
startStreamingPlayer(startStreaming); startStreamingPlayer(startStreaming);
} else { } else {
_streamed->ready = _streamed->instance.player().ready(); if (_streamed->instance.player().ready()) {
markStreamedReady();
} else {
_streamed->ready = false;
}
updatePlaybackState(); updatePlaybackState();
} }
return true; return true;
@ -3994,7 +4004,7 @@ void OverlayWidget::startStreamingPlayer(
const auto &player = _streamed->instance.player(); const auto &player = _streamed->instance.player();
if (player.playing()) { if (player.playing()) {
if (!_streamed->withSound) { if (!_streamed->withSound) {
_streamed->ready = true; markStreamedReady();
return; return;
} }
_pip = nullptr; _pip = nullptr;
@ -4012,6 +4022,18 @@ void OverlayWidget::startStreamingPlayer(
restartAtSeekPosition(_streamedPosition); restartAtSeekPosition(_streamedPosition);
} }
void OverlayWidget::markStreamedReady() {
Expects(_streamed != nullptr);
if (_streamed->ready) {
return;
}
_streamed->ready = true;
if (const auto sponsored = _streamed->sponsored.get()) {
sponsored->start();
}
}
void OverlayWidget::initStreamingThumbnail() { void OverlayWidget::initStreamingThumbnail() {
Expects(_photo || _document); Expects(_photo || _document);
@ -4083,7 +4105,7 @@ void OverlayWidget::initStreamingThumbnail() {
} }
void OverlayWidget::streamingReady(Streaming::Information &&info) { void OverlayWidget::streamingReady(Streaming::Information &&info) {
_streamed->ready = true; markStreamedReady();
if (videoShown()) { if (videoShown()) {
applyVideoSize(); applyVideoSize();
_streamedQualityChangeFrame = QImage(); _streamedQualityChangeFrame = QImage();
@ -4105,6 +4127,7 @@ void OverlayWidget::applyVideoSize() {
bool OverlayWidget::createStreamingObjects() { bool OverlayWidget::createStreamingObjects() {
Expects(_photo || _document); Expects(_photo || _document);
Expects(!_streamed);
const auto origin = fileOrigin(); const auto origin = fileOrigin();
const auto callback = [=] { waitingAnimationCallback(); }; const auto callback = [=] { waitingAnimationCallback(); };
@ -4137,6 +4160,18 @@ bool OverlayWidget::createStreamingObjects() {
_body, _body,
static_cast<PlaybackControls::Delegate*>(this)); static_cast<PlaybackControls::Delegate*>(this));
_streamed->controls->show(); _streamed->controls->show();
_streamed->sponsored = PlaybackSponsored::Has(_message)
? std::make_unique<PlaybackSponsored>(
_streamed->controls.get(),
uiShow(),
_message)
: nullptr;
if (const auto sponsored = _streamed->sponsored.get()) {
_layerBg->layerShownValue(
) | rpl::start_with_next([=](bool shown) {
sponsored->setPaused(shown);
}, sponsored->lifetime());
}
refreshClipControllerGeometry(); refreshClipControllerGeometry();
} }
return true; return true;

View file

@ -78,6 +78,7 @@ struct ContentLayout;
namespace Media::View { namespace Media::View {
class PlaybackSponsored;
class GroupThumbs; class GroupThumbs;
class Pip; class Pip;
@ -412,6 +413,7 @@ private:
const StartStreaming &startStreaming = StartStreaming()); const StartStreaming &startStreaming = StartStreaming());
void startStreamingPlayer(const StartStreaming &startStreaming); void startStreamingPlayer(const StartStreaming &startStreaming);
void initStreamingThumbnail(); void initStreamingThumbnail();
void markStreamedReady();
void streamingReady(Streaming::Information &&info); void streamingReady(Streaming::Information &&info);
[[nodiscard]] bool createStreamingObjects(); [[nodiscard]] bool createStreamingObjects();
void handleStreamingUpdate(Streaming::Update &&update); void handleStreamingUpdate(Streaming::Update &&update);

View file

@ -19,14 +19,13 @@ class MediaSlider;
class PopupMenu; class PopupMenu;
} // namespace Ui } // namespace Ui
namespace Media { namespace Media::Player {
namespace Player {
struct TrackState; struct TrackState;
class SettingsButton; class SettingsButton;
class SpeedController; class SpeedController;
} // namespace Player } // namespace Media::Player
namespace View { namespace Media::View {
class PlaybackProgress; class PlaybackProgress;
@ -131,5 +130,4 @@ private:
}; };
} // namespace View } // namespace Media::View
} // namespace Media

View file

@ -0,0 +1,767 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/view/media_view_playback_sponsored.h"
#include "boxes/premium_preview_box.h"
#include "data/components/sponsored_messages.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_sponsored.h"
#include "ui/effects/numbers_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "styles/style_chat.h"
#include "styles/style_media_view.h"
namespace Media::View {
namespace {
constexpr auto kStartDelayMin = crl::time(1000);
constexpr auto kDurationMin = 5 * crl::time(1000);
enum class Action {
Close,
PromotePremium,
Pause,
Unpause,
};
class Close final : public Ui::RippleButton {
public:
Close(
not_null<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
private:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
void paintEvent(QPaintEvent *e) override;
void updateProgress(crl::time now);
rpl::event_stream<Action> _actions;
Ui::NumbersAnimation _countdown;
Ui::Animations::Basic _progress;
base::Timer _noAnimationTimer;
crl::time _allowCloseAt = 0;
crl::time _startedAt = 0;
crl::time _pausedAt = 0;
int _secondsTill = 0;
int _rippleSize = 0;
QPoint _rippleOrigin;
bool _allowClose = false;
};
Close::Close(
not_null<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> allowCloseAt)
: RippleButton(parent, st)
, _countdown(st::mediaSponsoredCloseFont, [=] { update(); })
, _progress([=](crl::time now) { updateProgress(now); })
, _noAnimationTimer([=] { updateProgress(crl::now()); })
, _startedAt(crl::now()) {
resize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull);
const auto size = st::mediaSponsoredCloseRipple;
const auto cut = int(base::SafeRound((width() - size) / 2.));
_rippleSize = std::min(width() - 2 * cut, height() - 2 * cut);
_rippleOrigin = QPoint(
(width() - _rippleSize) / 2,
(height() - _rippleSize) / 2);
std::move(
allowCloseAt
) | rpl::start_with_next([=](crl::time at) {
const auto now = crl::now();
if (!at) {
updateProgress(now);
_pausedAt = now;
_progress.stop();
} else {
if (_pausedAt) {
_startedAt += now - base::take(_pausedAt);
}
_allowCloseAt = at;
updateProgress(now);
if (!anim::Disabled()) {
_progress.start();
} else if (!_allowClose) {
_noAnimationTimer.callEach(crl::time(200));
}
}
}, lifetime());
updateProgress(_startedAt);
setClickedCallback([=] {
_actions.fire(_allowClose ? Action::Close : Action::PromotePremium);
});
}
rpl::producer<Action> Close::actions() const {
return _actions.events();
}
void Close::updateProgress(crl::time now) {
update();
}
QPoint Close::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos()) - _rippleOrigin;
}
QImage Close::prepareRippleMask() const {
return Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize });
}
void Close::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
paintRipple(p, _rippleOrigin);
const auto now = crl::now();
if (!_pausedAt) {
_allowClose = (now >= _allowCloseAt);
}
const auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now);
const auto msFull = _allowCloseAt - _startedAt;
const auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000;
const auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000;
const auto allowCloseLeft = anim::Disabled()
? (secondsFull ? (secondsTill / float64(secondsFull)) : 0)
: std::max(msFull ? (msTill / float64(msFull)) : 0., 0.);
const auto duration = crl::time(st::fadeWrapDuration);
const auto allowedProgress = anim::Disabled()
? (secondsTill ? 0. : 1.)
: std::clamp(-msTill, crl::time(), duration) / float64(duration);
if (_secondsTill != secondsTill) {
const auto initial = !_secondsTill;
_secondsTill = secondsTill;
_countdown.setText(QString::number(_secondsTill), _secondsTill);
if (initial) {
_countdown.finishAnimating();
}
}
auto pen = st::mediaviewTextLinkFg->p;
if (allowedProgress < 1.) {
if (allowedProgress > 0.) {
p.setOpacity(1. - allowedProgress);
}
p.setPen(pen);
const auto inner = QRect(
(width() - st::mediaSponsoredCloseDiameter) / 2,
(height() - st::mediaSponsoredCloseDiameter) / 2,
st::mediaSponsoredCloseDiameter,
st::mediaSponsoredCloseDiameter);
p.setFont(st::mediaSponsoredCloseFont);
_countdown.paint(
p,
inner.x() + (inner.width() - _countdown.countWidth()) / 2,
(inner.y()
+ (inner.height()
- st::mediaSponsoredCloseFont->height) / 2),
width());
const auto skip = 0.23;
const auto len = int(base::SafeRound(
arc::kFullLength * (1. - skip) * allowCloseLeft));
if (len > 0) {
const auto from = arc::kFullLength / 4;
auto hq = PainterHighQualityEnabler(p);
pen.setWidthF(st::mediaSponsoredCloseStroke);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.drawArc(inner, from, len);
}
p.setOpacity(1.);
}
const auto sizeFinal = st::mediaSponsoredCloseSize;
const auto sizeSmall = st::mediaSponsoredCloseCorner;
const auto twiceFinal = st::mediaSponsoredCloseTwice;
const auto twiceSmall = st::mediaSponsoredCloseSmall;
const auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall);
const auto twice = twiceSmall
+ allowedProgress * (twiceFinal - twiceSmall);
const auto leftFinal = (width() - size) / 2.;
const auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2.
- (st::mediaSponsoredCloseStroke / 2.)
- sizeSmall;
const auto topFinal = (height() - size) / 2.;
const auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.;
const auto left = leftSmall + allowedProgress * (leftFinal - leftSmall);
const auto top = topSmall + allowedProgress * (topFinal - topSmall);
auto hq = PainterHighQualityEnabler(p);
pen.setWidthF(twice / 2.);
p.setPen(pen);
p.drawLine(QPointF(left, top), QPointF(left + size, top + size));
p.drawLine(QPointF(left + size, top), QPointF(left, top + size));
}
[[nodiscard]] style::RoundButton PrepareAboutStyle() {
static auto textBg = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.1);
return result;
});
static auto textBgOver = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.15);
return result;
});
static auto rippleColor = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.2);
return result;
});
auto result = st::mediaSponsoredAbout;
result.textFg = st::mediaviewTextLinkFg;
result.textFgOver = st::mediaviewTextLinkFg;
result.textBg = textBg.color();
result.textBgOver = textBgOver.color();
result.ripple.color = rippleColor.color();
return result;
}
} // namespace
class PlaybackSponsored::Message final : public Ui::RpWidget {
public:
Message(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
void setFinalPosition(int x, int y);
void fadeIn();
void fadeOut(Fn<void()> hidden);
private:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
int resizeGetHeight(int newWidth) override;
void populate();
void startFadeIn();
void updateShown(Fn<void()> finished = nullptr);
void startFade(Fn<void()> finished);
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const Data::SponsoredMessage _data;
style::RoundButton _aboutSt;
std::unique_ptr<Ui::RoundButton> _about;
std::unique_ptr<Close> _close;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<Action> _actions;
std::shared_ptr<Data::PhotoMedia> _photo;
Ui::Text::String _title;
Ui::Text::String _text;
QPoint _finalPosition;
int _left = 0;
int _top = 0;
int _titleHeight = 0;
int _textHeight = 0;
QImage _cache;
Ui::Animations::Simple _showAnimation;
bool _shown = false;
bool _over = false;
bool _pressed = false;
rpl::lifetime _photoLifetime;
};
PlaybackSponsored::Message::Message(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt)
: RpWidget(parent)
, _session(&data.history->session())
, _show(std::move(show))
, _data(data)
, _aboutSt(PrepareAboutStyle())
, _about(std::make_unique<Ui::RoundButton>(
this,
tr::lng_search_sponsored_button(),
_aboutSt))
, _close(
std::make_unique<Close>(
this,
_aboutSt.ripple,
std::move(allowCloseAt))) {
_about->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
setMouseTracking(true);
populate();
hide();
}
rpl::producer<Action> PlaybackSponsored::Message::actions() const {
return rpl::merge(_actions.events(), _close->actions());
}
void PlaybackSponsored::Message::setFinalPosition(int x, int y) {
_finalPosition = { x, y };
if (_shown) {
updateShown();
}
}
void PlaybackSponsored::Message::fadeIn() {
_shown = true;
if (!_photo || _photo->loaded()) {
startFadeIn();
return;
}
_photo->owner()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _photo->loaded();
}) | rpl::start_with_next([=] {
_photoLifetime.destroy();
startFadeIn();
}, _photoLifetime);
}
void PlaybackSponsored::Message::startFadeIn() {
if (!_shown) {
return;
}
startFade([=] {
_session->sponsoredMessages().view(_data.randomId);
});
show();
}
void PlaybackSponsored::Message::fadeOut(Fn<void()> hidden) {
if (!_shown) {
if (const auto onstack = hidden) {
onstack();
}
return;
}
_shown = false;
startFade(std::move(hidden));
}
void PlaybackSponsored::Message::startFade(Fn<void()> finished) {
_cache = Ui::GrabWidgetToImage(this);
_about->hide();
_close->hide();
const auto from = _shown ? 0. : 1.;
const auto till = _shown ? 1. : 0.;
_showAnimation.start([=] {
updateShown(finished);
}, from, till, st::fadeWrapDuration);
}
void PlaybackSponsored::Message::updateShown(Fn<void()> finished) {
const auto shown = _showAnimation.value(_shown ? 1. : 0.);
const auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown);
move(_finalPosition.x(), _finalPosition.y() + shift);
update();
if (!_showAnimation.animating()) {
_cache = QImage();
_close->show();
_about->show();
if (const auto onstack = finished) {
onstack();
}
}
}
void PlaybackSponsored::Message::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto shown = _showAnimation.value(_shown ? 1. : 0.);
if (!_cache.isNull()) {
p.setOpacity(shown);
p.drawImage(0, 0, _cache);
return;
}
Ui::FillRoundRect(
p,
rect(),
st::mediaviewSaveMsgBg,
Ui::MediaviewSaveCorners);
const auto &padding = st::mediaSponsoredPadding;
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Large)) {
const auto size = st::mediaSponsoredThumb;
const auto x = padding.left();
const auto y = (height() - size) / 2;
p.drawPixmap(
x,
y,
image->pixSingle(
size,
size,
{ .options = Images::Option::RoundCircle }));
}
}
p.setPen(st::mediaviewControlFg);
_title.draw(p, {
.position = { _left, _top },
.availableWidth = _about->x() - _left,
.palette = &st::mediaviewTextPalette,
});
_text.draw(p, {
.position = { _left, _top + _titleHeight },
.availableWidth = _close->x() - _left,
.palette = &st::mediaviewTextPalette,
});
}
void PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) {
const auto &padding = st::mediaSponsoredPadding;
const auto point = e->pos();
const auto about = _about->geometry();
const auto close = _close->geometry();
const auto over = !about.marginsAdded(padding).contains(point)
&& !close.marginsAdded(padding).contains(point);
if (_over != over) {
_over = over;
setCursor(_over ? style::cur_pointer : style::cur_default);
}
}
void PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) {
if (_over) {
_pressed = true;
}
}
void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) {
if (base::take(_pressed) && _over) {
_session->sponsoredMessages().clicked(_data.randomId, false, false);
UrlClickHandler::Open(_data.link);
}
}
int PlaybackSponsored::Message::resizeGetHeight(int newWidth) {
const auto &padding = st::mediaSponsoredPadding;
const auto userpic = st::mediaSponsoredThumb;
const auto innerWidth = newWidth - _left - _close->width();
const auto titleWidth = innerWidth - _about->width() - padding.right();
_titleHeight = _title.countHeight(titleWidth);
_textHeight = _text.countHeight(innerWidth);
const auto use = std::max(_titleHeight + _textHeight, userpic);
const auto height = padding.top() + use + padding.bottom();
_left = padding.left() + (_photo ? (userpic + padding.left()) : 0);
_top = padding.top() + (use - _titleHeight - _textHeight) / 2;
_about->move(
_left + std::min(titleWidth, _title.maxWidth()) + padding.right(),
_top);
_close->move(
newWidth - _close->width(),
(height - _close->height()) / 2);
return height;
}
void PlaybackSponsored::Message::populate() {
const auto &from = _data.from;
const auto photo = from.photoId
? _data.history->owner().photo(from.photoId).get()
: nullptr;
if (photo) {
_photo = photo->createMediaView();
photo->load({}, LoadFromCloudOrLocal, true);
}
_title = Ui::Text::String(
st::semiboldTextStyle,
from.title,
kDefaultTextOptions,
st::msgMinWidth);
_text = Ui::Text::String(
st::defaultTextStyle,
_data.textWithEntities,
kMarkupTextOptions,
st::msgMinWidth);
_about->setClickedCallback([=] {
_menu = nullptr;
const auto parent = parentWidget();
_menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::mediaviewPopupMenu);
const auto raw = _menu.get();
const auto addAction = Ui::Menu::CreateAddActionCallback(raw);
Menu::FillSponsored(
addAction,
_show,
Menu::SponsoredPhrases::Channel,
_session->sponsoredMessages().lookupDetails(_data),
_session->sponsoredMessages().createReportCallback(
_data.randomId,
crl::guard(this, [=] { _actions.fire(Action::Close); })),
{ .dark = true });
_actions.fire(Action::Pause);
Ui::Connect(raw, &QObject::destroyed, this, [=] {
_actions.fire(Action::Unpause);
});
raw->popup(QCursor::pos());
});
}
PlaybackSponsored::PlaybackSponsored(
not_null<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item)
: _parent(controls->parentWidget())
, _session(&item->history()->session())
, _show(std::move(show))
, _itemId(item->fullId())
, _controlsGeometry(controls->geometryValue())
, _timer([=] { update(); }) {
_session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=](
Data::SponsoredForVideo data) {
if (data.list.empty()) {
return;
}
_data = std::move(data);
if (_data->state.initial()
|| (_data->state.itemIndex > _data->list.size())
|| (_data->state.itemIndex == _data->list.size()
&& _data->state.leftTillShow <= 0)) {
_data->state.itemIndex = 0;
_data->state.leftTillShow = std::max(
_data->startDelay,
kStartDelayMin);
}
update();
}));
}
PlaybackSponsored::~PlaybackSponsored() {
saveState();
}
void PlaybackSponsored::start() {
_started = true;
if (!_paused) {
_start = crl::now();
update();
}
}
void PlaybackSponsored::setPaused(bool paused) {
setPausedOutside(paused);
}
void PlaybackSponsored::updatePaused() {
const auto paused = _pausedInside || _pausedOutside;
if (_paused == paused) {
return;
} else if (_started && paused) {
update();
}
_paused = paused;
if (!_started) {
return;
} else if (_paused) {
_start = 0;
_timer.cancel();
_allowCloseAt = 0;
} else {
_start = crl::now();
update();
}
}
void PlaybackSponsored::setPausedInside(bool paused) {
if (_pausedInside == paused) {
return;
}
_pausedInside = paused;
updatePaused();
}
void PlaybackSponsored::setPausedOutside(bool paused) {
if (_pausedOutside == paused) {
return;
}
_pausedOutside = paused;
updatePaused();
}
void PlaybackSponsored::finish() {
_timer.cancel();
if (_data) {
saveState();
_data = std::nullopt;
}
}
void PlaybackSponsored::update() {
if (!_data || !_start) {
return;
}
const auto [now, state] = computeState();
const auto message = (_data->state.itemIndex < _data->list.size())
? &_data->list[state.itemIndex]
: nullptr;
const auto duration = message
? std::max(
message->durationMin + kDurationMin,
message->durationMax)
: crl::time(0);
if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) {
_data->state.leftTillShow = 0;
if (duration) {
_allowCloseAt = now + message->durationMin;
show(*message);
_start = now;
_timer.callOnce(duration);
saveState();
} else {
finish();
}
} else if (_data->state.leftTillShow <= 0
&& state.leftTillShow <= -duration) {
hide(now);
} else {
if (state.leftTillShow <= 0 && duration) {
_allowCloseAt = now + state.leftTillShow + message->durationMin;
if (!_widget) {
show(*message);
}
}
_data->state = state;
_timer.callOnce((state.leftTillShow > 0)
? state.leftTillShow
: (state.leftTillShow + duration));
}
}
void PlaybackSponsored::show(const Data::SponsoredMessage &data) {
_widget = std::make_unique<Message>(
_parent,
_show,
data,
_allowCloseAt.value());
const auto raw = _widget.get();
_controlsGeometry.value() | rpl::start_with_next([=](QRect controls) {
raw->resizeToWidth(controls.width());
raw->setFinalPosition(
controls.x(),
controls.y() - st::mediaSponsoredSkip - raw->height());
}, raw->lifetime());
raw->actions() | rpl::start_with_next([=](Action action) {
switch (action) {
case Action::Close: hide(crl::now()); break;
case Action::PromotePremium: showPremiumPromo(); break;
case Action::Pause: setPausedInside(true); break;
case Action::Unpause: setPausedInside(false); break;
}
}, raw->lifetime());
raw->fadeIn();
}
void PlaybackSponsored::showPremiumPromo() {
ShowPremiumPreviewBox(_show, PremiumFeature::NoAds);
}
void PlaybackSponsored::hide(crl::time now) {
Expects(_widget != nullptr);
_widget->fadeOut([this, raw = _widget.get()] {
if (_widget.get() == raw) {
_widget = nullptr;
}
});
++_data->state.itemIndex;
_data->state.leftTillShow = std::max(
_data->betweenDelay,
kStartDelayMin);
_start = now;
_timer.callOnce(_data->state.leftTillShow);
saveState();
}
void PlaybackSponsored::saveState() {
_session->sponsoredMessages().updateForVideo(
_itemId,
computeState().data);
}
PlaybackSponsored::State PlaybackSponsored::computeState() const {
auto result = State{ crl::now() };
if (!_data) {
return result;
}
result.data = _data->state;
if (!_start) {
return result;
}
const auto elapsed = result.now - _start;
result.data.leftTillShow -= elapsed;
return result;
}
rpl::lifetime &PlaybackSponsored::lifetime() {
return _lifetime;
}
bool PlaybackSponsored::Has(HistoryItem *item) {
return item
&& item->history()->session().sponsoredMessages().canHaveFor(item);
}
} // namespace Media::View

View file

@ -0,0 +1,83 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "base/weak_ptr.h"
#include "data/components/sponsored_messages.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Media::View {
class PlaybackSponsored final : public base::has_weak_ptr {
public:
PlaybackSponsored(
not_null<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item);
~PlaybackSponsored();
void start();
void setPaused(bool paused);
[[nodiscard]] rpl::lifetime &lifetime();
[[nodiscard]] static bool Has(HistoryItem *item);
private:
class Message;
struct State {
crl::time now = 0;
Data::SponsoredForVideoState data;
};
void update();
void finish();
void updatePaused();
void showPremiumPromo();
void setPausedInside(bool paused);
void setPausedOutside(bool paused);
void show(const Data::SponsoredMessage &data);
void hide(crl::time now);
[[nodiscard]] State computeState() const;
void saveState();
const not_null<QWidget*> _parent;
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const FullMsgId _itemId;
rpl::variable<QRect> _controlsGeometry;
std::unique_ptr<Message> _widget;
rpl::variable<crl::time> _allowCloseAt;
crl::time _start = 0;
bool _started = false;
bool _paused = false;
bool _pausedInside = false;
bool _pausedOutside = false;
base::Timer _timer;
std::optional<Data::SponsoredForVideo> _data;
rpl::lifetime _lifetime;
};
} // namespace Media::View

View file

@ -287,14 +287,12 @@ void AboutBox(
top->setForceRippled(false); top->setForceRippled(false);
}); });
FillSponsored( FillSponsored(
top,
Ui::Menu::CreateAddActionCallback(menu->get()), Ui::Menu::CreateAddActionCallback(menu->get()),
show, show,
phrases, phrases,
details, details,
report, report,
false, { .skipAbout = true });
true);
const auto global = top->mapToGlobal( const auto global = top->mapToGlobal(
QPoint(top->width() / 4 * 3, top->height() / 2)); QPoint(top->width() / 4 * 3, top->height() / 2));
raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
@ -390,18 +388,17 @@ void ShowReportSponsoredBox(
} // namespace } // namespace
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
SponsoredPhrases phrases, SponsoredPhrases phrases,
const Data::SponsoredMessages::Details &details, const Data::SponsoredMessages::Details &details,
Data::SponsoredReportAction report, Data::SponsoredReportAction report,
bool mediaViewer, SponsoredMenuSettings settings) {
bool skipAbout) {
const auto session = &show->session(); const auto session = &show->session();
const auto &info = details.info; const auto &info = details.info;
const auto dark = settings.dark;
if (!mediaViewer && !info.empty()) { if (!settings.skipInfo && !info.empty()) {
auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) { auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
const auto allText = ranges::accumulate( const auto allText = ranges::accumulate(
info, info,
@ -416,8 +413,10 @@ void FillSponsored(
for (const auto &i : info) { for (const auto &i : info) {
auto item = base::make_unique_q<Ui::Menu::MultilineAction>( auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
menu, menu,
st::defaultMenu, dark ? st::storiesMenu : st::defaultMenu,
st::historySponsorInfoItem, (dark
? st::historySponsorInfoItemDark
: st::historySponsorInfoItem),
st::historyHasCustomEmojiPosition, st::historyHasCustomEmojiPosition,
base::duplicate(i)); base::duplicate(i));
item->clicks( item->clicks(
@ -431,27 +430,31 @@ void FillSponsored(
addAction({ addAction({
.text = tr::lng_sponsored_info_menu(tr::now), .text = tr::lng_sponsored_info_menu(tr::now),
.handler = nullptr, .handler = nullptr,
.icon = &st::menuIconChannel, .icon = (dark
? &st::mediaMenuIconChannel
: &st::menuIconChannel),
.fillSubmenu = std::move(fillSubmenu), .fillSubmenu = std::move(fillSubmenu),
}); });
addAction({ addAction({
.separatorSt = &st::expandedMenuSeparator, .separatorSt = (dark
? &st::mediaviewMenuSeparator
: &st::expandedMenuSeparator),
.isSeparator = true, .isSeparator = true,
}); });
} }
if (details.canReport) { if (details.canReport) {
if (!skipAbout) { if (!settings.skipAbout) {
addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] {
show->show(Box(AboutBox, show, phrases, details, report)); show->show(Box(AboutBox, show, phrases, details, report));
}, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); }, (dark ? &st::mediaMenuIconInfo : &st::menuIconInfo));
} }
addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] {
ShowReportSponsoredBox(show, report); ShowReportSponsoredBox(show, report);
}, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); }, (dark ? &st::mediaMenuIconBlock : &st::menuIconBlock));
addAction({ addAction({
.separatorSt = (mediaViewer .separatorSt = (dark
? &st::mediaviewMenuSeparator ? &st::mediaviewMenuSeparator
: &st::expandedMenuSeparator), : &st::expandedMenuSeparator),
.isSeparator = true, .isSeparator = true,
@ -464,26 +467,22 @@ void FillSponsored(
} else { } else {
ShowPremiumPreviewBox(show, PremiumFeature::NoAds); ShowPremiumPreviewBox(show, PremiumFeature::NoAds);
} }
}, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel)); }, (dark ? &st::mediaMenuIconCancel : &st::menuIconCancel));
} }
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const FullMsgId &fullId, const FullMsgId &fullId,
bool mediaViewer, SponsoredMenuSettings settings) {
bool skipAbout) {
const auto session = &show->session(); const auto session = &show->session();
FillSponsored( FillSponsored(
parent,
addAction, addAction,
show, show,
PhrasesForMessage(fullId), PhrasesForMessage(fullId),
session->sponsoredMessages().lookupDetails(fullId), session->sponsoredMessages().lookupDetails(fullId),
session->sponsoredMessages().createReportCallback(fullId), session->sponsoredMessages().createReportCallback(fullId),
mediaViewer, settings);
skipAbout);
} }
void ShowSponsored( void ShowSponsored(
@ -495,11 +494,9 @@ void ShowSponsored(
st::popupMenuWithIcons); st::popupMenuWithIcons);
FillSponsored( FillSponsored(
parent,
Ui::Menu::CreateAddActionCallback(menu), Ui::Menu::CreateAddActionCallback(menu),
show, show,
fullId, fullId);
false);
menu->popup(QCursor::pos()); menu->popup(QCursor::pos());
} }

View file

@ -33,23 +33,25 @@ enum class SponsoredPhrases {
Search, Search,
}; };
struct SponsoredMenuSettings {
bool dark = false;
bool skipAbout = false;
bool skipInfo = false;
};
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
SponsoredPhrases phrases, SponsoredPhrases phrases,
const Data::SponsoredMessageDetails &details, const Data::SponsoredMessageDetails &details,
Data::SponsoredReportAction report, Data::SponsoredReportAction report,
bool mediaViewer, SponsoredMenuSettings settings = {});
bool skipAbout);
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const FullMsgId &fullId, const FullMsgId &fullId,
bool mediaViewer, SponsoredMenuSettings settings = {});
bool skipAbout = false);
void ShowSponsored( void ShowSponsored(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,

View file

@ -1344,7 +1344,7 @@ messages.messageViews#b6c4f543 views:Vector<MessageViews> chats:Vector<Chat> use
messages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage; messages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage;
messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector<MessageEntity> quote_offset:flags.10?int = MessageReplyHeader; messageReplyHeader#6917560b flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector<MessageEntity> quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader;
messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader;
messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector<Peer> channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector<Peer> channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies;
@ -1649,7 +1649,7 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count
stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = stories.StoryViews; stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = stories.StoryViews;
inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector<MessageEntity> quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; inputReplyToMessage#869fbe10 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector<MessageEntity> quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int = InputReplyTo;
inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo;
inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo;
@ -2720,4 +2720,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
// LAYER 207 // LAYER 209

View file

@ -117,7 +117,7 @@ bool DarkTasbarValueValid/* = false*/;
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2 p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2
1.7 * xm, 1.7 * xm,
10.5 * ym, 9.5 * ym,
4.4 * xm, 4.4 * xm,
4.4 * ym)); 4.4 * ym));
return image; return image;

View file

@ -939,7 +939,8 @@ void FillUniqueGiftMenu(
&& e.id.isEmpty() && e.id.isEmpty()
&& (e.in || (giftChannel && giftChannel->canManageGifts())) && (e.in || (giftChannel && giftChannel->canManageGifts()))
&& !e.giftTransferred && !e.giftTransferred
&& !e.giftRefunded; && !e.giftRefunded
&& !e.converted;
const auto unique = e.uniqueGift; const auto unique = e.uniqueGift;
if (unique if (unique
@ -1148,7 +1149,6 @@ void GenericCreditsEntryBox(
const auto isStarGift = e.stargift || e.soldOutInfo; const auto isStarGift = e.stargift || e.soldOutInfo;
const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty(); const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty();
const auto sentStarGift = creditsHistoryStarGift && !e.in; const auto sentStarGift = creditsHistoryStarGift && !e.in;
const auto convertedStarGift = creditsHistoryStarGift && e.converted;
const auto giftToSelf = isStarGift const auto giftToSelf = isStarGift
&& (e.barePeerId == selfPeerId) && (e.barePeerId == selfPeerId)
&& (e.in || e.bareGiftOwnerId == selfPeerId); && (e.in || e.bareGiftOwnerId == selfPeerId);
@ -1164,7 +1164,8 @@ void GenericCreditsEntryBox(
const auto starGiftCanManage = isStarGift const auto starGiftCanManage = isStarGift
&& !creditsHistoryStarGift && !creditsHistoryStarGift
&& (e.in || giftToChannelCanManage) && (e.in || giftToChannelCanManage)
&& !e.fromGiftSlug; && !e.fromGiftSlug
&& !e.converted;
const auto starGiftCanTransfer = isStarGift const auto starGiftCanTransfer = isStarGift
&& !creditsHistoryStarGift && !creditsHistoryStarGift
&& (e.in || giftToChannelCanTransfer); && (e.in || giftToChannelCanTransfer);
@ -1250,12 +1251,13 @@ void GenericCreditsEntryBox(
EntryToSavedStarGiftId(session, e), EntryToSavedStarGiftId(session, e),
style); style);
}; };
const auto canResell = CanResellGift(session, e);
AddUniqueGiftCover( AddUniqueGiftCover(
content, content,
rpl::single(*uniqueGift), rpl::single(*uniqueGift),
{}, {},
std::move(price), std::move(price),
CanResellGift(session, e) ? std::move(change) : Fn<void()>()); canResell ? std::move(change) : Fn<void()>());
AddSkip(content, st::defaultVerticalListSkip * 2); AddSkip(content, st::defaultVerticalListSkip * 2);
@ -1263,6 +1265,10 @@ void GenericCreditsEntryBox(
const auto type = SavedStarGiftMenuType::View; const auto type = SavedStarGiftMenuType::View;
FillUniqueGiftMenu(show, menu, e, type, st); FillUniqueGiftMenu(show, menu, e, type, st);
}); });
if (canResell) {
Ui::PreloadUniqueGiftResellPrices(session);
}
} else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) {
const auto thumb = content->add(object_ptr<Ui::CenterWrap<>>( const auto thumb = content->add(object_ptr<Ui::CenterWrap<>>(
content, content,
@ -1419,7 +1425,7 @@ void GenericCreditsEntryBox(
? tr::lng_credits_box_history_entry_gift_unavailable(tr::now) ? tr::lng_credits_box_history_entry_gift_unavailable(tr::now)
: sentStarGift : sentStarGift
? tr::lng_credits_box_history_entry_gift_sent(tr::now) ? tr::lng_credits_box_history_entry_gift_sent(tr::now)
: convertedStarGift : e.converted
? tr::lng_credits_box_history_entry_gift_converted(tr::now) ? tr::lng_credits_box_history_entry_gift_converted(tr::now)
: (isStarGift && !starGiftCanManage) : (isStarGift && !starGiftCanManage)
? tr::lng_gift_link_label_gift(tr::now) ? tr::lng_gift_link_label_gift(tr::now)
@ -1622,7 +1628,7 @@ void GenericCreditsEntryBox(
} }
const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);
if (!uniqueGift && starGiftCanManage) { if (!uniqueGift && (starGiftCanManage || e.converted)) {
Ui::AddSkip(content); Ui::AddSkip(content);
const auto about = box->addRow( const auto about = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>( object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
@ -1751,7 +1757,8 @@ void GenericCreditsEntryBox(
const auto canToggle = starGiftCanManage const auto canToggle = starGiftCanManage
&& !e.giftTransferred && !e.giftTransferred
&& !e.giftRefunded; && !e.giftRefunded
&& !e.converted;
const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) { const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) {
const auto showSection = !e.fromGiftsList; const auto showSection = !e.fromGiftsList;
const auto savedId = EntryToSavedStarGiftId(&show->session(), e); const auto savedId = EntryToSavedStarGiftId(&show->session(), e);

View file

@ -843,7 +843,6 @@ void SetupPremium(
button->addClickHandler([=] { button->addClickHandler([=] {
showOther(BusinessId()); showOther(BusinessId());
}); });
Ui::NewBadge::AddToRight(button);
if (controller->session().premiumCanBuy()) { if (controller->session().premiumCanBuy()) {
const auto button = AddButtonWithIcon( const auto button = AddButtonWithIcon(
@ -852,6 +851,8 @@ void SetupPremium(
st::settingsButton, st::settingsButton,
{ .icon = &st::menuIconGiftPremium } { .icon = &st::menuIconGiftPremium }
); );
Ui::NewBadge::AddToRight(button);
button->addClickHandler([=] { button->addClickHandler([=] {
Ui::ChooseStarGiftRecipient(controller); Ui::ChooseStarGiftRecipient(controller);
}); });

Some files were not shown because too many files have changed in this diff Show more