Compare commits

...

56 commits
v5.16.3 ... dev

Author SHA1 Message Date
bleizix
1dfe68e9f3 fix: cache icons in settings 2025-07-29 01:55:47 +03:00
bleizix
64b824671a fix: don't increment channel views 2025-07-29 01:55:46 +03:00
bleizix
4940d85b29 fix: AyuForward crashes & improvements 2025-07-29 01:55:43 +03:00
AlexeyZavar
157039946f fix: allow copying inline button links 2025-07-16 00:33:27 +03:00
AlexeyZavar
09083beed0 fix: entities serialization 2025-07-16 00:00:55 +03:00
AlexeyZavar
8aa6c4582c fix: rename hide reactions
The logic is inverted everywhere, e.g. when `hideGroupReactions` is true, it's actually meant to show, not hide
2025-07-15 23:40:16 +03:00
AlexeyZavar
aeb610ff70 feat: copy pack ID & copy single emoji ID 2025-07-15 23:29:37 +03:00
AlexeyZavar
1586386d6c fix: hide panel if reactions are hidden 2025-07-15 22:48:47 +03:00
AlexeyZavar
2eee51f891 fix: tray dot misalignment 2025-07-15 22:07:14 +03:00
AlexeyZavar
adf46969ca chore: update nlohmann json 2025-07-15 21:33:19 +03:00
AlexeyZavar
bd6ee188f8 chore: update sqlite3 2025-07-15 21:20:05 +03:00
AlexeyZavar
57893bdbf8 chore: update devs list 2025-07-15 21:17:14 +03:00
AlexeyZavar
7a37ab1b25 fix: message seconds on linux 2025-07-15 21:06:09 +03:00
AlexeyZavar
72618f98d1 fix: message field configuration in forums 2025-07-15 20:26:29 +03:00
AlexeyZavar
d98e6fd51e fix: disable video ads 2025-07-15 19:48:19 +03:00
bleizix
608bcf7d42 feat: save deleted entities 2025-07-15 17:47:25 +03:00
bleizix
06d8d14ad7 fix: AyuForward crashes 2025-07-15 17:40:17 +03:00
AlexeyZavar
aef400db0f fix: message shot layout 2025-07-15 17:27:28 +03:00
AlexeyZavar
cdfdaf9f5f fix: make it build 2025-07-15 17:22:19 +03:00
AlexeyZavar
5270f155ff Merge tag 'v5.16.4' into dev 2025-07-15 16:48:06 +03:00
John Preston
0514f13af0 Version 5.16.4.
- Fix problem with negative unread counters.
- Fix stars values display in statistics.
- Fix crash in messages fee disabling.
2025-07-14 20:51:13 +04:00
John Preston
e6a6763228 Fix build with GCC. 2025-07-14 20:51:13 +04:00
23rd
38c74bf2cf Added lottie icon and button to dialogs widget when dialogs are empty. 2025-07-14 19:42:17 +04:00
John Preston
a770e47575 Fix build with Xcode. 2025-07-14 19:07:05 +04:00
John Preston
c998352ab7 Possibly fix a crash in subsection tabs.
Fixes #29550.
2025-07-14 18:30:59 +04:00
John Preston
3683fa3814 Remove paid message service info in groups. 2025-07-14 17:40:30 +04:00
John Preston
e62881e08b Fix blocking users in channel direct messages.
Fixes #29549.
2025-07-14 17:15:25 +04:00
John Preston
01e313e56b Fix crash after deleting sublist.
Fixes #29548.
2025-07-14 15:40:14 +04:00
John Preston
2dd5f80468 Allow transferring gifts to channels. 2025-07-14 14:29:31 +04:00
John Preston
275fb3e96a Fix actions for converted gifts. 2025-07-14 14:24:41 +04:00
John Preston
3463916b9b De-duplicate icon for checklist task adding. 2025-07-14 14:24:08 +04:00
John Preston
a20de2515a Fix text-send permissions in groups.
Fixes #29546.
2025-07-14 13:58:38 +04:00
John Preston
79ea992a0f Suggest relevant min price for gift resale. 2025-07-14 13:28:57 +04:00
John Preston
0132436dc8 Move the NEW badge to gifts (market). 2025-07-14 11:29:57 +04:00
John Preston
a285c1abec Fix unsupported filtering in monoforum export.
Fixes #29545.
2025-07-14 11:21:14 +04:00
John Preston
1c41e01f0d Update API scheme to layer 209. 2025-07-14 09:54:04 +04:00
John Preston
f2e53ea490 Try scrolling to the task on jump. 2025-07-11 19:26:01 +04:00
John Preston
bff86b90fb Highlight tasks from reply/service messages. 2025-07-11 19:26:01 +04:00
John Preston
b5c9b6f552 Make and display replies to tasks. 2025-07-11 19:26:01 +04:00
John Preston
23f5102f1b Update API scheme to layer 208. 2025-07-11 19:26:01 +04:00
John Preston
bf51e911b8 Fix unread counters with monoforums.
Fixes #29544.
2025-07-11 19:26:00 +04:00
John Preston
4039d7ab71 Update tgcalls. 2025-07-11 19:26:00 +04:00
John Preston
77a09a0e59 Fix some search options. 2025-07-11 19:26:00 +04:00
John Preston
154c777788 Improve formatting for some strange cases. 2025-07-11 19:26:00 +04:00
John Preston
628c36c87d Fix crash in the private fee disable. 2025-07-11 19:26:00 +04:00
John Preston
a746b7abcf Fix reactions in media-only service messages.
Fixes #29543.
2025-07-11 19:26:00 +04:00
John Preston
64184e6c90 Support controls on sponsored in video. 2025-07-11 19:26:00 +04:00
John Preston
ecc955d2ce Show sponsored messages in video. 2025-07-11 19:26:00 +04:00
John Preston
f7e1b2c70c Fix build with QT_VERSION_MAJOR provided. 2025-07-11 19:26:00 +04:00
John Preston
284cbda7c0 Track shown sponsored in video. 2025-07-11 19:26:00 +04:00
John Preston
e5ca9e4c39 Ability to request video ads. 2025-07-11 19:26:00 +04:00
John Preston
02aaa71e78 Fix updating sublist chats in Saved Messages.
Fixes #29448.
2025-07-11 19:26:00 +04:00
AlexeyZavar
f56b5ea03e fix: forward box crash
Co-authored-by: mmlo <eu@memelo.dev>
2025-07-10 14:43:33 +03:00
John Preston
6afd4dcdd1 Fix stats values display. 2025-07-09 09:33:50 +04:00
23rd
52bb189996 Fixed possible crash when paste invalid proxy link to proxy box. 2025-07-09 09:33:50 +04:00
John Preston
8ff6f9af45 Remove unused DELAYLOAD. 2025-07-08 23:24:48 +04:00
118 changed files with 4760 additions and 1956 deletions

View file

@ -1339,6 +1339,8 @@ PRIVATE
media/view/media_view_playback_controls.h
media/view/media_view_playback_progress.cpp
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.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-Synch-l1-2-0.dll # Synchronization.lib
/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-Error-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">
<g clip-path="url(#clip0_1067_20)">
<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"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1362_52)">
<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>
<defs>
<clipPath id="clip0_1067_20">
<rect width="16" height="16.0037" fill="white" transform="translate(0.665001 0.668152)"/>
<clipPath id="clip0_1362_52">
<rect width="16" height="16.0037" fill="white"/>
</clipPath>
</defs>
</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_bot_name" = "Bot name";
"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_saved_sublists" = "You can save messages from other chats here.";
"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_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add 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_preview_reply_to" = "Reply to {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_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_list.tgs">../../animations/edit_peers/topics_list.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="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>

View file

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

View file

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

View file

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

View file

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

View file

@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "main/main_session.h"
// AyuGram includes
#include "ayu/ayu_settings.h"
namespace Api {
namespace {
@ -83,6 +87,8 @@ void ViewsManager::pollExtendedMedia(
}
void ViewsManager::viewsIncrement() {
const auto &settings = AyuSettings::getInstance();
for (auto i = _toIncrement.begin(); i != _toIncrement.cend();) {
if (_incrementRequests.contains(i->first)) {
++i;
@ -97,7 +103,7 @@ void ViewsManager::viewsIncrement() {
const auto requestId = _api.request(MTPmessages_GetMessagesViews(
i->first->input,
MTP_vector<MTPint>(ids),
MTP_bool(true)
MTP_bool(settings.sendReadMessages)
)).done([=](
const MTPmessages_MessageViews &result,
mtpRequestId requestId) {

View file

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

View file

@ -76,8 +76,8 @@ public:
bool disableNotificationsDelay;
bool localPremium;
bool hideChannelReactions;
bool hideGroupReactions;
bool showChannelReactions;
bool showGroupReactions;
QString appIcon;
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(showStreamerToggleInTray)
NLOHMANN_JSON_TO(monoFont)
NLOHMANN_JSON_TO(hideChannelReactions)
NLOHMANN_JSON_TO(hideGroupReactions)
NLOHMANN_JSON_TO(showChannelReactions)
NLOHMANN_JSON_TO(showGroupReactions)
NLOHMANN_JSON_TO(hideNotificationCounters)
NLOHMANN_JSON_TO(hideNotificationBadge)
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(showStreamerToggleInTray)
NLOHMANN_JSON_FROM_WITH_DEFAULT(monoFont)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideChannelReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideGroupReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showChannelReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGroupReactions)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationCounters)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationBadge)
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) {
state = st;
session.changes().peerUpdated(session.data().peer(*peer), Data::PeerUpdate::Flag::Rights);
auto peerCopy = *peer;
crl::on_main([&, peerCopy]
{
session.changes().peerUpdated(session.data().peer(peerCopy), Data::PeerUpdate::Flag::Rights);
});
}
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);
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());
return prepared;
};
@ -120,7 +127,9 @@ static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
const auto groupId = startItem->groupId();
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) {
return list;
@ -132,7 +141,9 @@ static Ui::PreparedList prepareMedia(not_null<Main::Session*> session,
break;
}
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;
}
}
@ -202,12 +213,11 @@ void sendMedia(
}
bool isAyuForwardNeeded(const std::vector<not_null<HistoryItem*>> &items) {
for (const auto &item : items) {
if (isAyuForwardNeeded(item)) {
return true;
}
}
return false;
const auto needAyuForward = [&](const auto &item)
{
return isAyuForwardNeeded(item);
};
return std::ranges::any_of(items, needAyuForward);
}
bool isAyuForwardNeeded(not_null<HistoryItem*> item) {
@ -232,7 +242,10 @@ void intelligentForward(
const Api::SendAction &action,
const Data::ResolvedForwardDraft &draft) {
const auto history = action.history;
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
crl::on_main([&]
{
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
});
const auto items = draft.items;
const auto peer = history->peer;
@ -291,12 +304,15 @@ void forwardMessages(
not_null<Main::Session*> session,
const Api::SendAction &action,
bool forwardState,
Data::ResolvedForwardDraft draft) {
const Data::ResolvedForwardDraft &draft) {
const auto items = draft.items;
const auto history = action.history;
const auto peer = history->peer;
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
crl::on_main([&]
{
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
});
std::shared_ptr<ForwardState> state;
@ -378,6 +394,23 @@ void forwardMessages(
}
}
// remove not finished files
for (int j = preparedMedia.files.size() - 1; j >= 0; j--) {
auto &file = preparedMedia.files[j];
QFile f(file.path);
if (groupMedia[j]->photo() && f.size() < groupMedia[j]->photo()->imageByteSize(Data::PhotoSize::Large)
||
groupMedia[j]->document() && f.size() < groupMedia[j]->document()->size
) {
preparedMedia.files.erase(preparedMedia.files.begin() + j);
}
}
if (preparedMedia.files.empty()) {
continue;
}
auto groups = Ui::DivideByGroups(
std::move(preparedMedia),
way,

View file

@ -47,6 +47,6 @@ void forwardMessages(
not_null<Main::Session*> session,
const Api::SendAction &action,
bool forwardState,
Data::ResolvedForwardDraft draft);
const Data::ResolvedForwardDraft &draft);
}

View file

@ -5,8 +5,9 @@
//
// Copyright @Radolyn, 2025
#include "ayu_sync.h"
#include "apiwrap.h"
#include "api/api_sending.h"
#include "apiwrap.h"
#include "ayu/utils/telegram_helpers.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/file_utilities.h"
@ -54,7 +55,7 @@ private:
namespace AyuSync {
QString pathForSave(not_null<Main::Session*> session) {
const auto path = Core::App().settings().downloadPath();
auto path = Core::App().settings().downloadPath();
if (path.isEmpty()) {
return File::DefaultDownloadPath(session);
}
@ -65,6 +66,10 @@ QString pathForSave(not_null<Main::Session*> session) {
}
QString filePath(not_null<Main::Session*> session, const Data::Media *media) {
if (!media) {
return {};
}
if (const auto document = media->document()) {
if (!document->filename().isEmpty()) {
return pathForSave(session) + media->document()->filename();
@ -84,7 +89,7 @@ QString filePath(not_null<Main::Session*> session, const Data::Media *media) {
return pathForSave(session) + QString::number(photo->getDC()) + "_" + QString::number(photo->id) + ".jpg";
}
return QString();
return {};
}
qint64 fileSize(not_null<HistoryItem*> item) {
@ -127,21 +132,42 @@ void loadDocumentSync(not_null<Main::Session*> session, DocumentData *data, not_
auto latch = std::make_shared<TimedCountDownLatch>(1);
auto lifetime = std::make_shared<rpl::lifetime>();
data->save(Data::FileOriginMessage(item->fullId()), filePath(session, item->media()));
rpl::single() | rpl::then(
session->downloaderTaskFinished()
) | rpl::filter([&]
auto path = filePath(session, item->media());
if (path.isEmpty()) {
return;
}
crl::on_main([&]
{
return data->status == FileDownloadFailed || fileSize(item) == data->size;
}) | rpl::start_with_next([&]() mutable
{
latch->countDown();
base::take(lifetime)->destroy();
},
*lifetime);
data->save(Data::FileOriginMessage(item->fullId()), path);
latch->await(std::chrono::minutes(5));
rpl::single() | rpl::then(
session->downloaderTaskFinished()
) | rpl::filter([&]
{
return data->status == FileDownloadFailed || fileSize(item) == data->size;
}) | rpl::start_with_next([&]() mutable
{
latch->countDown();
},
*lifetime);
});
constexpr auto overall = std::chrono::minutes(15);
const auto startTime = std::chrono::steady_clock::now();
while (std::chrono::steady_clock::now() - startTime < overall) {
if (latch->await(std::chrono::minutes(5))) {
break;
}
if (!data->loading()) {
break;
}
}
base::take(lifetime)->destroy();
}
void forwardMessagesSync(not_null<Main::Session*> session,
@ -207,19 +233,21 @@ void loadPhotoSync(not_null<Main::Session*> session, const std::pair<not_null<Ph
if (finalCheck()) {
saveToFiles();
} else {
session->downloaderTaskFinished() | rpl::filter([&]
crl::on_main([&]
{
return finalCheck();
}) | rpl::start_with_next([&]() mutable
{
saveToFiles();
latch->countDown();
base::take(lifetime)->destroy();
},
*lifetime);
session->downloaderTaskFinished() | rpl::filter([&]
{
return finalCheck();
}) | rpl::start_with_next([&]() mutable
{
saveToFiles();
latch->countDown();
},
*lifetime);
});
latch->await(std::chrono::minutes(5));
base::take(lifetime)->destroy();
}
latch->await(std::chrono::minutes(5));
}
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &message) {
@ -240,19 +268,21 @@ void waitForMsgSync(not_null<Main::Session*> session, const Api::SendAction &act
auto latch = std::make_shared<TimedCountDownLatch>(1);
auto lifetime = std::make_shared<rpl::lifetime>();
crl::on_main([&]
{
session->data().itemIdChanged()
| rpl::filter([&](const Data::Session::IdChange &update)
{
return action.history->peer->id == update.newId.peer;
}) | rpl::start_with_next([&]
{
latch->countDown();
},
*lifetime);
});
session->data().itemIdChanged()
| rpl::filter([&](const Data::Session::IdChange &update)
{
return action.history->peer->id == update.newId.peer;
}) | rpl::start_with_next([&]
{
latch->countDown();
base::take(lifetime)->destroy();
},
*lifetime);
latch->await(std::chrono::minutes(2));
latch->await(std::chrono::minutes(5));
base::take(lifetime)->destroy();
}
void sendDocumentSync(not_null<Main::Session*> session,
@ -260,9 +290,6 @@ void sendDocumentSync(not_null<Main::Session*> session,
SendMediaType type,
TextWithTags &&caption,
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>();
groupId->groupId = base::RandomValue<uint64>();
@ -272,27 +299,7 @@ void sendDocumentSync(not_null<Main::Session*> session,
session->api().sendFiles(std::move(lst), type, std::move(caption), groupId, 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();
waitForMsgSync(session, action);
}
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() {
using Mode = HistoryView::ElementChatMode;
// Mode::Wide;
return Mode::Default;
return Mode::Wide;
}
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
** 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
** unit. This allows many compilers to do optimizations that would not be
** 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.
**
** 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()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742"
#define SQLITE_VERSION "3.50.2"
#define SQLITE_VERSION_NUMBER 3050002
#define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079"
/*
** 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
** 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
** is safe to pass to routines like:
** <ul>
@ -5079,7 +5079,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt
**
** ^(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:
**
** <ul>
@ -5124,7 +5124,7 @@ typedef struct sqlite3_context sqlite3_context;
**
** [[byte-order determination rules]] ^The byte-order of
** 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
** machine for sqlite3_bind_text16() or the byte order specified in
** 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
** that parameter must be the byte offset
** 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
** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined.
@ -5356,7 +5356,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt
**
** ^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.
** ^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
@ -5925,8 +5925,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
**
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL
** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of
** 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].
**
** ^(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
** is deleted, either by being overloaded or when the database connection
** 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
**
** ^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.
** ^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
@ -6395,7 +6395,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs.
**
** ^(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
** the same aggregate function instance will not resize the memory
** 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
** 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.
**
** 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
** function result. If the 3rd parameter is non-negative, then it
** 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
** parameter, then the resulting string will contain embedded NULs and the
** 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()
** 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
** the [sqlite3_context] pointer, the results are undefined.
*/
@ -7135,7 +7135,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3
**
** ^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
** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** 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
** in a read transaction. Content has been read from the database file
** 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
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [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
** in a write transaction. Content has been written to the database file
** 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_READ 1
@ -7520,7 +7520,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size
**
** 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
** 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>)^
**
** 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_hard_heap_limit64(sqlite3_int64 N);
@ -7693,8 +7693,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc.
** ^(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".
** If that does not work, it constructs a name "sqlite3_X_init" where the
** X is consists of the lower-case equivalent of all ASCII alphabetic
** If that does not work, it constructs a name "sqlite3_X_init" where
** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^
** ^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
** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the
** entry point where as follows:
** entry point were as follows:
**
** <blockquote><pre>
** &nbsp; int xEntryPoint(
@ -7929,7 +7929,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** 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,
** when the omit flag is true there is no guarantee that the constraint will
** 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
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [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
** return at most one row.
**
@ -8096,7 +8096,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through
** 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
** 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
** 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()]
** 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:
** <ul>
@ -8381,7 +8381,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
**
** ^Returns the size in bytes of the BLOB accessible via 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.
**
** 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
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** 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:
**
** <ul>
@ -8764,7 +8764,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection
** 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
** when the [threading mode] is Serialized.
** ^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
**
** 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,
** 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
** 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
** [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
** 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
@ -9141,7 +9141,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The
** 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
** 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>
** <dd>This parameter returns the number of malloc attempts that were
** 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]]
** ^(<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
** memory requested being larger than the lookaside slot size.
** 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]]
** ^(<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
** memory already being in use.
** 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>
** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<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
** connections.)^ In other words, if none of the pager caches associated
** 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
** 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>
** <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
** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <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
** cache overflowing. Transactions are more efficient if they are written
** 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.
** </dd>
**
@ -9784,7 +9786,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically
** 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
** 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().
**
** ^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.
** ^If an out-of-memory condition or IO error occurred during any prior
** 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
**
** ^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
** 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
@ -15442,8 +15444,8 @@ typedef INT16_TYPE LogEst;
** assuming n is a signed integer type. UMXV(n) is similar for unsigned
** integer types.
*/
#define SMXV(n) ((((i64)1)<<(sizeof(n)-1))-1)
#define UMXV(n) ((((i64)1)<<(sizeof(n)))-1)
#define SMXV(n) ((((i64)1)<<(sizeof(n)*8-1))-1)
#define UMXV(n) ((((i64)1)<<(sizeof(n)*8))-1)
/*
** 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_REAL 0x45 /* 'E' */
#define SQLITE_AFF_FLEXNUM 0x46 /* 'F' */
#define SQLITE_AFF_DEFER 0x58 /* 'X' - defer computation until later */
#define sqlite3IsNumericAffinity(X) ((X)>=SQLITE_AFF_NUMERIC)
@ -19253,7 +19256,7 @@ struct AggInfo {
** from source tables rather than from accumulators */
u8 useSortingIdx; /* In direct mode, reference the sorting index rather
** 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 sortingIdxPTab; /* Cursor number of pseudo-table */
int iFirstReg; /* First register in range for aCol[] and aFunc[] */
@ -19262,8 +19265,8 @@ struct AggInfo {
Table *pTab; /* Source table */
Expr *pCExpr; /* The original expression */
int iTable; /* Cursor number of the source table */
i16 iColumn; /* Column number within the source table */
i16 iSorterColumn; /* Column number in the sorting index */
int iColumn; /* Column number within the source table */
int iSorterColumn; /* Column number in the sorting index */
} *aCol;
int nColumn; /* Number of used entries in aCol[] */
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
** in the following list than the lock being obtained are already held:
**
** 1. Checkpointer lock (ofst==1).
** 2. Write lock (ofst==0).
** 3. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK).
** 1. Recovery lock (ofst==2).
** 2. Checkpointer lock (ofst==1).
** 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
** occur later in the above list than the lock being obtained may be
** held.
**
** It is not permitted to block on the RECOVER lock.
*/
#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG)
{
u16 lockMask = (p->exclMask|p->sharedMask);
assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || (
(ofst!=2) /* not RECOVER */
(ofst!=2 || lockMask==0)
&& (ofst!=1 || lockMask==0 || lockMask==2)
&& (ofst!=0 || lockMask<3)
&& (ofst<3 || lockMask<(1<<ofst))
@ -49849,7 +49851,11 @@ static int winHandleLockTimeout(
if( res==WAIT_OBJECT_0 ){
ret = TRUE;
}else if( res==WAIT_TIMEOUT ){
#if SQLITE_ENABLE_SETLK_TIMEOUT==1
rc = SQLITE_BUSY_TIMEOUT;
#else
rc = SQLITE_BUSY;
#endif
}else{
/* Some other error has occurred */
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
** in the following list than the lock being obtained are already held:
**
** 1. Checkpointer lock (ofst==1).
** 2. Write lock (ofst==0).
** 3. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK).
** 1. Recovery lock (ofst==2).
** 2. Checkpointer lock (ofst==1).
** 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
** occur later in the above list than the lock being obtained may be
** held.
**
** It is not permitted to block on the RECOVER lock.
*/
#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG)
{
u16 lockMask = (p->exclMask|p->sharedMask);
assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || (
(ofst!=2) /* not RECOVER */
(ofst!=2 || lockMask==0)
&& (ofst!=1 || lockMask==0 || lockMask==2)
&& (ofst!=0 || lockMask<3)
&& (ofst<3 || lockMask<(1<<ofst))
@ -54963,7 +54968,9 @@ bitvec_set_rehash:
}else{
memcpy(aiValues, p->u.aHash, sizeof(p->u.aHash));
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);
for(j=0; j<BITVEC_NINT; 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" */
char *zWal; /* File name for write-ahead log */
#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->journalSizeLimit, &pPager->pWal
);
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT
if( rc==SQLITE_OK ){
sqlite3WalDb(pPager->pWal, pPager->dbWal);
}
#endif
}
pagerFixMaplimit(pPager);
@ -65750,6 +65765,7 @@ SQLITE_PRIVATE int sqlite3PagerWalWriteLock(Pager *pPager, int bLock){
** blocking locks are required.
*/
SQLITE_PRIVATE void sqlite3PagerWalDb(Pager *pPager, sqlite3 *db){
pPager->dbWal = db;
if( pagerUseWal(pPager) ){
sqlite3WalDb(pPager->pWal, db);
}
@ -68923,7 +68939,6 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int *pCnt){
rc = walIndexReadHdr(pWal, pChanged);
}
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT
walDisableBlocking(pWal);
if( rc==SQLITE_BUSY_TIMEOUT ){
rc = SQLITE_BUSY;
*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
** right on the second iteration.
*/
(void)walEnableBlocking(pWal);
if( pWal->apWiData[0]==0 ){
/* This branch is taken when the xShmMap() method returns SQLITE_BUSY.
** 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;
}
}
walDisableBlocking(pWal);
if( rc!=SQLITE_OK ){
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);
}
SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; )
pWal->iReCksum = 0;
}
return rc;
}
@ -69688,6 +69706,9 @@ SQLITE_PRIVATE int sqlite3WalSavepointUndo(Wal *pWal, u32 *aWalData){
walCleanupHash(pWal);
}
SEH_EXCEPT( rc = SQLITE_IOERR_IN_PAGE; )
if( pWal->iReCksum>pWal->hdr.mxFrame ){
pWal->iReCksum = 0;
}
}
return rc;
@ -75228,6 +75249,13 @@ static SQLITE_NOINLINE int btreeBeginTrans(
(void)sqlite3PagerWalWriteLock(pPager, 0);
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 &&
btreeInvokeBusyHandler(pBt) );
sqlite3PagerWalDb(pPager, 0);
@ -105039,7 +105067,7 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit(
assert( pCsr->eCurType==CURTYPE_SORTER );
assert( sizeof(KeyInfo) + UMXV(pCsr->pKeyInfo->nKeyField)*sizeof(CollSeq*)
< 0x7fffffff );
szKeyInfo = SZ_KEYINFO(pCsr->pKeyInfo->nKeyField+1);
szKeyInfo = SZ_KEYINFO(pCsr->pKeyInfo->nKeyField);
sz = SZ_VDBESORTER(nWorker+1);
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
);
}
if( op==TK_VECTOR ){
if( op==TK_VECTOR
|| (op==TK_FUNCTION && pExpr->affExpr==SQLITE_AFF_DEFER)
){
assert( ExprUseXList(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;
continue;
}
if( op==TK_VECTOR ){
if( op==TK_VECTOR
|| (op==TK_FUNCTION && p->affExpr==SQLITE_AFF_DEFER)
){
assert( ExprUseXList(p) );
p = p->x.pList->a[0].pExpr;
continue;
@ -117322,7 +117354,9 @@ static void findOrCreateAggInfoColumn(
){
struct AggInfo_col *pCol;
int k;
int mxTerm = pParse->db->aLimit[SQLITE_LIMIT_COLUMN];
assert( mxTerm <= SMXV(i16) );
assert( pAggInfo->iFirstReg==0 );
pCol = pAggInfo->aCol;
for(k=0; k<pAggInfo->nColumn; k++, pCol++){
@ -117340,6 +117374,10 @@ static void findOrCreateAggInfoColumn(
assert( pParse->db->mallocFailed );
return;
}
if( k>mxTerm ){
sqlite3ErrorMsg(pParse, "more than %d aggregate terms", mxTerm);
k = mxTerm;
}
pCol = &pAggInfo->aCol[k];
assert( ExprUseYTab(pExpr) );
pCol->pTab = pExpr->y.pTab;
@ -117373,6 +117411,7 @@ fix_up_expr:
if( pExpr->op==TK_COLUMN ){
pExpr->op = TK_AGG_COLUMN;
}
assert( k <= SMXV(pExpr->iAgg) );
pExpr->iAgg = (i16)k;
}
@ -117457,13 +117496,19 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){
** function that is already in the pAggInfo structure
*/
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++){
if( NEVER(pItem->pFExpr==pExpr) ) break;
if( sqlite3ExprCompare(0, pItem->pFExpr, pExpr, -1)==0 ){
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[]
*/
u8 enc = ENC(pParse->db);
@ -117517,6 +117562,7 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){
*/
assert( !ExprHasProperty(pExpr, EP_TokenOnly|EP_Reduced) );
ExprSetVVAProperty(pExpr, EP_NoReduce);
assert( i <= SMXV(pExpr->iAgg) );
pExpr->iAgg = (i16)i;
pExpr->pAggInfo = pAggInfo;
return WRC_Prune;
@ -131975,7 +132021,7 @@ static void concatFuncCore(
int nSep,
const char *zSep
){
i64 j, k, n = 0;
i64 j, n = 0;
int i;
char *z;
for(i=0; i<argc; i++){
@ -131989,8 +132035,8 @@ static void concatFuncCore(
}
j = 0;
for(i=0; i<argc; i++){
k = sqlite3_value_bytes(argv[i]);
if( k>0 ){
if( sqlite3_value_type(argv[i])!=SQLITE_NULL ){
int k = sqlite3_value_bytes(argv[i]);
const char *v = (const char*)sqlite3_value_text(argv[i]);
if( v!=0 ){
if( j>0 && nSep>0 ){
@ -145364,7 +145410,7 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){
}
pE1 = sqlite3CreateColumnExpr(db, pSrc, 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
** JOINs. If only a single table on the left side of this join
** 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() */
static const Token tkCoalesce = { "coalesce", 8 };
assert( pE1!=0 );
ExprSetProperty(pE1, EP_CanBeNull);
while( tableAndColumnIndex(pSrc, iLeft+1, i, zName, &iLeft, &iLeftCol,
pRight->fg.isSynthUsing)!=0 ){
if( pSrc->a[iLeft].fg.isUsing==0
@ -145396,7 +145444,13 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){
if( pFuncArgs ){
pFuncArgs = sqlite3ExprListAppend(pParse, pFuncArgs, pE1);
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);
sqlite3SrcItemColumnUsed(pRight, iRightCol);
@ -149004,9 +149058,9 @@ static int compoundHasDifferentAffinities(Select *p){
** from 2015-02-09.)
**
** (3) If the subquery is the right operand of a LEFT JOIN then
** (3a) the subquery may not be a join and
** (3b) the FROM clause of the subquery may not contain a virtual
** table and
** (3a) the subquery may not be a join
** (**) Was (3b): "the FROM clause of the subquery may not contain
** a virtual table"
** (**) Was: "The outer query may not have a GROUP BY." This case
** is now managed correctly
** (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( pSubSrc->nSrc>1 /* (3a) */
|| IsVirtual(pSubSrc->a[0].pSTab) /* (3b) */
/**** || IsVirtual(pSubSrc->a[0].pSTab) (3b)-omitted */
|| (p->selFlags & SF_Distinct)!=0 /* (3d) */
|| (pSubitem->fg.jointype & JT_RIGHT)!=0 /* (26) */
){
@ -161722,12 +161776,13 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart(
if( pLevel->iLeftJoin==0 ){
/* 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
** 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
** for a LEFT JOIN.
** 2019-11-02 ticket 623eff57e76d45f6 (LEFT JOIN)
** 2025-05-29 forum post 7dee41d32506c4ae (RIGHT JOIN)
*/
if( pIdx->pPartIdxWhere ){
if( pIdx->pPartIdxWhere && pLevel->pRJ==0 ){
whereApplyPartialIndexConstraints(pIdx->pPartIdxWhere, iCur, pWC);
}
}else{
@ -163400,30 +163455,42 @@ static void exprAnalyzeOrTerm(
** 1. The SQLITE_Transitive optimization must be enabled
** 2. Must be either an == or an IS operator
** 3. Not originating in the ON clause of an OUTER JOIN
** 4. The affinities of A and B must be compatible
** 5a. Both operands use the same collating sequence OR
** 5b. The overall collating sequence is BINARY
** 4. The operator is not IS or else the query does not contain RIGHT JOIN
** 5. The affinities of A and B must be compatible
** 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
** 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
** 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;
CollSeq *pColl;
if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0;
if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0;
if( ExprHasProperty(pExpr, EP_OuterON) ) return 0;
if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; /* (1) */
if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; /* (2) */
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);
aff2 = sqlite3ExprAffinity(pExpr->pRight);
if( aff1!=aff2
&& (!sqlite3IsNumericAffinity(aff1) || !sqlite3IsNumericAffinity(aff2))
){
return 0;
return 0; /* (5) */
}
pColl = sqlite3ExprCompareCollSeq(pParse, pExpr);
if( sqlite3IsBinary(pColl) ) return 1;
return sqlite3ExprCollSeqMatch(pParse, pExpr->pLeft, pExpr->pRight);
if( !sqlite3IsBinary(pColl)
&& !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;
pTerm = &pWC->a[idxTerm];
pTerm->wtFlags |= TERM_COPIED;
if( termIsEquivalence(pParse, pDup) ){
assert( pWInfo->pTabList!=0 );
if( termIsEquivalence(pParse, pDup, pWInfo->pTabList) ){
pTerm->eOperator |= WO_EQUIV;
eExtraOp = WO_EQUIV;
}
@ -184391,6 +184458,7 @@ SQLITE_API int sqlite3_setlk_timeout(sqlite3 *db, int ms, int flags){
#endif
if( ms<-1 ) return SQLITE_RANGE;
#ifdef SQLITE_ENABLE_SETLK_TIMEOUT
sqlite3_mutex_enter(db->mutex);
db->setlkTimeout = ms;
db->setlkFlags = flags;
sqlite3BtreeEnterAll(db);
@ -184402,6 +184470,7 @@ SQLITE_API int sqlite3_setlk_timeout(sqlite3 *db, int ms, int flags){
}
}
sqlite3BtreeLeaveAll(db);
sqlite3_mutex_leave(db->mutex);
#endif
#if !defined(SQLITE_ENABLE_API_ARMOR) && !defined(SQLITE_ENABLE_SETLK_TIMEOUT)
UNUSED_PARAMETER(db);
@ -209021,8 +209090,10 @@ static int jsonBlobChangePayloadSize(
nExtra = 1;
}else if( szType==13 ){
nExtra = 2;
}else{
}else if( szType==14 ){
nExtra = 4;
}else{
nExtra = 8;
}
if( szPayload<=11 ){
nNeeded = 0;
@ -213407,6 +213478,8 @@ SQLITE_PRIVATE int sqlite3JsonTableFunctions(sqlite3 *db){
#endif
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
** found in sqliteInt.h
@ -235449,6 +235522,7 @@ SQLITE_EXTENSION_INIT1
/* #include <string.h> */
/* #include <assert.h> */
/* #include <stddef.h> */
#ifndef SQLITE_AMALGAMATION
@ -257192,7 +257266,7 @@ static void fts5SourceIdFunc(
){
assert( nArg==0 );
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++){
if( pConfig->abUnindexed[iCol-1]==0 ){
sqlite3_value *pVal = 0;
sqlite3_value *pFree = 0;
const char *pText = 0;
int nText = 0;
const char *pLoc = 0;
@ -258023,11 +258098,22 @@ static int fts5StorageDeleteFromIndex(
if( pConfig->bLocale && sqlite3Fts5IsLocaleValue(pConfig, pVal) ){
rc = sqlite3Fts5DecodeLocaleValue(pVal, &pText, &nText, &pLoc, &nLoc);
}else{
pText = (const char*)sqlite3_value_text(pVal);
nText = sqlite3_value_bytes(pVal);
if( pConfig->bLocale && pSeek ){
pLoc = (const char*)sqlite3_column_text(pSeek, iCol + pConfig->nCol);
nLoc = sqlite3_column_bytes(pSeek, iCol + pConfig->nCol);
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);
nText = sqlite3_value_bytes(pVal);
if( pConfig->bLocale && pSeek ){
pLoc = (const char*)sqlite3_column_text(pSeek, iCol+pConfig->nCol);
nLoc = sqlite3_column_bytes(pSeek, iCol + pConfig->nCol);
}
}
}
@ -258043,6 +258129,7 @@ static int fts5StorageDeleteFromIndex(
}
sqlite3Fts5ClearLocale(pConfig);
}
sqlite3_value_free(pFree);
}
}
if( rc==SQLITE_OK && p->nTotalRow<1 ){

View file

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742"
#define SQLITE_VERSION "3.50.2"
#define SQLITE_VERSION_NUMBER 3050002
#define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079"
/*
** 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
** 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
** is safe to pass to routines like:
** <ul>
@ -4760,7 +4760,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt
**
** ^(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:
**
** <ul>
@ -4805,7 +4805,7 @@ typedef struct sqlite3_context sqlite3_context;
**
** [[byte-order determination rules]] ^The byte-order of
** 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
** machine for sqlite3_bind_text16() or the byte order specified in
** 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
** that parameter must be the byte offset
** 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
** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined.
@ -5037,7 +5037,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt
**
** ^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.
** ^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
@ -5606,8 +5606,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
**
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL
** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of
** 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].
**
** ^(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
** is deleted, either by being overloaded or when the database connection
** 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
**
** ^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.
** ^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
@ -6076,7 +6076,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs.
**
** ^(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
** the same aggregate function instance will not resize the memory
** 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
** 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.
**
** 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
** function result. If the 3rd parameter is non-negative, then it
** 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
** parameter, then the resulting string will contain embedded NULs and the
** 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()
** 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
** the [sqlite3_context] pointer, the results are undefined.
*/
@ -6816,7 +6816,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3
**
** ^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
** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** 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
** in a read transaction. Content has been read from the database file
** 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
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [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
** in a write transaction. Content has been written to the database file
** 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_READ 1
@ -7201,7 +7201,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size
**
** 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
** 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>)^
**
** 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_hard_heap_limit64(sqlite3_int64 N);
@ -7374,8 +7374,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc.
** ^(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".
** If that does not work, it constructs a name "sqlite3_X_init" where the
** X is consists of the lower-case equivalent of all ASCII alphabetic
** If that does not work, it constructs a name "sqlite3_X_init" where
** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^
** ^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
** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the
** entry point where as follows:
** entry point were as follows:
**
** <blockquote><pre>
** &nbsp; int xEntryPoint(
@ -7610,7 +7610,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** 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,
** when the omit flag is true there is no guarantee that the constraint will
** 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
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [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
** return at most one row.
**
@ -7777,7 +7777,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through
** 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
** 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
** 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()]
** 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:
** <ul>
@ -8062,7 +8062,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
**
** ^Returns the size in bytes of the BLOB accessible via 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.
**
** 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
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** 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:
**
** <ul>
@ -8445,7 +8445,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection
** 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
** when the [threading mode] is Serialized.
** ^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
**
** 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,
** 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
** 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
** [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
** 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
@ -8822,7 +8822,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The
** 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
** 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>
** <dd>This parameter returns the number of malloc attempts that were
** 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]]
** ^(<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
** memory requested being larger than the lookaside slot size.
** 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]]
** ^(<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
** memory already being in use.
** 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>
** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<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
** connections.)^ In other words, if none of the pager caches associated
** 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
** 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>
** <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
** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <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
** cache overflowing. Transactions are more efficient if they are written
** 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.
** </dd>
**
@ -9465,7 +9467,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically
** 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
** 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().
**
** ^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.
** ^If an out-of-memory condition or IO error occurred during any prior
** 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
**
** ^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
** 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

View file

@ -37,6 +37,7 @@ const QVector<QString> icons{
AyuAssets::CHIBI2_ICON,
AyuAssets::EXTERA2_ICON,
};
std::unordered_map<QString, QImage> cachedIcons;
const auto rows = static_cast<int>(icons.size()) / 4 + std::min(1, static_cast<int>(icons.size()) % 4);
@ -83,6 +84,9 @@ IconPicker::IconPicker(QWidget *parent)
setMinimumSize(st::boxWidth, (st::cpIconSize + st::cpPadding) * rows - st::cpPadding);
}
IconPicker::~IconPicker() {
cachedIcons.clear();
}
void IconPicker::paintEvent(QPaintEvent *e) {
Painter p(this);
PainterHighQualityEnabler hq(p);
@ -98,10 +102,16 @@ void IconPicker::paintEvent(QPaintEvent *e) {
if (iconName.isEmpty()) {
continue;
}
auto icon = AyuAssets::loadPreview(iconName)
.scaled(st::cpIconSize, st::cpIconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage icon;
if (const auto cached = cachedIcons.find(iconName); cached != cachedIcons.end()) {
icon = cached->second;
} else {
icon = cachedIcons[iconName] = AyuAssets::loadPreview(iconName).scaled(
st::cpIconSize,
st::cpIconSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
auto opacity = 0.0f;
if (iconName == wasSelected) {
opacity = 1.0f - animation.value(1.0f);

View file

@ -13,6 +13,7 @@ class IconPicker : public Ui::RpWidget
{
public:
IconPicker(QWidget *parent);
~IconPicker();
protected:
void paintEvent(QPaintEvent *e) override;

View file

@ -9,6 +9,7 @@
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "ayu/utils/ayu_mapper.h"
#include "ayu/ui/message_history/history_inner.h"
#include "base/unixtime.h"
#include "core/application.h"
@ -118,7 +119,10 @@ void GenerateItems(
};
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

View file

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

View file

@ -6,9 +6,14 @@
// Copyright @Radolyn, 2025
#include "ayu_mapper.h"
#include "apiwrap.h"
#include "api/api_text_entities.h"
#include "history/history.h"
#include "history/history_item.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 {
@ -39,17 +44,56 @@ constexpr auto kMessageFlagHasTTL = 0x02000000;
constexpr auto kMessageFlagInvertMedia = 0x08000000;
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) {
if (item->emptyText()) {
return std::make_pair("", std::vector<char>());
}
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);
}
MTPVector<MTPMessageEntity> deserializeTextWithEntities(std::vector<char> serialized) {
return deserializeObject<MTPVector<MTPMessageEntity>>(serialized);
}
int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item) {
int flags = 0;

View file

@ -8,7 +8,14 @@
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);
[[nodiscard]] MTPVector<MTPMessageEntity> deserializeTextWithEntities(std::vector<char> serialized);
int mapItemFlagsToMTPFlags(not_null<HistoryItem*> item);
} // namespace AyuMapper

View file

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

View file

@ -320,6 +320,22 @@ QString formatDateTime(const QDateTime &date) {
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) {
if (!message->media()) {
return -1;

View file

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

View file

@ -111,6 +111,13 @@ void AddProxyFromClipboard(
QGuiApplication::clipboard()->text());
const auto isSingle = maybeUrls.size() == 1;
enum class Result {
Success,
Failed,
Unsupported,
Invalid,
};
const auto proceedUrl = [=](const auto &local) {
const auto command = base::StringViewMid(
local,
@ -146,6 +153,11 @@ void AddProxyFromClipboard(
match->captured(1),
qthelp::UrlParamNameTransform::ToLower);
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 toast = (contains
? tr::lng_proxy_add_from_clipboard_existing_toast
@ -158,19 +170,29 @@ void AddProxyFromClipboard(
}
break;
}
return true;
return Result::Success;
}
return false;
return Result::Failed;
};
auto success = false;
auto success = Result::Failed;
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) {
show->showToast(
tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
if (success != Result::Success) {
if (success == Result::Failed) {
show->showToast(
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) {
auto ownedWrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner));
const auto peer = items.front()->history()->peer;
auto ownedWrap = peer->isMonoforum()
? nullptr
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner));
auto computeRestrictions = Fn<ChatRestrictions()>();
const auto wrap = ownedWrap.data();
Ui::AddSkip(inner);
Ui::AddSkip(inner);
@ -371,7 +376,9 @@ void CreateModerateMessagesBox(
object_ptr<Ui::Checkbox>(
box,
rpl::conditional(
ownedWrap->toggledValue(),
(ownedWrap
? ownedWrap->toggledValue()
: rpl::single(false) | rpl::type_erased()),
tr::lng_restrict_user(
lt_count,
rpl::single(participants.size()) | tr::to_count()),
@ -390,136 +397,141 @@ void CreateModerateMessagesBox(
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto wrap = inner->add(std::move(ownedWrap));
const auto container = wrap->entity();
wrap->toggle(false, anim::type::instant);
if (ownedWrap) {
inner->add(std::move(ownedWrap));
const auto session = &participants.front()->session();
const auto emojiMargin = QMargins(
-st::moderateBoxExpandInnerSkip,
-st::moderateBoxExpandInnerSkip / 2,
0,
0);
const auto emojiUp = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIcon,
emojiMargin,
false));
const auto emojiDown = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIconDown,
emojiMargin,
false));
const auto container = wrap->entity();
wrap->toggle(false, anim::type::instant);
auto label = object_ptr<Ui::FlatLabel>(
inner,
QString(),
st::moderateBoxDividerLabel);
const auto raw = label.data();
const auto session = &participants.front()->session();
const auto emojiMargin = QMargins(
-st::moderateBoxExpandInnerSkip,
-st::moderateBoxExpandInnerSkip / 2,
0,
0);
const auto emojiUp = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIcon,
emojiMargin,
false));
const auto emojiDown = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIconDown,
emojiMargin,
false));
auto &lifetime = wrap->lifetime();
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
label->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
}
wrap->toggle(!wrap->toggled(), anim::type::normal);
{
inner->heightValue() | rpl::start_with_next([=] {
if (!wrap->animating()) {
scrollLifetime->destroy();
Ui::PostponeCall(crl::guard(box, [=] {
auto label = object_ptr<Ui::FlatLabel>(
inner,
QString(),
st::moderateBoxDividerLabel);
const auto raw = label.data();
auto &lifetime = wrap->lifetime();
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
label->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
}
wrap->toggle(!wrap->toggled(), anim::type::normal);
{
inner->heightValue() | rpl::start_with_next([=] {
if (!wrap->animating()) {
scrollLifetime->destroy();
Ui::PostponeCall(crl::guard(box, [=] {
box->scrollToY(std::numeric_limits<int>::max());
}));
} else {
box->scrollToY(std::numeric_limits<int>::max());
}));
} else {
box->scrollToY(std::numeric_limits<int>::max());
}
}, *scrollLifetime);
}
return true;
});
wrap->toggledValue(
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
return ((toggled && isSingle)
? tr::lng_restrict_user_part
: (toggled && !isSingle)
? tr::lng_restrict_users_part
: isSingle
? tr::lng_restrict_user_full
: tr::lng_restrict_users_full)(
lt_emoji,
rpl::single(toggled ? emojiUp : emojiDown),
Ui::Text::WithEntities);
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
Core::TextContext({ .session = session }));
}, label->lifetime());
}
}, *scrollLifetime);
}
return true;
});
wrap->toggledValue(
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
return ((toggled && isSingle)
? tr::lng_restrict_user_part
: (toggled && !isSingle)
? tr::lng_restrict_users_part
: isSingle
? tr::lng_restrict_user_full
: tr::lng_restrict_users_full)(
lt_emoji,
rpl::single(toggled ? emojiUp : emojiDown),
Ui::Text::WithEntities);
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
Core::TextContext({ .session = session }));
}, label->lifetime());
Ui::AddSkip(inner);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(label),
st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom));
Ui::AddSkip(inner);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(label),
st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom));
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
const auto peer = items.front()->history()->peer;
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareFlags = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
{
const auto disabled = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
result.emplace(
disabled,
tr::lng_rights_restriction_for_all(tr::now));
}
return result;
}();
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareFlags = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
{
const auto disabled = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
result.emplace(
disabled,
tr::lng_rights_restriction_for_all(tr::now));
}
return result;
}();
Ui::AddSubsectionTitle(
inner,
rpl::conditional(
rpl::single(isSingle),
tr::lng_restrict_users_part_single_header(),
tr::lng_restrict_users_part_header(
lt_count,
rpl::single(participants.size()) | tr::to_count())));
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
box,
prepareFlags,
disabledMessages,
{ .isForum = peer->isForum() });
std::move(changes) | rpl::start_with_next([=] {
ban->setChecked(true);
}, ban->lifetime());
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
container->add(std::move(checkboxes));
Ui::AddSubsectionTitle(
inner,
rpl::conditional(
rpl::single(isSingle),
tr::lng_restrict_users_part_single_header(),
tr::lng_restrict_users_part_header(
lt_count,
rpl::single(participants.size()) | tr::to_count())));
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
box,
prepareFlags,
disabledMessages,
{ .isForum = peer->isForum() });
computeRestrictions = getRestrictions;
std::move(changes) | rpl::start_with_next([=] {
ban->setChecked(true);
}, ban->lifetime());
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
container->add(std::move(checkboxes));
}
// Handle confirmation manually.
confirms->events() | rpl::start_with_next([=] {
if (ban->checked() && controller->collectRequests) {
const auto kick = !wrap->toggled();
const auto restrictions = getRestrictions();
const auto kick = !wrap || !wrap->toggled();
const auto restrictions = computeRestrictions
? computeRestrictions()
: ChatRestrictions();
const auto request = [=](
not_null<PeerData*> peer,
not_null<ChannelData*> channel) {
@ -532,10 +544,15 @@ void CreateModerateMessagesBox(
nullptr,
nullptr);
} else {
channel->session().api().chatParticipants().kick(
channel,
peer,
{ channel->restrictions(), 0 });
const auto block = channel->isMonoforum()
? channel->monoforumBroadcast()
: channel.get();
if (block) {
block->session().api().chatParticipants().kick(
block,
peer,
{ block->restrictions(), 0 });
}
}
};
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 kResaleGiftsPerPage = 50;
constexpr auto kFiltersCount = 4;
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
using namespace HistoryView;
using namespace Info::PeerGifts;
@ -220,6 +221,33 @@ struct GiftDetails {
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 {
public:
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(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
@ -4422,6 +4499,132 @@ void UpdateGiftSellPrice(
}).send();
}
void UniqueGiftSellBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId,
int price,
Settings::GiftWearBoxStyleOverride st) {
box->setTitle(tr::lng_gift_sell_title());
box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {
box->closeBox();
});
const auto priceNow = unique->starsForResale;
const auto name = Data::UniqueGiftName(*unique);
const auto slug = unique->slug;
const auto session = &show->session();
AddSubsectionTitle(
box->verticalLayout(),
tr::lng_gift_sell_placeholder(),
(st::boxRowPadding - QMargins(
st::defaultSubsectionTitlePadding.left(),
0,
st::defaultSubsectionTitlePadding.right(),
st::defaultSubsectionTitlePadding.bottom())));
const auto &appConfig = session->appConfig();
const auto limit = appConfig.giftResalePriceMax();
const auto minimal = appConfig.giftResalePriceMin();
const auto thousandths = appConfig.giftResaleReceiveThousandths();
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
wrap,
st::editTagField,
rpl::single(QString()),
QString::number(priceNow ? priceNow : price ? price : minimal),
limit);
const auto field = owned.data();
wrap->widthValue() | rpl::start_with_next([=](int width) {
field->move(0, 0);
field->resize(width, field->height());
wrap->resize(width, field->height());
}, wrap->lifetime());
field->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(field);
st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());
}, field->lifetime());
field->selectAll();
box->setFocusCallback([=] {
field->setFocusFast();
});
const auto errors = box->lifetime().make_state<
rpl::event_stream<>
>();
auto goods = rpl::merge(
rpl::single(rpl::empty) | rpl::map_to(true),
base::qt_signal_producer(
field,
&Ui::NumberInput::changed
) | rpl::map_to(true),
errors->events() | rpl::map_to(false)
) | rpl::start_spawning(box->lifetime());
auto text = rpl::duplicate(goods) | rpl::map([=](bool good) {
const auto value = field->getLastText().toInt();
const auto receive = (int64(value) * thousandths) / 1000;
return !good
? tr::lng_gift_sell_min_price(
tr::now,
lt_count,
minimal,
Ui::Text::RichLangValue)
: (value >= minimal)
? tr::lng_gift_sell_amount(
tr::now,
lt_count,
receive,
Ui::Text::RichLangValue)
: tr::lng_gift_sell_about(
tr::now,
lt_percent,
TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) },
Ui::Text::RichLangValue);
});
const auto details = box->addRow(object_ptr<Ui::FlatLabel>(
box,
std::move(text) | rpl::after_next([=] {
box->verticalLayout()->resizeToWidth(box->width());
}),
st::boxLabel));
Ui::AddSkip(box->verticalLayout());
rpl::duplicate(goods) | rpl::start_with_next([=](bool good) {
details->setTextColorOverride(
good ? st::windowSubTextFg->c : st::boxTextFgError->c);
}, details->lifetime());
QObject::connect(field, &NumberInput::submitted, [=] {
const auto count = field->getLastText().toInt();
if (count < minimal) {
field->showError();
errors->fire({});
return;
}
box->closeBox();
UpdateGiftSellPrice(show, unique, savedId, count);
});
const auto button = box->addButton(priceNow
? tr::lng_gift_sell_update()
: tr::lng_gift_sell_put(), [=] { field->submitted({}); });
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
}
void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
@ -4430,125 +4633,11 @@ void ShowUniqueGiftSellBox(
if (ShowResaleGiftLater(show, unique)) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_gift_sell_title());
box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {
box->closeBox();
});
const auto priceNow = unique->starsForResale;
const auto name = Data::UniqueGiftName(*unique);
const auto slug = unique->slug;
const auto session = &show->session();
AddSubsectionTitle(
box->verticalLayout(),
tr::lng_gift_sell_placeholder(),
(st::boxRowPadding - QMargins(
st::defaultSubsectionTitlePadding.left(),
0,
st::defaultSubsectionTitlePadding.right(),
st::defaultSubsectionTitlePadding.bottom())));
const auto &appConfig = session->appConfig();
const auto limit = appConfig.giftResalePriceMax();
const auto minimal = appConfig.giftResalePriceMin();
const auto thousandths = appConfig.giftResaleReceiveThousandths();
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
wrap,
st::editTagField,
rpl::single(QString()),
QString::number(priceNow ? priceNow : minimal),
limit);
const auto field = owned.data();
wrap->widthValue() | rpl::start_with_next([=](int width) {
field->move(0, 0);
field->resize(width, field->height());
wrap->resize(width, field->height());
}, wrap->lifetime());
field->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(field);
st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());
}, field->lifetime());
field->selectAll();
box->setFocusCallback([=] {
field->setFocusFast();
});
const auto errors = box->lifetime().make_state<
rpl::event_stream<>
>();
auto goods = rpl::merge(
rpl::single(rpl::empty) | rpl::map_to(true),
base::qt_signal_producer(
field,
&Ui::NumberInput::changed
) | rpl::map_to(true),
errors->events() | rpl::map_to(false)
) | rpl::start_spawning(box->lifetime());
auto text = rpl::duplicate(goods) | rpl::map([=](bool good) {
const auto value = field->getLastText().toInt();
const auto receive = (int64(value) * thousandths) / 1000;
return !good
? tr::lng_gift_sell_min_price(
tr::now,
lt_count,
minimal,
Ui::Text::RichLangValue)
: (value >= minimal)
? tr::lng_gift_sell_amount(
tr::now,
lt_count,
receive,
Ui::Text::RichLangValue)
: tr::lng_gift_sell_about(
tr::now,
lt_percent,
TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) },
Ui::Text::RichLangValue);
});
const auto details = box->addRow(object_ptr<Ui::FlatLabel>(
box,
std::move(text) | rpl::after_next([=] {
box->verticalLayout()->resizeToWidth(box->width());
}),
st::boxLabel));
Ui::AddSkip(box->verticalLayout());
rpl::duplicate(goods) | rpl::start_with_next([=](bool good) {
details->setTextColorOverride(
good ? st::windowSubTextFg->c : st::boxTextFgError->c);
}, details->lifetime());
QObject::connect(field, &NumberInput::submitted, [=] {
const auto count = field->getLastText().toInt();
if (count < minimal) {
field->showError();
errors->fire({});
return;
}
box->closeBox();
UpdateGiftSellPrice(show, unique, savedId, count);
});
const auto button = box->addButton(priceNow
? tr::lng_gift_sell_update()
: tr::lng_gift_sell_put(), [=] { field->submitted({}); });
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
}));
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) {

View file

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

View file

@ -768,12 +768,14 @@ void StickerSetBox::updateButtons() {
&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) {
const auto &settings = AyuSettings::getInstance();
const auto weak = Ui::MakeWeak(this);
const auto session = _session;
const auto innerId = _inner->setId() >> 32;
const auto setId = _inner->setId();
const auto innerId = setId >> 32;
(*menu)->addAction(
tr::ayu_MessageDetailsPackOwnerPC(tr::now),
@ -816,6 +818,26 @@ void StickerSetBox::updateButtons() {
});
},
&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()) {
@ -865,7 +887,7 @@ void StickerSetBox::updateButtons() {
: tr::lng_stickers_share_pack)(tr::now),
[=] { share(); closeBox(); },
&st::menuIconShare);
addPackOwner(menu);
addPackIdActions(menu);
(*menu)->popup(QCursor::pos());
return true;
});
@ -918,7 +940,7 @@ void StickerSetBox::updateButtons() {
archive,
&st::menuIconArchive);
}
addPackOwner(menu);
addPackIdActions(menu);
(*menu)->popup(QCursor::pos());
return true;
});
@ -1486,6 +1508,16 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
QGuiApplication::clipboard()->setMimeData(data.release());
}
}, &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) {
const auto document = _pack[index];

View file

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

View file

@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04;
constexpr auto kReactionsCountEmojiProperty = 0x05;
constexpr auto kDocumentFilenameTooltipProperty = 0x06;
constexpr auto kPhoneNumberLinkProperty = 0x07;
constexpr auto kTodoListItemIdProperty = 0x08;
namespace Ui {
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 AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5016003;
constexpr auto AppVersionStr = "5.16.3";
constexpr auto AppVersion = 5016004;
constexpr auto AppVersionStr = "5.16.4";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -32,8 +32,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
namespace {
constexpr auto kMs = crl::time(1000);
constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);
const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
[[nodiscard]] bool TooEarlyForRequest(crl::time received) {
return (received > 0) && (received + kRequestTimeLimit > crl::now());
}
@ -77,17 +80,21 @@ void SponsoredMessages::clear() {
void SponsoredMessages::clearOldRequests() {
const auto now = crl::now();
while (true) {
const auto i = ranges::find_if(_requests, [&](const auto &value) {
const auto &request = value.second;
return !request.requestId
&& (request.lastReceived + kRequestTimeLimit <= now);
});
if (i == end(_requests)) {
break;
const auto clear = [&](auto &requests) {
while (true) {
const auto i = ranges::find_if(requests, [&](const auto &value) {
const auto &request = value.second;
return !request.requestId
&& (request.lastReceived + kRequestTimeLimit <= now);
});
if (i == end(requests)) {
break;
}
requests.erase(i);
}
_requests.erase(i);
}
};
clear(_requests);
clear(_requestsForVideo);
}
SponsoredMessages::AppendResult SponsoredMessages::append(
@ -241,6 +248,16 @@ bool SponsoredMessages::canHaveFor(not_null<History*> history) const {
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 {
const auto &settings = AyuSettings::getInstance();
if (settings.disableAds) {
@ -291,6 +308,78 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
}).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(
not_null<History*> history,
const MTPmessages_sponsoredMessages &list) {
@ -306,12 +395,9 @@ void SponsoredMessages::parse(
_session->data().processChats(data.vchats());
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.received = crl::now();
for (const auto &message : messages) {
append(history, list, message);
}
if (const auto postsBetween = data.vposts_between()) {
list.postsBetween = postsBetween->v;
list.state = State::InjectToMiddle;
@ -320,10 +406,66 @@ void SponsoredMessages::parse(
? State::AppendToEnd
: State::AppendToTopBar;
}
for (const auto &message : messages) {
append([=] {
return &_data[history].entries;
}, history, message);
}
}, [](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(
not_null<History*> history,
not_null<Ui::RpWidget*> widget) {
@ -373,8 +515,8 @@ rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {
}
void SponsoredMessages::append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history,
List &list,
const MTPSponsoredMessage &message) {
const auto &data = message.data();
const auto randomId = data.vrandom_id().v;
@ -385,14 +527,14 @@ void SponsoredMessages::append(
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) {
mediaPhoto = history->owner().processPhoto(data);
mediaPhoto = _session->data().processPhoto(data);
}, [](const MTPDphotoEmpty &) {
});
}
}, [&](const MTPDmessageMediaDocument &media) {
if (const auto tlDocument = media.vdocument()) {
tlDocument->match([&](const MTPDdocument &data) {
const auto d = history->owner().processDocument(
const auto d = _session->data().processDocument(
data,
media.valt_documents());
if (d->isVideoFile()
@ -413,7 +555,7 @@ void SponsoredMessages::append(
.link = qs(data.vurl()),
.buttonText = qs(data.vbutton_text()),
.photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id
? _session->data().processPhoto(*data.vphoto())->id
: PhotoId(0),
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
@ -449,25 +591,24 @@ void SponsoredMessages::append(
.link = from.link,
.sponsorInfo = std::move(sponsorInfo),
.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({
.sponsored = std::move(sharedMessage),
});
auto &entry = list.entries.back();
const auto itemId = entry.itemFullId = FullMsgId(
const auto itemId = FullMsgId(
history->peer->id,
_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.
static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
const auto preloaded = [=] {
const auto i = _data.find(history);
if (i == end(_data)) {
return;
}
auto &entries = i->second.entries;
const auto j = ranges::find(entries, itemId, &Entry::itemFullId);
if (j == end(entries)) {
const auto list = entries();
const auto j = ranges::find(*list, itemId, &Entry::itemFullId);
if (j == end(*list)) {
return;
}
auto &entry = *j;
@ -565,7 +706,11 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
if (!entryPtr) {
return {};
}
const auto &data = entryPtr->sponsored;
return lookupDetails(entryPtr->sponsored);
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const SponsoredMessage &data) const {
return {
.info = Prepare(data),
.link = data.link,

View file

@ -67,10 +67,12 @@ struct SponsoredMessage {
QByteArray randomId;
SponsoredFrom from;
TextWithEntities textWithEntities;
History *history = nullptr;
not_null<History*> history;
QString link;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
};
struct SponsoredMessageDetails {
@ -92,6 +94,23 @@ struct SponsoredReportAction {
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 {
public:
enum class AppendResult {
@ -111,10 +130,18 @@ public:
~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const;
[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
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);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const;
[[nodiscard]] Details lookupDetails(
const Api::SponsoredSearchResult &data) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
@ -166,18 +193,35 @@ private:
int postsBetween = 0;
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 {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
struct RequestForVideo {
std::vector<Fn<void(SponsoredForVideo)>> callbacks;
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void parse(
not_null<History*> history,
const MTPmessages_sponsoredMessages &list);
void parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list);
void append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history,
List &list,
const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const;
@ -189,6 +233,9 @@ private:
base::flat_map<not_null<History*>, Request> _requests;
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::lifetime _lifetime;

View file

@ -72,7 +72,7 @@ namespace {
| (data.is_send_audios() ? Flag::SendMusic : Flag())
| (data.is_send_voices() ? Flag::SendVoiceMessages : 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_change_info() ? Flag::ChangeInfo : 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::SendVoiceMessages) ? Flag::f_send_voices : 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::ChangeInfo) ? Flag::f_change_info : Flag())
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())

View file

@ -92,7 +92,8 @@ MTPInputReplyTo ReplyToForMTP(
: Flag())
| (quoteEntities.v.isEmpty()
? 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.topicRootId),
(external
@ -103,7 +104,8 @@ MTPInputReplyTo ReplyToForMTP(
MTP_int(replyTo.quoteOffset),
(replyToMonoforumPeerId
? history->owner().peer(replyToMonoforumPeerId)->input
: MTPInputPeer()));
: MTPInputPeer()),
MTP_int(replyTo.todoItemId));
} else if (history->peer->amMonoforumAdmin()
&& 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);
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 {
FullMsgId messageId;
TextWithEntities quote;
@ -179,7 +192,11 @@ struct FullReplyTo {
MsgId topicRootId = 0;
PeerId monoforumPeerId = 0;
int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
}
[[nodiscard]] bool replying() const {
return messageId || (storyId && storyId.peer);
}

View file

@ -96,17 +96,6 @@ Thread *SavedMessages::activeSubsectionThread() const {
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() {
clear();
}
@ -458,6 +447,9 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {
if (ranges::contains(_lastSublists, not_null(raw))) {
reorderLastSublists();
}
if (_activeSubsectionSublist == raw) {
_activeSubsectionSublist = nullptr;
}
_sublistDestroyed.fire(raw);
session().changes().sublistUpdated(

View file

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

View file

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

View file

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

View file

@ -244,6 +244,17 @@ dialogsEmptyLabel: FlatLabel(defaultFlatLabel) {
align: align(top);
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 {
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_options.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/vertical_list.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
@ -58,8 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/options.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "settings/settings_common.h"
#include "storage/storage_account.h"
#include "apiwrap.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 "base/qt/qt_common_adapters.h"
#include "styles/style_dialogs.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h" // popupMenuExpandedSeparator
#include "styles/style_chat_helpers.h"
#include "styles/style_color_indices.h"
@ -3081,6 +3082,11 @@ void InnerWidget::clearSelection() {
}
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 text = all ? "Only one from chat" : "Show all messages";
menu->addAction(text, [=] {
@ -3091,9 +3097,11 @@ void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) {
void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) {
const auto folder = session().data().folderLoaded(Data::Folder::kId);
const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)
|| (_searchState.tab == ChatSearchTab::PublicPosts);
if (!folder
|| !folder->chatsList()->fullSize().current()
|| _searchState.inChat) {
|| (!globalSearch && _searchState.inChat)) {
return;
}
const auto skip = session().settings().skipArchiveInSearch();
@ -3263,16 +3271,13 @@ void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) {
refresh();
});
Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu),
_controller->uiShow(),
Menu::SponsoredPhrases::Search,
session().sponsoredMessages().lookupDetails(entry->sponsored->data),
session().sponsoredMessages().createReportCallback(
entry->sponsored->data.randomId,
remove),
false,
false);
remove));
QObject::connect(_menu.get(), &QObject::destroyed, [=] {
if (_peerSearchMenu >= 0
&& _peerSearchMenu < _peerSearchResults.size()) {
@ -3811,7 +3816,7 @@ void InnerWidget::itemRemoved(not_null<const HistoryItem*> item) {
}
bool InnerWidget::uniqueSearchResults() const {
return _controller->uniqueChatsInSearchResults();
return _controller->uniqueChatsInSearchResults(_searchState);
}
bool InnerWidget::hasHistoryInResults(not_null<History*> history) const {
@ -3869,7 +3874,8 @@ void InnerWidget::searchReceived(
? _searchState.inChat
: Key(_openedForum->history());
if (inject
&& (!_searchState.inChat
&& (globalSearch
|| !_searchState.inChat
|| inject->history() == _searchState.inChat.history())) {
Assert(_searchResults.empty());
Assert(!toPreview);
@ -4082,9 +4088,18 @@ void InnerWidget::refreshEmpty() {
if (state == EmptyState::None) {
_emptyState = state;
_empty.destroy();
_emptyList.destroy();
_emptyButton.destroy();
return;
} else if (_emptyState == state) {
_empty->setVisible(_state == WidgetState::Default);
if (_emptyList) {
_emptyList->setVisible(_state == WidgetState::Default);
_empty->setVisible(!_emptyList->isVisible());
}
if (_emptyButton) {
_emptyButton->setVisible(_state == WidgetState::Default);
}
return;
}
_emptyState = state;
@ -4115,7 +4130,6 @@ void InnerWidget::refreshEmpty() {
return result;
});
_empty.create(this, std::move(full), st::dialogsEmptyLabel);
resizeEmpty();
_empty->overrideLinkClickHandler([=] {
if (_emptyState == EmptyState::NoContacts) {
_controller->showAddContact();
@ -4127,6 +4141,58 @@ void InnerWidget::refreshEmpty() {
}
});
_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() {
@ -4135,6 +4201,13 @@ void InnerWidget::resizeEmpty() {
_empty->resizeToWidth(width() - 2 * skip);
_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) {
_searchEmpty->resizeToWidth(width());
_searchEmpty->move(0, searchedOffset());

View file

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

View file

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

View file

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

View file

@ -1167,8 +1167,15 @@ Chat ParseChat(const MTPChat &data) {
result.colorIndex = (color && color->data().vcolor())
? color->data().vcolor()->v
: PeerColorIndex(result.bareId);
result.isMonoforum = data.is_monoforum();
result.isBroadcast = data.is_broadcast();
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());
if (const auto username = data.vusername()) {
result.username = ParseString(*username);
@ -1188,15 +1195,6 @@ Chat ParseChat(const MTPChat &data) {
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 {
return firstName.isEmpty()
? (lastName.isEmpty()
@ -1273,6 +1271,20 @@ std::map<PeerId, Peer> ParsePeersLists(
auto parsed = ParseChat(chat);
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;
}
@ -2191,7 +2203,13 @@ const DialogInfo *DialogsInfo::item(int index) const {
DialogInfo::Type DialogTypeFromChat(const Chat &chat) {
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
? Type::PrivateChannel
: chat.isSupergroup
@ -2252,6 +2270,11 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) {
info.migratedToChannelId = peer.chat()
? peer.chat()->migratedToChannelId
: 0;
info.isMonoforum = peer.chat()
&& peer.chat()->isMonoforum;
info.monoforumBroadcastInput = peer.chat()
? peer.chat()->monoforumBroadcastInput
: MTPInputPeer(MTP_inputPeerEmpty());
}
info.topMessageId = fields.vtop_message().v;
const auto messageIt = messages.find(MessageId{
@ -2290,6 +2313,10 @@ DialogInfo DialogInfoFromChat(const Chat &data) {
result.topMessageId = 0;
result.type = DialogTypeFromChat(data);
result.migratedToChannelId = data.migratedToChannelId;
result.isMonoforum = data.isMonoforum;
if (data.isMonoforumAdmin) {
result.monoforumBroadcastInput = data.monoforumBroadcastInput;
}
return result;
}
@ -2424,7 +2451,8 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) {
}
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -390,6 +390,7 @@ ReplyFields ReplyFieldsFromMTP(
= data.vreply_to_top_id().value_or(result.messageId.bare);
result.topicPost = data.is_forum_topic() ? 1 : 0;
}
result.todoItemId = data.vtodo_item_id().value_or_empty();
if (const auto header = data.vreply_from()) {
const auto &data = header->data();
result.externalPostAuthor
@ -704,7 +705,8 @@ auto ReplyMarkupClickHandler::getUrlButton() const
-> const HistoryMessageMarkupButton* {
if (const auto button = getButton()) {
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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_message_reactions.h"
@ -1360,9 +1361,18 @@ void Element::validateText() {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
if (!done->completed.empty() && !done->incompleted.empty()) {
const auto todoItemId = (done->incompleted.size() == 1)
? done->incompleted.front()
: 0;
setServicePreMessage(
item->composeTodoIncompleted(done),
done->lnk);
JumpToMessageClickHandler(
(done->peerId
? history()->owner().peer(done->peerId)
: history()->peer),
done->msgId,
item->fullId(),
{ .todoItemId = todoItemId }));
} else {
setServicePreMessage({});
}
@ -2205,7 +2215,7 @@ SelectedQuote Element::FindSelectedQuote(
++i;
}
}
return { item, result, modified.from, overflown };
return { item, { result, modified.from }, overflown };
}
TextSelection Element::FindSelectionFromQuote(
@ -2213,17 +2223,18 @@ TextSelection Element::FindSelectionFromQuote(
const SelectedQuote &quote) {
Expects(quote.item != nullptr);
if (quote.text.empty()) {
const auto &rich = quote.highlight.quote;
if (rich.empty()) {
return {};
}
const auto &original = quote.item->originalText();
if (quote.offset == kSearchQueryOffsetHint) {
if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {
return ApplyModificationsFrom(
FindSearchQueryHighlight(original.text, quote.text.text),
FindSearchQueryHighlight(original.text, rich.text),
text);
}
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) {
return TextSelection{
uint16(offset),
@ -2234,7 +2245,7 @@ TextSelection Element::FindSelectionFromQuote(
if (offset > length - qlength) {
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();
};
const auto findOneBefore = [&](int offset) {
@ -2243,7 +2254,7 @@ TextSelection Element::FindSelectionFromQuote(
}
const auto end = std::min(offset + qlength - 1, length);
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();
};
const auto findAfter = [&](int offset) {
@ -2281,7 +2292,7 @@ TextSelection Element::FindSelectionFromQuote(
? before
: after;
};
auto result = findTwoWays(quote.offset);
auto result = findTwoWays(quote.highlight.quoteOffset);
if (result.empty()) {
return {};
}
@ -2468,6 +2479,70 @@ int FindViewY(not_null<Element*> view, uint16 symbol, int yfrom) {
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) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {

View file

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

View file

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

View file

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

View file

@ -490,6 +490,8 @@ void Message::initPaidInformation() {
refreshSuggestedInfo(item, suggest, replyData);
}
return;
} else if (!item->history()->peer->isUser()) {
return;
}
const auto media = this->media();
const auto mine = PaidInformation{
@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote(
const SelectedQuote &quote) const {
Expects(quote.item != nullptr);
if (quote.text.empty()) {
if (quote.highlight.quote.empty()) {
return {};
}
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_session.h"
#include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h"
#include "history/view/history_view_item_preview.h"
#include "history/history.h"
@ -42,6 +43,85 @@ namespace {
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
void ValidateBackgroundEmoji(
@ -197,6 +277,22 @@ void Reply::update(
const auto item = view->data();
const auto &fields = data->fields();
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 externalMedia = fields.externalMedia.get();
if (!_externalSender) {
@ -214,7 +310,6 @@ void Reply::update(
_hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
? (message->originalHiddenSenderInfo()->colorIndex + 1)
: 0;
const auto hasPreview = (story && story->hasReplyPreview())
|| (message
&& message->media()
@ -229,8 +324,13 @@ void Reply::update(
&& !fields.quote.empty();
_hasQuoteIcon = hasQuoteIcon ? 1 : 0;
const auto session = &view->history()->session();
const auto text = (!_displaying && data->unavailable())
? TextWithEntities()
: task
? Ui::Text::Colorized(task->completionDate
? TaskDoneIcon(session)
: TaskIcon(session)).append(task->text)
: (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText()
: !fields.quote.empty()
@ -288,10 +388,11 @@ void Reply::setLinkFrom(
const auto &fields = data->fields();
const auto externalChannelId = peerToChannel(fields.externalPeerId);
const auto messageId = fields.messageId;
const auto quote = fields.manualQuote
? fields.quote
: TextWithEntities();
const auto quoteOffset = fields.quoteOffset;
const auto highlight = MessageHighlightId{
.quote = fields.manualQuote ? fields.quote : TextWithEntities(),
.quoteOffset = int(fields.quoteOffset),
.todoItemId = fields.todoItemId,
};
const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
@ -314,8 +415,7 @@ void Reply::setLinkFrom(
channel,
messageId,
returnToId,
quote,
quoteOffset
highlight
)->onClick(context);
} else {
controller->showPeerInfo(channel);
@ -336,7 +436,7 @@ void Reply::setLinkFrom(
const auto message = data->resolvedMessage.get();
const auto story = data->resolvedStory.get();
_link = message
? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset)
? JumpToMessageClickHandler(message, returnToId, highlight)
: story
? JumpToStoryClickHandler(story)
: (data->external()
@ -873,18 +973,28 @@ TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName(
not_null<History*> history,
not_null<HistoryItem*> to,
bool quote) {
const FullReplyTo &replyTo) {
const auto sender = [&] {
if (const auto from = to->displayFrom()) {
return not_null(from);
}
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 displayAsExternal = (to->history() != history);
const auto groupNameAdded = displayAsExternal
&& (toPeer != sender)
&& (toPeer->isChat() || toPeer->isMegagroup());
const auto quote = replyTo && !replyTo.quote.empty();
const auto shorten = groupNameAdded || quote;
auto nameFull = TextWithEntities();

View file

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

View file

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

View file

@ -463,17 +463,16 @@ QSize Service::performCountCurrentSize(int newWidth) {
const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed();
auto contentWidth = newWidth;
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
}
contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins
if (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()) {
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
}
contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins
if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {
contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;
}
auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0);
newHeight += (contentWidth >= maxWidth())
? minHeight()

View file

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

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) {
const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id);
}));
result->setProperty(kTodoListItemIdProperty, id);
return result;
}
void TodoList::startToggleAnimation(Task &task) {
@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) {
if (i == end(_tasks)) {
return;
}
const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) {
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);
repaint();
@ -467,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw,
width(),
context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) {
heavy = true;
} else if (!task.userpic.null()) {
@ -561,6 +577,33 @@ int TodoList::paintTask(
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(
Painter &p,
const Task &task,

View file

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

View file

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

View file

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

View file

@ -731,8 +731,8 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
manageGroupReactions: IconButton(defaultIconButton) {
width: 24px;
height: 36px;
icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }};
iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }};
icon: icon{{ "menu/add", historyComposeIconFg }};
iconOver: icon{{ "menu/add", historyComposeIconFgOver }};
}
manageGroupReactionsField: InputField(defaultInputField) {
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 "apiwrap.h"
#include "boxes/star_gift_box.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_session.h"
@ -380,6 +381,7 @@ void InnerWidget::loadMore() {
_entries.clear();
}
_entries.reserve(_entries.size() + data.vgifts().v.size());
auto hasUnique = false;
for (const auto &gift : data.vgifts().v) {
if (auto parsed = Api::FromTL(_peer, gift)) {
auto descriptor = DescriptorForGift(_peer, *parsed);
@ -387,10 +389,15 @@ void InnerWidget::loadMore() {
.gift = std::move(*parsed),
.descriptor = std::move(descriptor),
});
hasUnique = (parsed->info.unique != nullptr);
}
}
refreshButtons();
refreshAbout();
if (hasUnique) {
Ui::PreloadUniqueGiftResellPrices(&_peer->session());
}
}).fail([=] {
_loadMoreRequestId = 0;
_allLoaded = true;

View file

@ -949,7 +949,20 @@ QString FormatCountDecimal(int64 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) {

View file

@ -1087,3 +1087,34 @@ mediaviewSponsoredButton: RoundButton(defaultActiveButton) {
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_overlay_raster.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_view.h"
#include "media/streaming/media_streaming_document.h"
@ -339,6 +340,7 @@ struct OverlayWidget::Streamed {
Streaming::Instance instance;
std::unique_ptr<PlaybackControls> controls;
std::unique_ptr<PlaybackSponsored> sponsored;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
bool ready = false;
@ -1617,7 +1619,11 @@ void OverlayWidget::fillContextMenuActions(
if (const auto window = findWindow()) {
const auto show = window->uiShow();
const auto fullId = _message->fullId();
Menu::FillSponsored(_body, addAction, show, fullId, true);
Menu::FillSponsored(
addAction,
show,
fullId,
{ .dark = true, .skipInfo = true });
}
return;
}
@ -3981,7 +3987,11 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {
&& !_streamed->instance.player().finished())) {
startStreamingPlayer(startStreaming);
} else {
_streamed->ready = _streamed->instance.player().ready();
if (_streamed->instance.player().ready()) {
markStreamedReady();
} else {
_streamed->ready = false;
}
updatePlaybackState();
}
return true;
@ -3994,7 +4004,7 @@ void OverlayWidget::startStreamingPlayer(
const auto &player = _streamed->instance.player();
if (player.playing()) {
if (!_streamed->withSound) {
_streamed->ready = true;
markStreamedReady();
return;
}
_pip = nullptr;
@ -4012,6 +4022,18 @@ void OverlayWidget::startStreamingPlayer(
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() {
Expects(_photo || _document);
@ -4083,7 +4105,7 @@ void OverlayWidget::initStreamingThumbnail() {
}
void OverlayWidget::streamingReady(Streaming::Information &&info) {
_streamed->ready = true;
markStreamedReady();
if (videoShown()) {
applyVideoSize();
_streamedQualityChangeFrame = QImage();
@ -4105,6 +4127,7 @@ void OverlayWidget::applyVideoSize() {
bool OverlayWidget::createStreamingObjects() {
Expects(_photo || _document);
Expects(!_streamed);
const auto origin = fileOrigin();
const auto callback = [=] { waitingAnimationCallback(); };
@ -4137,6 +4160,18 @@ bool OverlayWidget::createStreamingObjects() {
_body,
static_cast<PlaybackControls::Delegate*>(this));
_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();
}
return true;

View file

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

View file

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

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

View file

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

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