Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
|
1dfe68e9f3 | ||
|
64b824671a | ||
|
4940d85b29 | ||
|
157039946f | ||
|
09083beed0 | ||
|
8aa6c4582c | ||
|
aeb610ff70 | ||
|
1586386d6c | ||
|
2eee51f891 | ||
|
adf46969ca | ||
|
bd6ee188f8 | ||
|
57893bdbf8 | ||
|
7a37ab1b25 | ||
|
72618f98d1 | ||
|
d98e6fd51e | ||
|
608bcf7d42 | ||
|
06d8d14ad7 | ||
|
aef400db0f | ||
|
cdfdaf9f5f | ||
|
5270f155ff | ||
|
0514f13af0 | ||
|
e6a6763228 | ||
|
38c74bf2cf | ||
|
a770e47575 | ||
|
c998352ab7 | ||
|
3683fa3814 | ||
|
e62881e08b | ||
|
01e313e56b | ||
|
2dd5f80468 | ||
|
275fb3e96a | ||
|
3463916b9b | ||
|
a20de2515a | ||
|
79ea992a0f | ||
|
0132436dc8 | ||
|
a285c1abec | ||
|
1c41e01f0d | ||
|
f2e53ea490 | ||
|
bff86b90fb | ||
|
b5c9b6f552 | ||
|
23f5102f1b | ||
|
bf51e911b8 | ||
|
4039d7ab71 | ||
|
77a09a0e59 | ||
|
154c777788 | ||
|
628c36c87d | ||
|
a746b7abcf | ||
|
64184e6c90 | ||
|
ecc955d2ce | ||
|
f7e1b2c70c | ||
|
284cbda7c0 | ||
|
e5ca9e4c39 | ||
|
02aaa71e78 | ||
|
f56b5ea03e | ||
|
6afd4dcdd1 | ||
|
52bb189996 | ||
|
8ff6f9af45 |
|
@ -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
|
||||
|
|
BIN
Telegram/Resources/animations/no_chats.tgs
Normal file
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 899 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -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 |
|
@ -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.";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -47,6 +47,6 @@ void forwardMessages(
|
|||
not_null<Main::Session*> session,
|
||||
const Api::SendAction &action,
|
||||
bool forwardState,
|
||||
Data::ResolvedForwardDraft draft);
|
||||
const Data::ResolvedForwardDraft &draft);
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
** 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 ){
|
||||
|
|
|
@ -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>
|
||||
** 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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,6 +13,7 @@ class IconPicker : public Ui::RpWidget
|
|||
{
|
||||
public:
|
||||
IconPicker(QWidget *parent);
|
||||
~IconPicker();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 = [=](
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -84,8 +84,6 @@ public:
|
|||
void saveActiveSubsectionThread(not_null<Thread*> thread);
|
||||
Thread *activeSubsectionThread() const;
|
||||
|
||||
[[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -65,6 +65,7 @@ private:
|
|||
struct Highlight {
|
||||
FullMsgId itemId;
|
||||
TextSelection part;
|
||||
int todoListId = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return itemId.operator bool();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 "e, 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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -644,8 +644,13 @@ bool AddReplyToMessageAction(
|
|||
return false;
|
||||
}
|
||||
|
||||
const auto todoListTaskId = request.link
|
||||
? request.link->property(kTodoListItemIdProperty).toInt()
|
||||
: 0;
|
||||
const auto "e = 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 "e) {
|
||||
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()) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 "e) const {
|
||||
Expects(quote.item != nullptr);
|
||||
|
||||
if (quote.text.empty()) {
|
||||
if (quote.highlight.quote.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto item = quote.item;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|