From 5d1aa1076811bb973680125c4a6cb7346a57846c Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Mon, 19 Feb 2024 05:27:09 +0400 Subject: [PATCH 001/108] Remove no longer needed boost-program-options from snap Looks like cppgir has stopped to use it during some of the updates --- snap/snapcraft.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c36f7e512..0996a2797 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -96,7 +96,6 @@ parts: - python3 - libasound2-dev - libavif-dev - - libboost-program-options1.74-dev - libboost-regex1.74-dev - libfmt-dev - libgirepository1.0-dev From 778ab70b72ad7eca8a6701f1a8fa00eaab45d88e Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:07:08 +0100 Subject: [PATCH 002/108] Fix libvpx build on VS 17.8+ use with https://github.com/desktop-app/patches/pull/182 --- Telegram/build/prepare/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index f53417ea3..d60c818e3 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -418,7 +418,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 94be868240 + git checkout bed08b53a3 """) stage('msys64', """ From ec427ad45df99365aa09f0a7b9412a4bb73ba42f Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:12:13 +0100 Subject: [PATCH 003/108] Use TOOLCHAIN variable name --- Telegram/build/prepare/prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index d60c818e3..fce761f88 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -855,9 +855,9 @@ win: SET MSYS2_PATH_TYPE=inherit if "%X8664%" equ "x64" ( - SET "TARGET=x86_64-win64-vs17" + SET "TOOLCHAIN=x86_64-win64-vs17" ) else ( - SET "TARGET=x86-win32-vs17" + SET "TOOLCHAIN=x86-win32-vs17" ) depends:patches/build_libvpx_win.sh From a8b5061003b0c2231bfcebc2ef44d9fd5c3ff3ed Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 1 Mar 2024 10:26:05 +0400 Subject: [PATCH 004/108] Fix a std::clamp assertion --- Telegram/SourceFiles/history/view/media/history_view_photo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index e5996d1bf..2532509a7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -232,7 +232,7 @@ QSize Photo::countCurrentSize(int newWidth) { const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize); const auto minWidth = std::clamp( _parent->minWidthForMedia(), - (_parent->hasBubble() + qMin(thumbMaxWidth, _parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), thumbMaxWidth); From da047edbc5ca747ebb9947d8943605f944a390e0 Mon Sep 17 00:00:00 2001 From: GitHub Action <action@github.com> Date: Fri, 1 Mar 2024 00:25:21 +0000 Subject: [PATCH 005/108] Update User-Agent for DNS to Chrome 122.0.0.0. --- .../SourceFiles/mtproto/details/mtproto_domain_resolver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index b058b9bb8..ea0a53f30 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/121.0.6167.85 Safari/537.36"); + "Chrome/122.0.0.0 Safari/537.36"); return kResult; } From 95b4fc021622e75146ced592e87bbd3b2f3ba73e Mon Sep 17 00:00:00 2001 From: xmdn <72883689+xmdnx@users.noreply.github.com> Date: Sun, 25 Feb 2024 15:00:35 +0300 Subject: [PATCH 006/108] use modern installer style --- Telegram/build/setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/setup.iss b/Telegram/build/setup.iss index f5975b097..8b0715581 100644 --- a/Telegram/build/setup.iss +++ b/Telegram/build/setup.iss @@ -33,6 +33,7 @@ VersionInfoVersion={#MyAppVersion}.0 CloseApplications=force DisableDirPage=no DisableProgramGroupPage=no +WizardStyle=modern #if MyBuildTarget == "win64" ArchitecturesAllowed="x64 arm64" From 50f51d074721cda4e1cd8466a31685a6122be4fb Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:47:32 +0100 Subject: [PATCH 007/108] update new script location in qt repo --- Telegram/build/prepare/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index fce761f88..d0fefbea5 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1302,7 +1302,7 @@ if buildQt5: stage('qt_5_15_12', """ git clone https://github.com/qt/qt5.git qt_5_15_12 cd qt_5_15_12 - perl init-repository --module-subset=qtbase,qtimageformats,qtsvg + perl init-repository.pl --module-subset=qtbase,qtimageformats,qtsvg git checkout v5.15.12-lts-lgpl git submodule update qtbase qtimageformats qtsvg depends:patches/qtbase_5.15.12/*.patch From b040b62b4eefcef173ac78d4ae9dddf3b27e51f9 Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Sat, 2 Mar 2024 22:07:16 +0100 Subject: [PATCH 008/108] prepare.py: simplify qt5 clone --- Telegram/build/prepare/prepare.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index d0fefbea5..fceedf2b5 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1300,11 +1300,9 @@ release: if buildQt5: stage('qt_5_15_12', """ - git clone https://github.com/qt/qt5.git qt_5_15_12 + git clone -b v5.15.12-lts-lgpl https://github.com/qt/qt5.git qt_5_15_12 cd qt_5_15_12 - perl init-repository.pl --module-subset=qtbase,qtimageformats,qtsvg - git checkout v5.15.12-lts-lgpl - git submodule update qtbase qtimageformats qtsvg + perl init-repository --module-subset=qtbase,qtimageformats,qtsvg depends:patches/qtbase_5.15.12/*.patch cd qtbase win: From 5b62d97288b03750d5570f9c59ec7c3f88f762b4 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 6 Mar 2024 20:12:31 +0400 Subject: [PATCH 009/108] Update submodules --- .gitmodules | 3 + Telegram/CMakeLists.txt | 3 - .../linux/notifications_manager_linux.cpp | 72 ++++--- .../linux/org.freedesktop.portal.Inhibit.xml | 186 ------------------ Telegram/ThirdParty/xdg-desktop-portal | 1 + Telegram/lib_base | 2 +- Telegram/lib_webview | 2 +- cmake | 2 +- 8 files changed, 41 insertions(+), 230 deletions(-) delete mode 100644 Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml create mode 160000 Telegram/ThirdParty/xdg-desktop-portal diff --git a/.gitmodules b/.gitmodules index 101674a90..bf3c35f42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -100,3 +100,6 @@ [submodule "Telegram/ThirdParty/libprisma"] path = Telegram/ThirdParty/libprisma url = https://github.com/desktop-app/libprisma.git +[submodule "Telegram/ThirdParty/xdg-desktop-portal"] + path = Telegram/ThirdParty/xdg-desktop-portal + url = https://github.com/flatpak/xdg-desktop-portal.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e1766f5e6..64409ea88 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1651,9 +1651,6 @@ else() desktop-app::external_glibmm ) - include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake) - generate_dbus(Telegram org.freedesktop.portal. XdpInhibit ${src_loc}/platform/linux/org.freedesktop.portal.Inhibit.xml) - if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) target_link_libraries(Telegram PRIVATE diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index c94400ff1..0473edf72 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -78,24 +78,24 @@ std::unique_ptr<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() { Gio::DBus::BusType::SESSION); const auto activatable = [&] { - try { - return ranges::contains( - base::Platform::DBus::ListActivatableNames(connection), - kService, - &Glib::ustring::raw); - } catch (...) { + const auto names = base::Platform::DBus::ListActivatableNames( + connection->gobj()); + + if (!names) { // avoid service restart loop in sandboxed environments return true; } + + return ranges::contains(*names, kService); }(); return std::make_unique<base::Platform::DBus::ServiceWatcher>( - connection, + connection->gobj(), kService, [=]( - const Glib::ustring &service, - const Glib::ustring &oldOwner, - const Glib::ustring &newOwner) { + const std::string &service, + const std::string &oldOwner, + const std::string &newOwner) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { if (activatable && newOwner.empty()) { Core::App().notifications().clearAll(); @@ -115,27 +115,28 @@ void StartServiceAsync(Fn<void()> callback) { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); - base::Platform::DBus::StartServiceByNameAsync( - connection, + namespace DBus = base::Platform::DBus; + DBus::StartServiceByNameAsync( + connection->gobj(), kService, - [=](Fn<base::Platform::DBus::StartReply()> result) { + [=](Fn<DBus::Result<DBus::StartReply>()> result) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { - try { - result(); // get the error if any - } catch (const Glib::Error &e) { + // get the error if any + if (const auto ret = result(); !ret) { static const auto NotSupportedErrors = { "org.freedesktop.DBus.Error.ServiceUnknown", }; - const auto errorName = - Gio::DBus::ErrorUtils::get_remote_error(e) - .raw(); - - if (!ranges::contains( + if (ranges::none_of( NotSupportedErrors, - errorName)) { - throw; + [&](const auto &error) { + return strstr( + ret.error()->what(), + error); + })) { + throw std::runtime_error( + ret.error()->what()); } } }); @@ -156,25 +157,20 @@ bool GetServiceRegistered() { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); - const auto hasOwner = [&] { - try { - return base::Platform::DBus::NameHasOwner( - connection, - kService); - } catch (...) { - return false; - } - }(); + const auto hasOwner = base::Platform::DBus::NameHasOwner( + connection->gobj(), + kService + ).value_or(false); static const auto activatable = [&] { - try { - return ranges::contains( - base::Platform::DBus::ListActivatableNames(connection), - kService, - &Glib::ustring::raw); - } catch (...) { + const auto names = base::Platform::DBus::ListActivatableNames( + connection->gobj()); + + if (!names) { return false; } + + return ranges::contains(*names, kService); }(); return hasOwner || activatable; diff --git a/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml b/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml deleted file mode 100644 index e91bd22d3..000000000 --- a/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml +++ /dev/null @@ -1,186 +0,0 @@ -<?xml version="1.0"?> -<!-- - Copyright (C) 2016 Red Hat, Inc. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library. If not, see <http://www.gnu.org/licenses/>. - - Author: Matthias Clasen <mclasen@redhat.com> ---> - -<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> - <!-- - org.freedesktop.portal.Inhibit: - @short_description: Portal for inhibiting session transitions - - This simple interface lets sandboxed applications inhibit the user - session from ending, suspending, idling or getting switched away. - - This documentation describes version 3 of this interface. - --> - <interface name="org.freedesktop.portal.Inhibit"> - <!-- - Inhibit: - @window: Identifier for the window - @flags: Flags identifying what is inhibited - @options: Vardict with optional further information - @handle: Object path for the #org.freedesktop.portal.Request object representing this call - - Inhibits a session status changes. To remove the inhibition, - call org.freedesktop.portal.Request.Close() on the returned - handle. - - The flags determine what changes are inhibited: - <simplelist> - <member>1: Logout</member> - <member>2: User Switch</member> - <member>4: Suspend</member> - <member>8: Idle</member> - </simplelist> - - Supported keys in the @options vardict include: - <variablelist> - <varlistentry> - <term>handle_token s</term> - <listitem><para> - A string that will be used as the last element of the @handle. Must be a valid - object path element. See the #org.freedesktop.portal.Request documentation for - more information about the @handle. - </para></listitem> - </varlistentry> - <varlistentry> - <term>reason s</term> - <listitem><para>User-visible reason for the inhibition.</para></listitem> - </varlistentry> - </variablelist> - --> - <method name="Inhibit"> - <arg type="s" name="window" direction="in"/> - <arg type="u" name="flags" direction="in"/> - <arg type="a{sv}" name="options" direction="in"/> - <arg type="o" name="handle" direction="out"/> - </method> - - <!-- - CreateMonitor: - @window: the parent window - @options: Vardict with optional further information - @handle: Object path for the #org.freedesktop.portal.Request object representing this call - - Creates a monitoring session. While this session is - active, the caller will receive StateChanged signals - with updates on the session state. - - A successfully created session can at any time be closed using - org.freedesktop.portal.Session::Close, or may at any time be closed - by the portal implementation, which will be signalled via - #org.freedesktop.portal.Session::Closed. - - Supported keys in the @options vardict include: - <variablelist> - <varlistentry> - <term>handle_token s</term> - <listitem><para> - A string that will be used as the last element of the @handle. Must be a valid - object path element. See the #org.freedesktop.portal.Request documentation for - more information about the @handle. - </para></listitem> - </varlistentry> - <varlistentry> - <term>session_handle_token s</term> - <listitem><para> - A string that will be used as the last element of the session handle. Must be a valid - object path element. See the #org.freedesktop.portal.Session documentation for - more information about the session handle. - </para></listitem> - </varlistentry> - </variablelist> - - The following results get returned via the #org.freedesktop.portal.Request::Response signal: - <variablelist> - <varlistentry> - <term>session_handle o</term> - <listitem><para> - The session handle. An object path for the - #org.freedesktop.portal.Session object representing the created - session. - </para></listitem> - </varlistentry> - </variablelist> - - This method was added in version 2 of this interface. - --> - <method name="CreateMonitor"> - <arg type="s" name="window" direction="in"/> - <arg type="a{sv}" name="options" direction="in"/> - <arg type="o" name="handle" direction="out"/> - </method> - - <!-- - StateChanged: - @session_handle: Object path for the #org.freedesktop.portal.Session object - @state: Vardict with information about the session state - - The StateChanged signal is sent to active monitoring sessions when - the session state changes. - - When the session state changes to 'Query End', clients with active monitoring - sessions are expected to respond by calling - org.freedesktop.portal.Inhibit.QueryEndResponse() within a second - of receiving the StateChanged signal. They may call org.freedesktop.portal.Inhibit.Inhibit() - first to inhibit logout, to prevent the session from proceeding to the Ending state. - - The following information may get returned in the @state vardict: - <variablelist> - <varlistentry> - <term>screensaver-active b</term> - <listitem><para> - Whether the screensaver is active. - </para></listitem> - </varlistentry> - <varlistentry> - <term>session-state u</term> - <listitem><para> - The state of the session. This member is new in version 3. - </para> - <simplelist> - <member>1: Running</member> - <member>2: Query End</member> - <member>3: Ending</member> - </simplelist> - </listitem> - </varlistentry> - </variablelist> - --> - <signal name="StateChanged"> - <arg type="o" name="session_handle" direction="out"/> - <arg type="a{sv}" name="state" direction="out"/> - </signal> - - <!-- - QueryEndResponse: - @session_handle: Object path for the #org.freedesktop.portal.Session object - - Acknowledges that the caller received the #org.freedesktop.portal.Inhibit::StateChanged - signal. This method should be called within one second or receiving a StateChanged - signal with the 'Query End' state. - - Since version 3. - --> - <method name="QueryEndResponse"> - <arg type="o" name="session_handle" direction="in"/> - </method> - - <property name="version" type="u" access="read"/> - </interface> -</node> diff --git a/Telegram/ThirdParty/xdg-desktop-portal b/Telegram/ThirdParty/xdg-desktop-portal new file mode 160000 index 000000000..fa8d41a2f --- /dev/null +++ b/Telegram/ThirdParty/xdg-desktop-portal @@ -0,0 +1 @@ +Subproject commit fa8d41a2f9a5d30a1e41568b6fb53b046dce14dc diff --git a/Telegram/lib_base b/Telegram/lib_base index 888a19075..cee9211bd 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 888a19075b569eda3d18a977543320823b984ae0 +Subproject commit cee9211bd58e054f24ad5e7f122037f71a44b237 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 4fce8b197..27af88195 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 4fce8b1971721da739619acf36da0fe79d614a23 +Subproject commit 27af88195bca687e9d2a52b4fcd4e83ef5476be9 diff --git a/cmake b/cmake index a46279fcf..b699c232d 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit a46279fcfe69ebcc806bb31679ccece5f7c07508 +Subproject commit b699c232d57d50070a7b1b861809e206624f48d4 From a66b886c513d3554dc035a0aeff7b85da92576d3 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 6 Mar 2024 20:13:17 +0400 Subject: [PATCH 010/108] Initialize Linux lock screen monitor fully asynchronously --- .../platform/linux/integration_linux.cpp | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 1087e09fb..6613f7382 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -180,7 +180,7 @@ gi::ref_ptr<Application> MakeApplication() { return result; } -class LinuxIntegration final : public Integration { +class LinuxIntegration final : public Integration, public base::has_weak_ptr { public: LinuxIntegration(); @@ -200,13 +200,6 @@ private: LinuxIntegration::LinuxIntegration() : _application(MakeApplication()) -, _inhibitProxy( - XdpInhibit::InhibitProxy::new_for_bus_sync( - Gio::BusType::SESSION_, - Gio::DBusProxyFlags::DO_NOT_AUTO_START_AT_CONSTRUCTION_, - base::Platform::XDP::kService, - base::Platform::XDP::kObjectPath, - nullptr)) , _darkModeWatcher( "org.freedesktop.appearance", "color-scheme", @@ -230,7 +223,18 @@ LinuxIntegration::LinuxIntegration() } void LinuxIntegration::init() { - initInhibit(); + XdpInhibit::InhibitProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) { + _inhibitProxy = XdpInhibit::InhibitProxy::new_for_bus_finish( + res, + nullptr); + + initInhibit(); + })); } void LinuxIntegration::initInhibit() { From 1e9b7e2726add6b87bd54d8532783b30cc22c553 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 7 Mar 2024 01:24:50 +0400 Subject: [PATCH 011/108] Use XDP::kObjectPath for session and request paths --- Telegram/SourceFiles/platform/linux/integration_linux.cpp | 3 ++- .../SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp | 4 ++-- Telegram/SourceFiles/platform/linux/specific_linux.cpp | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 6613f7382..fef646b20 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -252,7 +252,8 @@ void LinuxIntegration::initInhibit() { const auto sessionHandleToken = "tdesktop" + std::to_string(base::RandomValue<uint>()); - const auto sessionHandle = "/org/freedesktop/portal/desktop/session/" + const auto sessionHandle = base::Platform::XDP::kObjectPath + + std::string("/session/") + uniqueName + '/' + sessionHandleToken; diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp index e75bcb421..b01d5abbe 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp @@ -66,8 +66,8 @@ bool ShowXDPOpenWithDialog(const QString &filepath) { uniqueName.erase(0, 1); uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - const auto requestPath = Glib::ustring( - "/org/freedesktop/portal/desktop/request/") + const auto requestPath = base::Platform::XDP::kObjectPath + + Glib::ustring("/request/") + uniqueName + '/' + handleToken; diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index a63c54bc7..c0cbed82e 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -105,8 +105,8 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) { uniqueName.erase(0, 1); uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - const auto requestPath = Glib::ustring( - "/org/freedesktop/portal/desktop/request/") + const auto requestPath = base::Platform::XDP::kObjectPath + + Glib::ustring("/request/") + uniqueName + '/' + handleToken; From 7b8cdb43c49d6d91e406c459f004fbf9a8658981 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 7 Mar 2024 02:21:09 +0400 Subject: [PATCH 012/108] Port linux_xdp_open_with_dialog to cppgir --- .../linux/linux_xdp_open_with_dialog.cpp | 205 ++++++++---------- 1 file changed, 95 insertions(+), 110 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp index b01d5abbe..bd82cb17f 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp @@ -13,130 +13,115 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include <fcntl.h> -#include <glibmm.h> -#include <giomm.h> +#include <xdpopenuri/xdpopenuri.hpp> +#include <xdprequest/xdprequest.hpp> namespace Platform { namespace File { namespace internal { namespace { -constexpr auto kXDPOpenURIInterface = "org.freedesktop.portal.OpenURI"; -constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; +using namespace gi::repository; using base::Platform::XdgActivationToken; } // namespace bool ShowXDPOpenWithDialog(const QString &filepath) { - try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); + auto proxy = XdpOpenURI::OpenURIProxy::new_for_bus_sync( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + nullptr); - const auto version = connection->call_sync( - base::Platform::XDP::kObjectPath, - kPropertiesInterface, - "Get", - Glib::create_variant(std::tuple{ - Glib::ustring(kXDPOpenURIInterface), - Glib::ustring("version"), - }), - base::Platform::XDP::kService - ).get_child(0).get_dynamic<Glib::Variant<uint>>().get(); - - if (version < 3) { - return false; - } - - const auto filepathUtf8 = filepath.toUtf8(); - - const auto fd = open( - filepathUtf8.constData(), - O_RDONLY); - - if (fd == -1) { - return false; - } - - const auto fdGuard = gsl::finally([&] { ::close(fd); }); - - const auto handleToken = Glib::ustring("tdesktop") - + std::to_string(base::RandomValue<uint>()); - - auto uniqueName = connection->get_unique_name(); - uniqueName.erase(0, 1); - uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - - const auto requestPath = base::Platform::XDP::kObjectPath - + Glib::ustring("/request/") - + uniqueName - + '/' - + handleToken; - - const auto loop = Glib::MainLoop::create(); - - const auto signalId = connection->signal_subscribe( - [&]( - const Glib::RefPtr<Gio::DBus::Connection> &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { - loop->quit(); - }, - base::Platform::XDP::kService, - base::Platform::XDP::kRequestInterface, - "Response", - requestPath); - - const auto signalGuard = gsl::finally([&] { - if (signalId != 0) { - connection->signal_unsubscribe(signalId); - } - }); - - auto outFdList = Glib::RefPtr<Gio::UnixFDList>(); - - connection->call_sync( - base::Platform::XDP::kObjectPath, - kXDPOpenURIInterface, - "OpenFile", - Glib::create_variant(std::tuple{ - base::Platform::XDP::ParentWindowID(), - Glib::DBusHandle(), - std::map<Glib::ustring, Glib::VariantBase>{ - { - "handle_token", - Glib::create_variant(handleToken) - }, - { - "activation_token", - Glib::create_variant( - Glib::ustring(XdgActivationToken().toStdString())) - }, - { - "ask", - Glib::create_variant(true) - }, - }, - }), - Gio::UnixFDList::create(std::vector<int>{ fd }), - outFdList, - base::Platform::XDP::kService); - - if (signalId != 0) { - QWidget window; - window.setAttribute(Qt::WA_DontShowOnScreen); - window.setWindowModality(Qt::ApplicationModal); - window.show(); - loop->run(); - } - - return true; - } catch (...) { + if (!proxy) { + return false; } - return false; + auto interface = XdpOpenURI::OpenURI(proxy); + if (interface.get_version() < 3) { + return false; + } + + const auto fd = open( + QFile::encodeName(filepath).constData(), + O_RDONLY); + + if (fd == -1) { + return false; + } + + const auto fdGuard = gsl::finally([&] { close(fd); }); + + const auto handleToken = "tdesktop" + + std::to_string(base::RandomValue<uint>()); + + std::string uniqueName = proxy.get_connection().get_unique_name(); + uniqueName.erase(0, 1); + uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); + + auto request = XdpRequest::Request( + XdpRequest::RequestProxy::new_sync( + proxy.get_connection(), + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath + + std::string("/request/") + + uniqueName + + '/' + + handleToken, + nullptr, + nullptr)); + + if (!request) { + return false; + } + + auto loop = GLib::MainLoop::new_(); + + const auto signalId = request.signal_response().connect([=]( + XdpRequest::Request, + guint, + GLib::Variant) mutable { + loop.quit(); + }); + + const auto signalGuard = gsl::finally([&] { + request.disconnect(signalId); + }); + + auto result = interface.call_open_file_sync( + std::string(base::Platform::XDP::ParentWindowID()), + GLib::Variant::new_handle(0), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(handleToken))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("activation_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string( + XdgActivationToken().toStdString()))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("ask"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(true))), + }), + Gio::UnixFDList::new_from_array((std::array{ fd }).data(), 1), + nullptr); + + if (!result) { + return false; + } + + QWidget window; + window.setAttribute(Qt::WA_DontShowOnScreen); + window.setWindowModality(Qt::ApplicationModal); + window.show(); + loop.run(); + + return true; } } // namespace internal From 41481129f7775faeb51f889937d4d6de0d18ddd1 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 7 Mar 2024 02:57:38 +0400 Subject: [PATCH 013/108] Port main_window_linux to cppgir --- .../platform/linux/main_window_linux.cpp | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index f4c400f32..e6a3b68a6 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -43,8 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <QtWidgets/QLineEdit> #include <QtWidgets/QTextEdit> -#include <glibmm.h> -#include <giomm.h> +#include <gio/gio.hpp> namespace Platform { namespace { @@ -236,6 +235,8 @@ void MainWindow::updateUnityCounter() { #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) qApp->setBadgeNumber(Core::App().unreadBadge()); #else // Qt >= 6.6.0 + using namespace gi::repository; + static const auto djbStringHash = [](const std::string &string) { uint hash = 5381; for (const auto &curChar : string) { @@ -244,40 +245,36 @@ void MainWindow::updateUnityCounter() { return hash; }; - const auto launcherUrl = Glib::ustring( - "application://" - + QGuiApplication::desktopFileName().toStdString() - + ".desktop"); + const auto launcherUrl = "application://" + + QGuiApplication::desktopFileName().toStdString() + + ".desktop"; + const auto counterSlice = std::min(Core::App().unreadBadge(), 9999); - std::map<Glib::ustring, Glib::VariantBase> dbusUnityProperties; - if (counterSlice > 0) { - // According to the spec, it should be of 'x' D-Bus signature, - // which corresponds to signed 64-bit integer - // https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry - dbusUnityProperties["count"] = Glib::create_variant( - int64(counterSlice)); - dbusUnityProperties["count-visible"] = Glib::create_variant(true); - } else { - dbusUnityProperties["count-visible"] = Glib::create_variant(false); + auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr); + if (!connection) { + return; } - try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); - - connection->emit_signal( - "/com/canonical/unity/launcherentry/" - + std::to_string(djbStringHash(launcherUrl)), - "com.canonical.Unity.LauncherEntry", - "Update", - {}, - Glib::create_variant(std::tuple{ - launcherUrl, - dbusUnityProperties, - })); - } catch (...) { - } + connection.emit_signal( + {}, + "/com/canonical/unity/launcherentry/" + + std::to_string(djbStringHash(launcherUrl)), + "com.canonical.Unity.LauncherEntry", + "Update", + GLib::Variant::new_tuple({ + GLib::Variant::new_string(launcherUrl), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("count"), + GLib::Variant::new_variant( + GLib::Variant::new_int64(counterSlice))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("count-visible"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(counterSlice))), + }), + })); #endif // Qt < 6.6.0 } From 5971aefb8384d8743d616699c4f6def1489f6949 Mon Sep 17 00:00:00 2001 From: mrbesen <y.g.2@gmx.de> Date: Tue, 28 Nov 2023 19:42:17 +0100 Subject: [PATCH 014/108] add bash shebang --- Telegram/build/prepare/linux.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Telegram/build/prepare/linux.sh b/Telegram/build/prepare/linux.sh index d3ee5b5be..e4d2f920e 100755 --- a/Telegram/build/prepare/linux.sh +++ b/Telegram/build/prepare/linux.sh @@ -1,3 +1,5 @@ +#!/bin/bash + set -e FullExecPath=$PWD pushd `dirname $0` > /dev/null From c26982be3e31c99a6125b0f10e49e91038494322 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 18 Feb 2024 20:48:05 +0300 Subject: [PATCH 015/108] Added support for AVIF, HEIF and JPEG XL on macOS. --- .github/workflows/mac.yml | 2 +- Telegram/build/prepare/prepare.py | 234 +++++++++++++++++++++--------- docs/building-mac.md | 2 +- 3 files changed, 169 insertions(+), 69 deletions(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8e99d7162..68bab389c 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -64,7 +64,7 @@ jobs: - name: First set up. run: | sudo chown -R `whoami`:admin /usr/local/share - brew install automake ninja pkg-config + brew install automake ninja pkg-config nasm meson # Disable spotlight. sudo mdutil -a -i off diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index fceedf2b5..30d5084f4 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -202,6 +202,11 @@ def removeDir(folder): return 'if exist ' + folder + ' rmdir /Q /S ' + folder + '\nif exist ' + folder + ' exit /b 1' return 'rm -rf ' + folder +def setVar(key, value): + if win: + return 'SET ' + key + '="' + value + '"'; + return key + '="' + value + '"'; + def filterByPlatform(commands): commands = commands.split('\n') result = '' @@ -690,10 +695,9 @@ mac: """) stage('dav1d', """ -win: git clone -b 1.2.1 --depth 1 https://code.videolan.org/videolan/dav1d.git cd dav1d - +win: if "%X8664%" equ "x64" ( SET "TARGET=x86_64" ) else ( @@ -709,7 +713,7 @@ win: echo system = 'windows' >> %FILE% echo cpu_family = '%TARGET%' >> %FILE% echo cpu = '%TARGET%' >> %FILE% - echo endian = 'little'>> %FILE% + echo endian = 'little' >> %FILE% depends:python/Scripts/activate.bat %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat @@ -723,12 +727,55 @@ release: win: copy %LIBS_DIR%\\local\\lib\\libdav1d.a %LIBS_DIR%\\local\\lib\\dav1d.lib deactivate +mac: + buildOneArch() { + arch=$1 + folder=`pwd`/$2 + + TARGET="\'${arch}\'" + MIN="\'${MIN_VER}\'" + FILE=cross-file.txt + echo "[binaries]" > $FILE + echo "c = ['clang', '-arch', ${TARGET}]" >> $FILE + echo "cpp = ['clang++', '-arch', ${TARGET}]" >> $FILE + echo "ar = 'ar'" >> $FILE + echo "strip = 'strip'" >> $FILE + echo "[built-in options]" >> $FILE + echo "c_args = [${MIN}]" >> $FILE + echo "cpp_args = [${MIN}]" >> $FILE + echo "c_link_args = [${MIN}]" >> $FILE + echo "cpp_link_args = [${MIN}]" >> $FILE + echo "[host_machine]" >> $FILE + echo "system = 'darwin'" >> $FILE + echo "subsystem = 'macos'" >> $FILE + echo "cpu_family = ${TARGET}" >> $FILE + echo "cpu = ${TARGET}" >> $FILE + echo "endian = 'little'" >> $FILE + + meson setup \\ + --cross-file $FILE \\ + --prefix ${USED_PREFIX} \\ + --default-library=static \\ + --buildtype=minsize \\ + -Denable_tools=false \\ + -Denable_tests=false \\ + ${folder} + meson compile -C ${folder} + meson install -C ${folder} + + mv ${USED_PREFIX}/lib/libdav1d.a ${folder}/libdav1d.a + } + + buildOneArch arm64 build.arm64 + buildOneArch x86_64 build + + lipo -create build.arm64/libdav1d.a build/libdav1d.a -output ${USED_PREFIX}/lib/libdav1d.a """) stage('libavif', """ -win: git clone -b v0.11.1 --depth 1 https://github.com/AOMediaCodec/libavif.git cd libavif +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -743,12 +790,22 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D AVIF_ENABLE_WERROR=OFF \\ + -D AVIF_CODEC_DAV1D=ON + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libde265', """ -win: git clone --depth 1 -b v1.0.12 https://github.com/strukturag/libde265.git cd libde265 +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -768,12 +825,63 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D DISABLE_SSE=ON \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D ENABLE_DECODER=ON \\ + -D ENABLE_ENCODER=OFF + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel +""") + +stage('libwebp', """ + git clone -b v1.3.2 https://github.com/webmproject/libwebp.git + cd libwebp +win: + nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all + nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all + copy out\\release-static\\$X8664\\lib\\libwebp.lib out\\release-static\\$X8664\\lib\\webp.lib + copy out\\release-static\\$X8664\\lib\\libwebpdemux.lib out\\release-static\\$X8664\\lib\\webpdemux.lib + copy out\\release-static\\$X8664\\lib\\libwebpmux.lib out\\release-static\\$X8664\\lib\\webpmux.lib +mac: + buildOneArch() { + arch=$1 + folder=$2 + + CFLAGS=$UNGUARDED cmake -B $folder -G Ninja . \\ + -D CMAKE_BUILD_TYPE=Release \\ + -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_OSX_ARCHITECTURES=$arch \\ + -D WEBP_BUILD_ANIM_UTILS=OFF \\ + -D WEBP_BUILD_CWEBP=OFF \\ + -D WEBP_BUILD_DWEBP=OFF \\ + -D WEBP_BUILD_GIF2WEBP=OFF \\ + -D WEBP_BUILD_IMG2WEBP=OFF \\ + -D WEBP_BUILD_VWEBP=OFF \\ + -D WEBP_BUILD_WEBPMUX=OFF \\ + -D WEBP_BUILD_WEBPINFO=OFF \\ + -D WEBP_BUILD_EXTRAS=OFF + cmake --build $folder $MAKE_THREADS_CNT + } + buildOneArch arm64 build.arm64 + buildOneArch x86_64 build + + lipo -create build.arm64/libsharpyuv.a build/libsharpyuv.a -output build/libsharpyuv.a + lipo -create build.arm64/libwebp.a build/libwebp.a -output build/libwebp.a + lipo -create build.arm64/libwebpdemux.a build/libwebpdemux.a -output build/libwebpdemux.a + lipo -create build.arm64/libwebpmux.a build/libwebpmux.a -output build/libwebpmux.a + cmake --install build """) stage('libheif', """ -win: git clone --depth 1 -b v1.16.2 https://github.com/strukturag/libheif.git cd libheif +win: %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' libheif/CMakeLists.txt cmake . ^ @@ -797,12 +905,55 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D ENABLE_PLUGIN_LOADING=OFF \\ + -D WITH_AOM_ENCODER=OFF \\ + -D WITH_AOM_DECODER=OFF \\ + -D WITH_X265=OFF \\ + -D WITH_SvtEnc=OFF \\ + -D WITH_RAV1E=OFF \\ + -D WITH_DAV1D=ON \\ + -D WITH_LIBDE265=ON \\ + -D LIBDE265_INCLUDE_DIR=$USED_PREFIX/include/ \\ + -D LIBDE265_LIBRARY=$USED_PREFIX/lib/libde265.a \\ + -D LIBSHARPYUV_INCLUDE_DIR=$USED_PREFIX/include/webp/ \\ + -D LIBSHARPYUV_LIBRARY=$USED_PREFIX/lib/libsharpyuv.a \\ + -D WITH_EXAMPLES=OFF + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libjxl', """ -win: git clone -b v0.8.2 --depth 1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl +""" + setVar("cmake_defines", ' '.join(""" + -DBUILD_SHARED_LIBS=OFF + -DBUILD_TESTING=OFF + -DJPEGXL_ENABLE_FUZZERS=OFF + -DJPEGXL_ENABLE_DEVTOOLS=OFF + -DJPEGXL_ENABLE_TOOLS=OFF + -DJPEGXL_ENABLE_DOXYGEN=OFF + -DJPEGXL_ENABLE_MANPAGES=OFF + -DJPEGXL_ENABLE_EXAMPLES=OFF + -DJPEGXL_ENABLE_JNI=OFF + -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF + -DJPEGXL_ENABLE_SJPEG=OFF + -DJPEGXL_ENABLE_OPENEXR=OFF + -DJPEGXL_ENABLE_SKCMS=ON + -DJPEGXL_BUNDLE_SKCMS=ON + -DJPEGXL_ENABLE_VIEWERS=OFF + -DJPEGXL_ENABLE_TCMALLOC=OFF + -DJPEGXL_ENABLE_PLUGINS=OFF + -DJPEGXL_ENABLE_COVERAGE=OFF + -DJPEGXL_ENABLE_PROFILER=OFF + -DJPEGXL_WARNINGS_AS_ERRORS=OFF +""".replace('\n', '').split())) + """ +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -813,31 +964,20 @@ win: -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ - -DBUILD_SHARED_LIBS=OFF ^ - -DBUILD_TESTING=OFF ^ - -DJPEGXL_ENABLE_FUZZERS=OFF ^ - -DJPEGXL_ENABLE_DEVTOOLS=OFF ^ - -DJPEGXL_ENABLE_TOOLS=OFF ^ - -DJPEGXL_ENABLE_DOXYGEN=OFF ^ - -DJPEGXL_ENABLE_MANPAGES=OFF ^ - -DJPEGXL_ENABLE_EXAMPLES=OFF ^ - -DJPEGXL_ENABLE_JNI=OFF ^ - -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF ^ - -DJPEGXL_ENABLE_SJPEG=OFF ^ - -DJPEGXL_ENABLE_OPENEXR=OFF ^ - -DJPEGXL_ENABLE_SKCMS=ON ^ - -DJPEGXL_BUNDLE_SKCMS=ON ^ - -DJPEGXL_ENABLE_VIEWERS=OFF ^ - -DJPEGXL_ENABLE_TCMALLOC=OFF ^ - -DJPEGXL_ENABLE_PLUGINS=OFF ^ - -DJPEGXL_ENABLE_COVERAGE=OFF ^ - -DJPEGXL_ENABLE_PROFILER=OFF ^ - -DJPEGXL_WARNINGS_AS_ERRORS=OFF + %cmake_defines% cmake --build . --config Debug cmake --install . --config Debug release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + ${cmake_defines} + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libvpx', """ @@ -906,46 +1046,6 @@ depends:yasm/yasm make install """) -stage('libwebp', """ - git clone -b v1.3.2 https://github.com/webmproject/libwebp.git - cd libwebp -win: - nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all - nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all - copy out\\release-static\\$X8664\\lib\\libwebp.lib out\\release-static\\$X8664\\lib\\webp.lib - copy out\\release-static\\$X8664\\lib\\libwebpdemux.lib out\\release-static\\$X8664\\lib\\webpdemux.lib - copy out\\release-static\\$X8664\\lib\\libwebpmux.lib out\\release-static\\$X8664\\lib\\webpmux.lib -mac: - buildOneArch() { - arch=$1 - folder=$2 - - CFLAGS=$UNGUARDED cmake -B $folder -G Ninja . \\ - -D CMAKE_BUILD_TYPE=Release \\ - -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\ - -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ - -D CMAKE_OSX_ARCHITECTURES=$arch \\ - -D WEBP_BUILD_ANIM_UTILS=OFF \\ - -D WEBP_BUILD_CWEBP=OFF \\ - -D WEBP_BUILD_DWEBP=OFF \\ - -D WEBP_BUILD_GIF2WEBP=OFF \\ - -D WEBP_BUILD_IMG2WEBP=OFF \\ - -D WEBP_BUILD_VWEBP=OFF \\ - -D WEBP_BUILD_WEBPMUX=OFF \\ - -D WEBP_BUILD_WEBPINFO=OFF \\ - -D WEBP_BUILD_EXTRAS=OFF - cmake --build $folder $MAKE_THREADS_CNT - } - buildOneArch arm64 build.arm64 - buildOneArch x86_64 build - - lipo -create build.arm64/libsharpyuv.a build/libsharpyuv.a -output build/libsharpyuv.a - lipo -create build.arm64/libwebp.a build/libwebp.a -output build/libwebp.a - lipo -create build.arm64/libwebpdemux.a build/libwebpdemux.a -output build/libwebpdemux.a - lipo -create build.arm64/libwebpmux.a build/libwebpmux.a -output build/libwebpmux.a - cmake --install build -""") - stage('nv-codec-headers', """ win: git clone https://github.com/FFmpeg/nv-codec-headers.git diff --git a/docs/building-mac.md b/docs/building-mac.md index d357c66c1..11435a8dd 100644 --- a/docs/building-mac.md +++ b/docs/building-mac.md @@ -13,7 +13,7 @@ You will require **api_id** and **api_hash** to access the Telegram API servers. Go to ***BuildPath*** and run ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - brew install git automake cmake wget pkg-config gnu-tar ninja + brew install git automake cmake wget pkg-config gnu-tar ninja nasm meson sudo xcode-select -s /Applications/Xcode.app/Contents/Developer From de1bd6ef28ed625bcadbc4ef089dc3342a768fc0 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 19 Feb 2024 18:55:56 +0300 Subject: [PATCH 016/108] Slightly simplified creation of universal variables in prepare.py. --- Telegram/build/prepare/prepare.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 30d5084f4..c3a566796 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -202,10 +202,11 @@ def removeDir(folder): return 'if exist ' + folder + ' rmdir /Q /S ' + folder + '\nif exist ' + folder + ' exit /b 1' return 'rm -rf ' + folder -def setVar(key, value): +def setVar(key, multilineValue): + singlelineValue = ' '.join(multilineValue.replace('\n', '').split()); if win: - return 'SET ' + key + '="' + value + '"'; - return key + '="' + value + '"'; + return 'SET ' + key + '="' + singlelineValue + '"'; + return key + '="' + singlelineValue + '"'; def filterByPlatform(commands): commands = commands.split('\n') @@ -931,7 +932,7 @@ mac: stage('libjxl', """ git clone -b v0.8.2 --depth 1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl -""" + setVar("cmake_defines", ' '.join(""" +""" + setVar("cmake_defines", """ -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_FUZZERS=OFF @@ -952,7 +953,7 @@ stage('libjxl', """ -DJPEGXL_ENABLE_COVERAGE=OFF -DJPEGXL_ENABLE_PROFILER=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF -""".replace('\n', '').split())) + """ +""") + """ win: cmake . ^ -A %WIN32X64% ^ From 52c779bffa8dde3c5c09826add2607328fae0924 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 21 Feb 2024 20:47:11 +0300 Subject: [PATCH 017/108] Added support of inline markup reply to HTML export. --- Telegram/Resources/export_html/css/style.css | 23 +++++++++ Telegram/Resources/export_html/js/script.js | 6 +++ .../export/data/export_data_types.cpp | 24 +++++++++ .../export/data/export_data_types.h | 2 + .../export/output/export_output_html.cpp | 49 +++++++++++++++++++ .../export/output/export_output_json.cpp | 26 +--------- 6 files changed, 106 insertions(+), 24 deletions(-) diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index 79b680cc2..102f5f3a5 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -559,3 +559,26 @@ div.toast_shown { opacity: 0; user-select: none; } + +.bot_buttons_table { + border-spacing: 0px 2px; + width: 100%; +} +.bot_button { + border-radius: 8px; + text-align: center; + vertical-align: middle; + background-color: #168acd40; +} +.bot_button_row { + display: table; + table-layout: fixed; + padding: 0px; + width:100%; +} +.bot_button_row div { + display: table-cell; +} +.bot_button_column_separator { + width: 2px +} diff --git a/Telegram/Resources/export_html/js/script.js b/Telegram/Resources/export_html/js/script.js index 8d25f5302..284232202 100644 --- a/Telegram/Resources/export_html/js/script.js +++ b/Telegram/Resources/export_html/js/script.js @@ -62,6 +62,12 @@ function ShowNotAvailableEmoji() { return false; } +function ShowTextCopied(content) { + navigator.clipboard.writeText(content); + ShowToast("Text copied to clipboard."); + return false; +} + function ShowSpoiler(target) { if (target.classList.contains("hidden")) { target.classList.toggle("hidden"); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index f7928f006..1f56f9ec1 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -160,6 +160,30 @@ std::vector<std::vector<HistoryMessageMarkupButton>> ButtonRowsFromTL( } // namespace +QByteArray HistoryMessageMarkupButton::TypeToString( + const HistoryMessageMarkupButton &button) { + using Type = HistoryMessageMarkupButton::Type; + switch (button.type) { + case Type::Default: return "default"; + case Type::Url: return "url"; + case Type::Callback: return "callback"; + case Type::CallbackWithPassword: return "callback_with_password"; + case Type::RequestPhone: return "request_phone"; + case Type::RequestLocation: return "request_location"; + case Type::RequestPoll: return "request_poll"; + case Type::RequestPeer: return "request_peer"; + case Type::SwitchInline: return "switch_inline"; + case Type::SwitchInlineSame: return "switch_inline_same"; + case Type::Game: return "game"; + case Type::Buy: return "buy"; + case Type::Auth: return "auth"; + case Type::UserProfile: return "user_profile"; + case Type::WebView: return "web_view"; + case Type::SimpleWebView: return "simple_web_view"; + } + Unexpected("Type in HistoryMessageMarkupButton::Type."); +} + uint8 PeerColorIndex(BareId bareId) { const uint8 map[] = { 0, 7, 4, 1, 6, 3, 5 }; return map[bareId % base::array_size(map)]; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 5f2c2da39..76585991c 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -690,6 +690,8 @@ struct HistoryMessageMarkupButton { SimpleWebView, }; + static QByteArray TypeToString(const HistoryMessageMarkupButton &); + Type type; QString text; QByteArray data; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 395315227..9df6bcbb3 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1443,6 +1443,55 @@ auto HtmlWriter::Wrap::pushMessage( block.append(text); block.append(popTag()); } + if (!message.inlineButtonRows.empty()) { + using Type = HistoryMessageMarkupButton::Type; + const auto endline = u" | "_q; + block.append(pushTag("table", { { "class", "bot_buttons_table" } })); + block.append(pushTag("tbody")); + for (const auto &row : message.inlineButtonRows) { + block.append(pushTag("tr")); + block.append(pushTag("td", { { "class", "bot_button_row" } })); + for (const auto &button : row) { + using Attribute = std::pair<QByteArray, QByteArray>; + const auto content = (!button.data.isEmpty() + ? (u"Data: "_q + button.data + endline) + : QString()) + + (!button.forwardText.isEmpty() + ? (u"Forward text: "_q + button.forwardText + endline) + : QString()) + + (u"Type: "_q + + HistoryMessageMarkupButton::TypeToString(button)); + const auto link = (button.type == Type::Url) + ? button.data + : QByteArray(); + const auto onclick = (button.type != Type::Url) + ? ("return ShowTextCopied('" + content + "');").toUtf8() + : QByteArray(); + block.append(pushTag("div", { { "class", "bot_button" } })); + block.append(pushTag("a", { + link.isEmpty() ? Attribute() : Attribute{ "href", link }, + onclick.isEmpty() + ? Attribute() + : Attribute{ "onclick", onclick }, + })); + block.append(pushTag("div")); + block.append(button.text.toUtf8()); + block.append(popTag()); + block.append(popTag()); + block.append(popTag()); + + if (&button != &row.back()) { + block.append(pushTag("div", { + { "class", "bot_button_column_separator" } + })); + block.append(popTag()); + } + } + block.append(popTag()); + block.append(popTag()); + } + block.append(popTag()); + } if (!message.signature.isEmpty()) { block.append(pushDiv("signature details")); block.append(SerializeString(message.signature)); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index dde36a5a3..5bd7f2c37 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -784,29 +784,6 @@ QByteArray SerializeMessage( pushBare("text_entities", SerializeText(context, message.text, true)); if (!message.inlineButtonRows.empty()) { - const auto typeString = []( - const HistoryMessageMarkupButton &entry) -> QByteArray { - using Type = HistoryMessageMarkupButton::Type; - switch (entry.type) { - case Type::Default: return "default"; - case Type::Url: return "url"; - case Type::Callback: return "callback"; - case Type::CallbackWithPassword: return "callback_with_password"; - case Type::RequestPhone: return "request_phone"; - case Type::RequestLocation: return "request_location"; - case Type::RequestPoll: return "request_poll"; - case Type::RequestPeer: return "request_peer"; - case Type::SwitchInline: return "switch_inline"; - case Type::SwitchInlineSame: return "switch_inline_same"; - case Type::Game: return "game"; - case Type::Buy: return "buy"; - case Type::Auth: return "auth"; - case Type::UserProfile: return "user_profile"; - case Type::WebView: return "web_view"; - case Type::SimpleWebView: return "simple_web_view"; - } - Unexpected("Type in HistoryMessageMarkupButton::Type."); - }; const auto serializeRow = [&]( const std::vector<HistoryMessageMarkupButton> &row) { context.nesting.push_back(Context::kArray); @@ -817,7 +794,8 @@ QByteArray SerializeMessage( auto pairs = std::vector<std::pair<QByteArray, QByteArray>>(); pairs.push_back({ "type", - SerializeString(typeString(entry)), + SerializeString( + HistoryMessageMarkupButton::TypeToString(entry)), }); if (!entry.text.isEmpty()) { pairs.push_back({ From 93d1a187ca4b10a6f26418664a046fadb84829cf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 Feb 2024 05:04:35 +0300 Subject: [PATCH 018/108] Improved view style of contacts. --- Telegram/Resources/langs/lang.strings | 3 + .../SourceFiles/data/data_media_types.cpp | 2 +- .../view/media/history_view_contact.cpp | 487 ++++++++++++++---- .../history/view/media/history_view_contact.h | 52 +- 4 files changed, 425 insertions(+), 119 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 636909863..ea6c5f2e8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4724,6 +4724,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boosts_prepaid_giveaway_status#one" = "{count} subscription {duration}"; "lng_boosts_prepaid_giveaway_status#other" = "{count} subscriptions {duration}"; +"lng_contact_add" = "Add"; +"lng_contact_send_message" = "message"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7361373df..3e43cd47a 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1253,7 +1253,7 @@ const SharedContact *MediaContact::sharedContact() const { } TextWithEntities MediaContact::notificationText() const { - return tr::lng_in_dlg_contact(tr::now, Ui::Text::WithEntities); + return Ui::Text::Colorized(tr::lng_in_dlg_contact(tr::now)); } QString MediaContact::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 7b0ac35b9..908bf1c0c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -7,28 +7,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_contact.h" -#include "core/click_handler_types.h" // ClickHandlerContext -#include "lang/lang_keys.h" -#include "layout/layout_selection.h" -#include "mainwindow.h" #include "boxes/add_contact_box.h" -#include "history/history_item_components.h" -#include "history/history_item.h" -#include "history/history.h" -#include "history/view/history_view_element.h" -#include "history/view/history_view_cursor_state.h" -#include "window/window_session_controller.h" -#include "ui/empty_userpic.h" -#include "ui/chat/chat_style.h" -#include "ui/text/format_values.h" // Ui::FormatPhone -#include "ui/text/text_options.h" -#include "ui/painter.h" +#include "core/click_handler_types.h" // ClickHandlerContext #include "data/data_session.h" #include "data/data_user.h" -#include "data/data_media_types.h" -#include "data/data_cloud_file.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_reply.h" +#include "history/view/media/history_view_media_common.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "ui/chat/chat_style.h" +#include "ui/empty_userpic.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" // Ui::FormatPhone +#include "ui/text/text_options.h" +#include "window/window_session_controller.h" namespace HistoryView { namespace { @@ -81,17 +80,32 @@ Contact::Contact( const QString &last, const QString &phone) : Media(parent) -, _userId(userId) -, _fname(first) -, _lname(last) -, _phone(Ui::FormatPhone(phone)) { +, _st(st::historyPagePreview) +, _pixh(st::contactsPhotoSize) +, _userId(userId) { history()->owner().registerContactView(userId, parent); - _name.setText( - st::semiboldTextStyle, - tr::lng_full_name(tr::now, lt_first_name, first, lt_last_name, last).trimmed(), - Ui::NameTextOptions()); - _phonew = st::normalFont->width(_phone); + _nameLine.setText( + st::webPageTitleStyle, + tr::lng_full_name( + tr::now, + lt_first_name, + first, + lt_last_name, + last).trimmed(), + Ui::WebpageTextTitleOptions()); + + _phoneLine.setText( + st::webPageDescriptionStyle, + Ui::FormatPhone(QString(phone).replace(QChar('+'), QString())), + Ui::WebpageTextTitleOptions()); + +#if 0 // No info. + _infoLine.setText( + st::webPageDescriptionStyle, + phone, + Ui::WebpageTextTitleOptions()); +#endif } Contact::~Contact() { @@ -111,121 +125,300 @@ void Contact::updateSharedContactUserId(UserId userId) { } QSize Contact::countOptimalSize() { - const auto item = _parent->data(); - auto maxWidth = st::msgFileMinWidth; - _contact = _userId - ? item->history()->owner().userLoaded(_userId) + ? _parent->data()->history()->owner().userLoaded(_userId) : nullptr; if (_contact) { _contact->loadUserpic(); } else { - const auto full = _name.toString(); + const auto full = _nameLine.toString(); _photoEmpty = std::make_unique<Ui::EmptyUserpic>( Ui::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId ? peerFromUser(_userId) : Data::FakePeerIdForJustName(full))), full); } - if (_contact && _contact->isContact()) { - _linkl = SendMessageClickHandler(_contact); - _link = tr::lng_profile_send_message(tr::now).toUpper(); - } else if (_userId) { - _linkl = AddContactClickHandler(_parent->data()); - _link = tr::lng_profile_add_contact(tr::now).toUpper(); - } - _linkw = _link.isEmpty() ? 0 : st::semiboldFont->width(_link); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - - const auto tleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto tright = st.padding.right(); - if (_userId) { - accumulate_max(maxWidth, tleft + _phonew + tright); + _buttons.clear(); + if (_contact) { + const auto message = tr::lng_contact_send_message(tr::now).toUpper(); + _buttons.push_back({ + message, + st::semiboldFont->width(message), + SendMessageClickHandler(_contact), + }); + if (!_contact->isContact()) { + const auto add = tr::lng_contact_add(tr::now).toUpper(); + _buttons.push_back({ + add, + st::semiboldFont->width(add), + AddContactClickHandler(_parent->data()), + }); + } + _mainButton.link = _buttons.front().link; } else { - accumulate_max(maxWidth, tleft + _phonew + _parent->skipBlockWidth() + st::msgPadding.right()); +#if 0 // Can't view contact. + const auto view = tr::lng_profile_add_contact(tr::now).toUpper(); + _buttons.push_back({ + view, + st::semiboldFont->width(view), + AddContactClickHandler(_parent->data()), + }); +#endif + _mainButton.link = nullptr; } - accumulate_max(maxWidth, tleft + _name.maxWidth() + tright); - accumulate_min(maxWidth, st::msgMaxWidth); - auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom(); - if (_parent->bottomInfoIsWide()) { - minHeight += st::msgDateFont->height - st::msgDateDelta.y(); + const auto padding = inBubblePadding() + innerMargin(); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineHeight = UnitedLineHeight(); + + auto maxWidth = _parent->skipBlockWidth(); + auto minHeight = 0; + + auto textMinHeight = 0; + if (!_nameLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _nameLine.maxWidth()); + textMinHeight += 1 * lineHeight; } - if (!isBubbleTop()) { - minHeight -= st::msgFileTopMinus; + if (!_phoneLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _phoneLine.maxWidth()); + textMinHeight += 1 * lineHeight; } + if (!_infoLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _infoLine.maxWidth()); + textMinHeight += std::min(_infoLine.minHeight(), 1 * lineHeight); + } + minHeight = std::max(textMinHeight, st::contactsPhotoSize); + + if (!_buttons.empty()) { + auto buttonsWidth = rect::m::sum::h(st::historyPageButtonPadding); + for (const auto &button : _buttons) { + buttonsWidth += button.width; + } + accumulate_max(maxWidth, buttonsWidth); + } + maxWidth += rect::m::sum::h(padding); + minHeight += rect::m::sum::v(padding); + return { maxWidth, minHeight }; } void Contact::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - auto paintw = width(); + if (width() < rect::m::sum::h(st::msgPadding) + 1) { + return; + } + const auto st = context.st; + const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); - accumulate_min(paintw, maxWidth()); + const auto bubble = st::msgPadding; + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + auto tshift = inner.top(); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto nametop = st.nameTop - topMinus; - const auto nameright = st.padding.right(); - const auto statustop = st.statusTop - topMinus; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (_userId) { - QRect rthumb(style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, paintw)); - if (_contact) { - const auto was = !_userpic.null(); - _contact->paintUserpic(p, _userpic, rthumb.x(), rthumb.y(), st.thumbSize); - if (!was && !_userpic.null()) { - history()->owner().registerHeavyViewPart(_parent); + const auto selected = context.selected(); + const auto view = parent(); + const auto colorIndex = _contact + ? _contact->colorIndex() + : Data::DecideColorIndex( + Data::FakePeerIdForJustName(_nameLine.toString())); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + const auto backgroundEmojiId = _contact + ? _contact->backgroundEmojiId() + : DocumentId(); + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + true, + colorIndex + 1)] + : nullptr; + Ui::Text::ValidateQuotePaintCache(*cache, _st); + Ui::Text::FillQuotePaint(p, outer, *cache, _st); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + view); + if (!backgroundEmojiCache->frames[0].isNull()) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto r = outer + - QMargins(0, 0, 0, rect::bottom(outer) - end); + FillBackgroundEmoji(p, r, false, *backgroundEmojiCache); + } + } + + if (_mainButton.ripple) { + _mainButton.ripple->paint( + p, + outer.x(), + outer.y(), + width(), + &cache->bg); + if (_mainButton.ripple->empty()) { + _mainButton.ripple = nullptr; + } + } + + { + const auto left = inner.left(); + const auto top = tshift; + if (_userId) { + if (_contact) { + const auto was = !_userpic.null(); + _contact->paintUserpic(p, _userpic, left, top, _pixh); + if (!was && !_userpic.null()) { + history()->owner().registerHeavyViewPart(_parent); + } + } else { + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } if (context.selected()) { - PainterHighQualityEnabler hq(p); + auto hq = PainterHighQualityEnabler(p); p.setBrush(p.textPalette().selectOverlay); p.setPen(Qt::NoPen); - p.drawEllipse(rthumb); + p.drawEllipse(left, top, _pixh, _pixh); } - - bool over = ClickHandler::showAsActive(_linkl); - p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); - p.setPen(stm->msgFileThumbLinkFg); - p.drawTextLeft(nameleft, linktop, paintw, _link, _linkw); - } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); } - const auto namewidth = paintw - nameleft - nameright; - p.setFont(st::semiboldFont); - p.setPen(stm->historyFileNameFg); - _name.drawLeftElided(p, nameleft, nametop, namewidth, paintw); + const auto lineHeight = UnitedLineHeight(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineWidth = rect::right(inner) - lineLeft; - p.setFont(st::normalFont); - p.setPen(stm->mediaFg); - p.drawTextLeft(nameleft, statustop, paintw, _phone); + { + p.setPen(cache->icon); + p.setTextPalette(context.outbg + ? stm->semiboldPalette + : st->coloredTextPalette(selected, colorIndex)); + + const auto endskip = _nameLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _nameLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + context.selection); + tshift += lineHeight; + + p.setTextPalette(stm->textPalette); + } + p.setPen(stm->historyTextFg); + { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _phoneLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _phoneLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + toTitleSelection(context.selection)); + tshift += 1 * lineHeight; + } + if (!_infoLine.isEmpty()) { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _infoLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _parent->prepareCustomEmojiPaint(p, context, _infoLine); + _infoLine.draw(p, { + .position = { lineLeft, tshift }, + .outerWidth = width(), + .availableWidth = lineWidth, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .selection = toDescriptionSelection(context.selection), + .elisionHeight = (1 * lineHeight), + .elisionRemoveFromEnd = endskip, + }); + tshift += (1 * lineHeight); + } + + if (!_buttons.empty()) { + p.setFont(st::semiboldFont); + p.setPen(cache->icon); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + auto color = cache->icon; + color.setAlphaF(color.alphaF() * 0.3); + const auto top = end + st::historyPageButtonPadding.top(); + const auto buttonWidth = inner.width() / float64(_buttons.size()); + p.fillRect(inner.x(), end, inner.width(), line, color); + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + const auto left = inner.x() + i * buttonWidth; + if (button.ripple) { + button.ripple->paint(p, left, end, buttonWidth, &cache->bg); + if (button.ripple->empty()) { + _buttons[i].ripple = nullptr; + } + } + p.drawText( + left + (buttonWidth - button.width) / 2, + top + st::semiboldFont->ascent, + button.text); + } + } } TextState Contact::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - if (_userId) { - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (style::rtlrect(nameleft, linktop, _linkw, st::semiboldFont->height, width()).contains(point)) { - result.link = _linkl; - return result; + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + + _lastPoint = point; + + if (_buttons.size() > 1) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto left = inner.x() + i * bWidth; + if (QRectF(left, end, bWidth, bHeight).contains(point)) { + result.link = _buttons[i].link; + return result; + } } } - if (QRect(0, 0, width(), height()).contains(point) && _contact) { - result.link = _contact->openLink(); + if (outer.contains(point)) { + result.link = _mainButton.link; return result; } return result; @@ -239,4 +432,100 @@ bool Contact::hasHeavyPart() const { return !_userpic.null(); } +void Contact::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + if ((_lastPoint.y() < end) || (_buttons.size() <= 1)) { + if (p != _mainButton.link) { + return; + } + if (pressed) { + if (!_mainButton.ripple) { + const auto owner = &parent()->history()->owner(); + _mainButton.ripple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + outer.size(), + _st.radius), + [=] { owner->requestViewRepaint(parent()); }); + } + _mainButton.ripple->add(_lastPoint - outer.topLeft()); + } else if (_mainButton.ripple) { + _mainButton.ripple->lastStop(); + } + return; + } else if (_buttons.empty()) { + return; + } + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + if (p != button.link) { + continue; + } + if (pressed) { + if (!button.ripple) { + const auto owner = &parent()->history()->owner(); + + _buttons[i].ripple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + Ui::RippleAnimation::MaskByDrawer( + QSize(bWidth, bHeight), + false, + [=](QPainter &p) { + p.drawRect(0, 0, bWidth, bHeight); + }), + [=] { owner->requestViewRepaint(parent()); }); + } + button.ripple->add(_lastPoint + - QPoint(inner.x() + i * bWidth, end)); + } else if (button.ripple) { + button.ripple->lastStop(); + } + } +} + +QMargins Contact::inBubblePadding() const { + return { + st::msgPadding.left(), + isBubbleTop() ? st::msgPadding.left() : 0, + st::msgPadding.right(), + isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0 + }; +} + +QMargins Contact::innerMargin() const { + const auto button = _buttons.empty() ? 0 : st::historyPageButtonHeight; + return _st.padding + QMargins(0, 0, 0, button); +} + +int Contact::bottomInfoPadding() const { + if (!isBubbleBottom()) { + return 0; + } + + auto result = st::msgDateFont->height; + + // We use padding greater than st::msgPadding.bottom() in the + // bottom of the bubble so that the left line looks pretty. + // but if we have bottom skip because of the info display + // we don't need that additional padding so we replace it + // back with st::msgPadding.bottom() instead of left(). + result += st::msgPadding.bottom() - st::msgPadding.left(); + return result; +} + +TextSelection Contact::toTitleSelection(TextSelection selection) const { + return UnshiftItemSelection(selection, _nameLine); +} + +TextSelection Contact::toDescriptionSelection(TextSelection selection) const { + return UnshiftItemSelection(toTitleSelection(selection), _phoneLine); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.h b/Telegram/SourceFiles/history/view/media/history_view_contact.h index ecca595f2..2dd665b94 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.h +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.h @@ -12,11 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class EmptyUserpic; +class RippleAnimation; } // namespace Ui namespace HistoryView { -class Contact : public Media { +class Contact final : public Media { public: Contact( not_null<Element*> parent, @@ -29,7 +30,8 @@ public: void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; - bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override { return true; } bool dragItemByHandler(const ClickHandlerPtr &p) const override { @@ -43,16 +45,6 @@ public: return false; } - const QString &fname() const { - return _fname; - } - const QString &lname() const { - return _lname; - } - const QString &phone() const { - return _phone; - } - // Should be called only by Data::Session. void updateSharedContactUserId(UserId userId) override; @@ -62,18 +54,40 @@ public: private: QSize countOptimalSize() override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, bool pressed) override; + + [[nodiscard]] QMargins inBubblePadding() const; + [[nodiscard]] QMargins innerMargin() const; + [[nodiscard]] int bottomInfoPadding() const; + + [[nodiscard]] TextSelection toTitleSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection toDescriptionSelection( + TextSelection selection) const; + + const style::QuoteStyle &_st; + const int _pixh; + UserId _userId = 0; UserData *_contact = nullptr; - int _phonew = 0; - QString _fname, _lname, _phone; - Ui::Text::String _name; + Ui::Text::String _nameLine; + Ui::Text::String _phoneLine; + Ui::Text::String _infoLine; + + struct Button { + QString text; + int width = 0; + ClickHandlerPtr link; + mutable std::unique_ptr<Ui::RippleAnimation> ripple; + }; + std::vector<Button> _buttons; + Button _mainButton; + std::unique_ptr<Ui::EmptyUserpic> _photoEmpty; mutable Ui::PeerUserpicView _userpic; - - ClickHandlerPtr _linkl; - int _linkw = 0; - QString _link; + mutable QPoint _lastPoint; }; From afdd22d15404bedc88ff88448d7e13afcef8e7dd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 Feb 2024 15:17:59 +0300 Subject: [PATCH 019/108] Fixed phone formatting generally. --- .../SourceFiles/history/view/media/history_view_contact.cpp | 2 +- Telegram/SourceFiles/ui/text/format_values.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 908bf1c0c..a6cb9635c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -97,7 +97,7 @@ Contact::Contact( _phoneLine.setText( st::webPageDescriptionStyle, - Ui::FormatPhone(QString(phone).replace(QChar('+'), QString())), + Ui::FormatPhone(phone), Ui::WebpageTextTitleOptions()); #if 0 // No info. diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index c5dd7763b..e792e6560 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -388,7 +388,9 @@ QString FormatPhone(const QString &phone) { if (phone.at(0) == '0') { return phone; } - return Countries::Instance().format({ .phone = phone }).formatted; + return Countries::Instance().format({ + .phone = (phone.at(0) == '+') ? phone.mid(1) : phone, + }).formatted; } QString FormatTTL(float64 ttl) { From ea20e41f1daf42c75455b429de212e902ecb9d82 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 Feb 2024 15:18:35 +0300 Subject: [PATCH 020/108] Added drag text to contact view. --- .../view/media/history_view_contact.cpp | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index a6cb9635c..1a3e35716 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -32,8 +32,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -ClickHandlerPtr SendMessageClickHandler(PeerData *peer) { - return std::make_shared<LambdaClickHandler>([peer](ClickContext context) { +class ContactClickHandler : public LambdaClickHandler { +public: + using LambdaClickHandler::LambdaClickHandler; + + void setDragText(const QString &t) { + _dragText = t; + } + + QString dragText() const override { + return _dragText; + } + +private: + QString _dragText; + +}; + +ClickHandlerPtr SendMessageClickHandler(not_null<PeerData*> peer) { + const auto clickHandlerPtr = std::make_shared<ContactClickHandler>([peer]( + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); if (const auto controller = my.sessionWindow.get()) { if (controller->session().uniqueId() @@ -45,30 +63,44 @@ ClickHandlerPtr SendMessageClickHandler(PeerData *peer) { Window::SectionShow::Way::Forward); } }); + if (const auto user = peer->asUser()) { + clickHandlerPtr->setDragText(user->phone().isEmpty() + ? peer->name() + : Ui::FormatPhone(user->phone())); + } + return clickHandlerPtr; } ClickHandlerPtr AddContactClickHandler(not_null<HistoryItem*> item) { const auto session = &item->history()->session(); - const auto fullId = item->fullId(); - return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto sharedContact = [=, fullId = item->fullId()] { + if (const auto item = session->data().message(fullId)) { + if (const auto media = item->media()) { + return media->sharedContact(); + } + } + return (const Data::SharedContact *)nullptr; + }; + const auto clickHandlerPtr = std::make_shared<ContactClickHandler>([=]( + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); if (const auto controller = my.sessionWindow.get()) { if (controller->session().uniqueId() != session->uniqueId()) { return; } - if (const auto item = session->data().message(fullId)) { - if (const auto media = item->media()) { - if (const auto contact = media->sharedContact()) { - controller->show(Box<AddContactBox>( - session, - contact->firstName, - contact->lastName, - contact->phoneNumber)); - } - } + if (const auto contact = sharedContact()) { + controller->show(Box<AddContactBox>( + session, + contact->firstName, + contact->lastName, + contact->phoneNumber)); } } }); + if (const auto contact = sharedContact()) { + clickHandlerPtr->setDragText(Ui::FormatPhone(contact->phoneNumber)); + } + return clickHandlerPtr; } } // namespace From 58443bc197474a2830c63ae6e6efba68a9e2ef43 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 Feb 2024 15:33:51 +0300 Subject: [PATCH 021/108] Fixed ability to schedule forwarded messages without comment. --- .../history/view/history_view_scheduled_section.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 6e019e6eb..af2008ed8 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -563,7 +563,7 @@ Api::SendAction ScheduledWidget::prepareSendAction( void ScheduledWidget::send() { const auto textWithTags = _composeControls->getTextWithAppliedMarkdown(); - if (textWithTags.text.isEmpty()) { + if (textWithTags.text.isEmpty() && !_composeControls->readyToForward()) { return; } @@ -592,6 +592,7 @@ void ScheduledWidget::send(Api::SendOptions options) { session().api().sendMessage(std::move(message)); + _composeControls->cancelForward(); _composeControls->clear(); //_saveDraftText = true; //_saveDraftStart = crl::now(); From d252427e34f95c5efabc0310946f6461b4d828df Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 27 Feb 2024 02:09:39 +0300 Subject: [PATCH 022/108] Added blockquote to white list for message links parser. --- Telegram/SourceFiles/chat_helpers/message_field.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 0cef46221..5715eea45 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -727,7 +727,8 @@ void MessageLinksParser::parse() { || (tag == Ui::InputField::kTagItalic) || (tag == Ui::InputField::kTagUnderline) || (tag == Ui::InputField::kTagStrikeOut) - || (tag == Ui::InputField::kTagSpoiler); + || (tag == Ui::InputField::kTagSpoiler) + || (tag == Ui::InputField::kTagBlockquote); }; _ranges.clear(); From a77c547a626251c9b43d1822dbb2e9f1679b07e1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 27 Feb 2024 04:19:15 +0300 Subject: [PATCH 023/108] Fixed edit of long media caption with emoji. --- .../SourceFiles/history/history_widget.cpp | 30 +++++++++---------- .../history_view_compose_controls.cpp | 9 ++++-- .../controls/history_view_compose_controls.h | 1 + .../view/history_view_replies_section.cpp | 26 ++++++++-------- .../view/history_view_scheduled_section.cpp | 26 ++++++++-------- Telegram/lib_ui | 2 +- 6 files changed, 49 insertions(+), 45 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 47db26aac..31667caef 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1953,6 +1953,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { updateControlsVisibility(); updateControlsGeometry(); refreshTopBarActiveChat(); + checkCharsLimitation(); if (_editMsgId) { updateReplyEditTexts(); if (!_replyEditMsg) { @@ -3869,17 +3870,12 @@ void HistoryWidget::saveEditMsg() { return; } const auto webPageDraft = _preview->draft(); - auto left = prepareTextForEditMsg(); - auto sending = TextWithEntities(); + const auto sending = prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) + if (sending.text.isEmpty() && (webPageDraft.removed || webPageDraft.url.isEmpty() || !webPageDraft.manual) @@ -3888,11 +3884,16 @@ void HistoryWidget::saveEditMsg() { controller()->show( Box<DeleteMessagesBox>(item, suggestModerateActions)); return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } } const auto weak = Ui::MakeWeak(this); @@ -7319,9 +7320,8 @@ void HistoryWidget::checkCharsLimitation() { _charsLimitation = nullptr; return; } - const auto limits = Data::PremiumLimits(&session()); - const auto left = prepareTextForEditMsg(); - const auto remove = left.text.size() - limits.captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) + - Data::PremiumLimits(&session()).captionLengthCurrent(); if (remove > 0) { if (!_charsLimitation) { _charsLimitation = base::make_unique_q<CharactersLimitLabel>( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index c8d47451c..cabfedbab 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -3150,6 +3150,10 @@ not_null<Ui::RpWidget*> ComposeControls::likeAnimationTarget() const { return _like; } +int ComposeControls::fieldCharacterCount() const { + return Ui::FieldCharacterCount(_field); +} + bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); @@ -3323,9 +3327,8 @@ void ComposeControls::checkCharsLimitation() { _charsLimitation = nullptr; return; } - const auto limits = Data::PremiumLimits(&session()); - const auto left = prepareTextForEditMsg(); - const auto remove = left.text.size() - limits.captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) + - Data::PremiumLimits(&session()).captionLengthCurrent(); if (remove > 0) { if (!_charsLimitation) { using namespace Controls; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index b1bc012ca..d5985d6cb 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -228,6 +228,7 @@ public: [[nodiscard]] rpl::producer<bool> hasSendTextValue() const; [[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const; [[nodiscard]] not_null<Ui::RpWidget*> likeAnimationTarget() const; + [[nodiscard]] int fieldCharacterCount() const; [[nodiscard]] TextWithEntities prepareTextForEditMsg() const; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 8ad662925..d8482c9d6 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1170,29 +1170,29 @@ void RepliesWidget::edit( return; } const auto webpage = _composeControls->webPageDraft(); - auto sending = TextWithEntities(); - auto left = _composeControls->prepareTextForEditMsg(); + const auto sending = _composeControls->prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) - && !hasMediaWithCaption) { + if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { controller()->show(Box<DeleteMessagesBox>(item, false)); } else { doSetInnerFocus(); } return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = _composeControls->fieldCharacterCount() + - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } } lifetime().add([=] { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index af2008ed8..ca1fff7c9 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -635,29 +635,29 @@ void ScheduledWidget::edit( return; } const auto webpage = _composeControls->webPageDraft(); - auto sending = TextWithEntities(); - auto left = _composeControls->prepareTextForEditMsg(); + const auto sending = _composeControls->prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) - && !hasMediaWithCaption) { + if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { controller()->show(Box<DeleteMessagesBox>(item, false)); } else { _composeControls->focus(); } return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = _composeControls->fieldCharacterCount() + - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } } lifetime().add([=] { diff --git a/Telegram/lib_ui b/Telegram/lib_ui index d42475113..333587d95 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit d4247511355a666903e9a57d821b1eb58884aade +Subproject commit 333587d95edefcae1ebaf8838d3f499639fc2de8 From 08717dcd78cc02d609224becdbf21992215c59a8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 27 Feb 2024 05:05:00 +0300 Subject: [PATCH 024/108] Added counter label of characters limit to edit mode without media. --- Telegram/SourceFiles/history/history_widget.cpp | 10 +++++++--- .../view/controls/history_view_compose_controls.cpp | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 31667caef..f288a1883 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7316,12 +7316,16 @@ void HistoryWidget::checkCharsLimitation() { return; } const auto item = session().data().message(_history->peer, _editMsgId); - if (!item || !item->media() || !item->media()->allowsEditCaption()) { + if (!item) { _charsLimitation = nullptr; return; } - const auto remove = Ui::FieldCharacterCount(_field) - - Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto hasMediaWithCaption = item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; if (remove > 0) { if (!_charsLimitation) { _charsLimitation = base::make_unique_q<CharactersLimitLabel>( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index cabfedbab..69f0f2de8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -3323,12 +3323,16 @@ void ComposeControls::checkCharsLimitation() { return; } const auto item = _history->owner().message(_header->editMsgId()); - if (!item || !item->media() || !item->media()->allowsEditCaption()) { + if (!item) { _charsLimitation = nullptr; return; } - const auto remove = Ui::FieldCharacterCount(_field) - - Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto hasMediaWithCaption = item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; if (remove > 0) { if (!_charsLimitation) { using namespace Controls; From 5dc6bdcc42ffb3bf966f0e5267f31cf45fbf3977 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 27 Feb 2024 05:53:48 +0300 Subject: [PATCH 025/108] Fixed reply bar stuck when sending file in replies section. --- .../SourceFiles/history/view/history_view_replies_section.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index d8482c9d6..fc556245b 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -967,7 +967,8 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage() == action.replyTo) { + if (_composeControls->replyingToMessage().messageId + == action.replyTo.messageId) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } From aec4857e7b6dc70765e91f74d3724bd77c2d7297 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 1 Mar 2024 03:50:11 +0300 Subject: [PATCH 026/108] Fixed format of phone number in intro widget. --- Telegram/SourceFiles/intro/intro_phone.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 7ac420956..5e8261674 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -32,13 +32,18 @@ namespace Intro { namespace details { namespace { -bool AllowPhoneAttempt(const QString &phone) { +[[nodiscard]] bool AllowPhoneAttempt(const QString &phone) { const auto digits = ranges::count_if( phone, [](QChar ch) { return ch.isNumber(); }); return (digits > 1); } +[[nodiscard]] QString DigitsOnly(QString value) { + static const auto RegExp = QRegularExpression("[^0-9]"); + return value.replace(RegExp, QString()); +} + } // namespace PhoneWidget::PhoneWidget( @@ -168,16 +173,12 @@ void PhoneWidget::submit() { cancelNearestDcRequest(); // Check if such account is authorized already. - const auto digitsOnly = [](QString value) { - static const auto RegExp = QRegularExpression("[^0-9]"); - return value.replace(RegExp, QString()); - }; - const auto phoneDigits = digitsOnly(phone); + const auto phoneDigits = DigitsOnly(phone); for (const auto &[index, existing] : Core::App().domain().accounts()) { const auto raw = existing.get(); if (const auto session = raw->maybeSession()) { if (raw->mtp().environment() == account().mtp().environment() - && digitsOnly(session->user()->phone()) == phoneDigits) { + && DigitsOnly(session->user()->phone()) == phoneDigits) { crl::on_main(raw, [=] { Core::App().domain().activate(raw); }); @@ -231,7 +232,7 @@ void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) { result.match([&](const MTPDauth_sentCode &data) { fillSentCodeData(data); - getData()->phone = _sentPhone; + getData()->phone = DigitsOnly(_sentPhone); getData()->phoneHash = qba(data.vphone_code_hash()); const auto next = data.vnext_type(); if (next && next->type() == mtpc_auth_codeTypeCall) { From c0c330a1503221304f09ffbf6447eb372af163af Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 1 Mar 2024 04:55:20 +0300 Subject: [PATCH 027/108] Fixed bubble color under strip of emoji pad for reactions. --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index a39019a93..a622c37ea 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -608,7 +608,7 @@ defaultComposeIcons: ComposeIcons { stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, - { "chat/reactions_bubble", windowBg }, + { "chat/reactions_bubble", emojiPanBg }, }; stripExpandPanel: icon{ { "chat/reactions_round_big", windowBgRipple }, From b790847fded28fe8bb8a39f2306a286f8311c84e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 1 Mar 2024 07:24:22 +0300 Subject: [PATCH 028/108] Added ability to close call panel without hanging up call. --- Telegram/SourceFiles/calls/calls_panel.cpp | 18 +++++++++++------- Telegram/SourceFiles/calls/calls_panel.h | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 8ee43eeda..05fdaaff2 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "apiwrap.h" #include "platform/platform_specific.h" +#include "base/event_filter.h" #include "base/platform/base_platform_info.h" #include "base/power_save_blocker.h" #include "media/streaming/media_streaming_utility.h" @@ -147,17 +148,18 @@ void Panel::initWindow() { window()->setTitle(_user->name()); window()->setTitleStyle(st::callTitle); - window()->events( - ) | rpl::start_with_next([=](not_null<QEvent*> e) { - if (e->type() == QEvent::Close) { - handleClose(); + base::install_event_filter(window().get(), [=](not_null<QEvent*> e) { + if (e->type() == QEvent::Close && handleClose()) { + e->ignore(); + return base::EventFilterResult::Cancel; } else if (e->type() == QEvent::KeyPress) { if ((static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) && window()->isFullScreen()) { window()->showNormal(); } } - }, window()->lifetime()); + return base::EventFilterResult::Continue; + }); window()->setBodyTitleArea([=](QPoint widgetPoint) { using Flag = Ui::WindowTitleHitTestFlag; @@ -828,10 +830,12 @@ void Panel::paint(QRect clip) { } } -void Panel::handleClose() { +bool Panel::handleClose() const { if (_call) { - _call->hangup(); + window()->hide(); + return true; } + return false; } not_null<Ui::RpWindow*> Panel::window() const { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index dc715584a..c98537eb9 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -106,7 +106,7 @@ private: void initLayout(); void initGeometry(); - void handleClose(); + [[nodiscard]] bool handleClose() const; void updateControlsGeometry(); void updateHangupGeometry(); From a704611705dfbf075c35a647135c507c2159b975 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 1 Mar 2024 15:24:40 +0300 Subject: [PATCH 029/108] Fixed ability to create vertical drum picker with first chosen item. --- Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp | 5 +++-- Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp index d0cc71ca3..58fd123ea 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp @@ -82,9 +82,10 @@ VerticalDrumPicker::VerticalDrumPicker( ) | rpl::start_with_next([=](const QSize &s) { _itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight); _itemsVisible.centerOffset = _itemsVisible.count / 2; - if (_pendingStartIndex && _itemsVisible.count) { - _index = normalizedIndex(base::take(_pendingStartIndex) + if ((_pendingStartIndex >= 0) && _itemsVisible.count) { + _index = normalizedIndex(_pendingStartIndex - _itemsVisible.centerOffset); + _pendingStartIndex = -1; } if (!_loopData.looped) { diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h index 802f72fba..4140d3397 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h @@ -75,7 +75,7 @@ private: PaintItemCallback _paintCallback; - int _pendingStartIndex = 0; + int _pendingStartIndex = -1; struct { int count = 0; From ef474f0dc8520b092c9712c2059246f36ee002ac Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 5 Mar 2024 17:09:25 +0300 Subject: [PATCH 030/108] Fixed opening of local links from webview bots in appropriate window. --- Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 9aba7d65b..39c48f7ad 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -566,9 +566,16 @@ bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) { if (!keepOpen) { botClose(); } - crl::on_main([=, shownUrl = _lastShownUrl] { + crl::on_main([=, shownUrl = _lastShownUrl, bot = _bot] { + if (bot->session().windows().empty()) { + Core::App().domain().activate(&bot->session().account()); + } + const auto window = !bot->session().windows().empty() + ? bot->session().windows().front() + : nullptr; const auto variant = QVariant::fromValue(ClickHandlerContext{ .attachBotWebviewUrl = shownUrl, + .sessionWindow = window, }); UrlClickHandler::Open(local, variant); }); From 48eb408fb8f17780925dcafda23e8942a1ea5c29 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 6 Mar 2024 02:32:18 +0300 Subject: [PATCH 031/108] Fixed text elision of vote amount in polls with reactions. --- .../SourceFiles/history/view/media/history_view_poll.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 7e96e068f..82ca69e8f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -803,9 +803,11 @@ void Poll::paintInlineFooter( p, left, top, - std::min( - _totalVotesLabel.maxWidth(), - paintw - _parent->bottomInfoFirstLineWidth()), + _parent->data()->reactions().empty() + ? std::min( + _totalVotesLabel.maxWidth(), + paintw - _parent->bottomInfoFirstLineWidth()) + : _totalVotesLabel.maxWidth(), width()); } From eab249fc138a07aa2d65210e7b156cbb1d7d583d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 6 Mar 2024 19:40:01 +0300 Subject: [PATCH 032/108] Fixed countdown label for input field of bio. --- Telegram/SourceFiles/history/history_widget.cpp | 6 ++++++ Telegram/SourceFiles/settings/settings_information.cpp | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index f288a1883..b658b8555 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -3892,7 +3892,13 @@ void HistoryWidget::saveEditMsg() { if (remove > 0) { controller()->showToast( tr::lng_edit_limit_reached(tr::now, lt_count, remove)); +#ifndef _DEBUG return; +#else + if (!base::IsCtrlPressed()) { + return; + } +#endif } } diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index ef6068987..6a46992cc 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -483,7 +483,8 @@ void SetupBio( } changed->fire(*current != text); const auto limit = self->isPremium() ? premiumLimit : defaultLimit; - const auto countLeft = limit - int(text.size()); + const auto countLeft = limit + - bio->lastTextSizeWithoutSurrogatePairsCount(); countdown->setText(QString::number(countLeft)); countdown->setTextColorOverride( countLeft < 0 ? st::boxTextFgError->c : std::optional<QColor>()); From 09285bc9cd7c3f8e797bd6af046490b49e650e3b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 7 Mar 2024 02:27:15 +0300 Subject: [PATCH 033/108] Moved out minimal levels of boosts for channel settings to single place. --- .../boxes/background_preview_box.cpp | 15 +--- .../boxes/peers/edit_peer_color_box.cpp | 12 +-- .../boxes/peers/replace_boost_box.cpp | 26 ++++--- Telegram/SourceFiles/boxes/stickers_box.cpp | 9 +-- .../SourceFiles/data/data_premium_limits.cpp | 76 +++++++++++++++++++ .../SourceFiles/data/data_premium_limits.h | 22 ++++++ Telegram/SourceFiles/data/data_session.cpp | 18 ++--- 7 files changed, 129 insertions(+), 49 deletions(-) diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 6dadab562..8406657e6 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -31,8 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_message.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "apiwrap.h" #include "data/data_session.h" @@ -42,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_resolver.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" +#include "data/data_premium_limits.h" #include "settings/settings_premium.h" #include "storage/file_upload.h" #include "storage/localimageloader.h" @@ -699,16 +698,10 @@ void BackgroundPreviewBox::checkLevelForChannel() { if (!weak) { return std::optional<Ui::AskBoostReason>(); } - const auto appConfig = &_forPeer->session().account().appConfig(); - const auto defaultRequired = appConfig->get<int>( - "channel_wallpaper_level_min", - 9); - const auto customRequired = appConfig->get<int>( - "channel_custom_wallpaper_level_min", - 10); + const auto limits = Data::LevelLimits(&_forPeer->session()); const auto required = _paperEmojiId.isEmpty() - ? customRequired - : defaultRequired; + ? limits.channelCustomWallpaperLevelMin() + : limits.channelWallpaperLevelMin(); if (level >= required) { applyForPeer(false); return std::optional<Ui::AskBoostReason>(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 2627d2342..e62aaac54 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_emoji_statuses.h" #include "data/data_file_origin.h" #include "data/data_peer.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "history/view/history_view_element.h" @@ -34,8 +35,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "lottie/lottie_single_player.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "settings/settings_premium.h" @@ -541,16 +540,13 @@ void Apply( : peerColors->requiredChannelLevelFor( peer->id, values.colorIndex); + const auto limits = Data::LevelLimits(&peer->session()); const auto iconRequired = values.backgroundEmojiId - ? session->account().appConfig().get<int>( - "channel_bg_icon_level_min", - 5) + ? limits.channelBgIconLevelMin() : 0; const auto statusRequired = (values.statusChanged && values.statusId) - ? session->account().appConfig().get<int>( - "channel_emoji_status_level_min", - 8) + ? limits.channelEmojiStatusLevelMin() : 0; const auto required = std::max({ colorRequired, diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index ef99b3a90..36f18ab23 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/event_filter.h" #include "base/unixtime.h" +#include "data/data_premium_limits.h" #include "boxes/peer_list_box.h" #include "data/data_channel.h" #include "data/data_cloud_themes.h" @@ -447,22 +448,23 @@ Ui::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) { if (themes.empty()) { channel->owner().cloudThemes().refreshChatThemes(); } + const auto levelLimits = Data::LevelLimits(&channel->session()); return Ui::BoostFeatures{ .nameColorsByLevel = std::move(nameColorsByLevel), .linkStylesByLevel = std::move(linkStylesByLevel), - .linkLogoLevel = get(u"channel_bg_icon_level_min"_q, 4, !group), - .transcribeLevel = get(u"group_transcribe_level_min"_q, 6, group), - .emojiPackLevel = get(u"group_emoji_stickers_level_min"_q, 4, group), - .emojiStatusLevel = get(group - ? u"group_emoji_status_level_min"_q - : u"channel_emoji_status_level_min"_q, 8), - .wallpaperLevel = get(group - ? u"group_wallpaper_level_min"_q - : u"channel_wallpaper_level_min"_q, 9), + .linkLogoLevel = group ? 0 : levelLimits.channelBgIconLevelMin(), + .transcribeLevel = group ? levelLimits.groupTranscribeLevelMin() : 0, + .emojiPackLevel = group ? levelLimits.groupEmojiStickersLevelMin() : 0, + .emojiStatusLevel = group + ? levelLimits.groupEmojiStatusLevelMin() + : levelLimits.channelEmojiStatusLevelMin(), + .wallpaperLevel = group + ? levelLimits.groupWallpaperLevelMin() + : levelLimits.channelWallpaperLevelMin(), .wallpapersCount = themes.empty() ? 8 : int(themes.size()), - .customWallpaperLevel = get(group - ? u"channel_custom_wallpaper_level_min"_q - : u"group_custom_wallpaper_level_min"_q, 10), + .customWallpaperLevel = group + ? levelLimits.groupCustomWallpaperLevelMin() + : levelLimits.channelCustomWallpaperLevelMin(), }; } diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 8a88acfba..7d72ebf5a 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_file_origin.h" #include "data/data_document_media.h" +#include "data/data_premium_limits.h" #include "data/stickers/data_stickers.h" #include "core/application.h" #include "lang/lang_keys.h" @@ -40,8 +41,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/unread_badge_paint.h" #include "media/clip/media_clip_reader.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -2050,10 +2049,8 @@ void StickersBox::Inner::checkGroupLevel(Fn<void()> done) { return std::optional<Ui::AskBoostReason>(); } _checkingGroupLevel = false; - const auto appConfig = &peer->session().account().appConfig(); - const auto required = appConfig->get<int>( - "group_emoji_stickers_level_min", - 4); + const auto required = Data::LevelLimits( + &peer->session()).groupEmojiStickersLevelMin(); if (level >= required) { save(); return std::optional<Ui::AskBoostReason>(); diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 443474040..ecca86149 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -217,4 +217,80 @@ bool PremiumLimits::isPremium() const { return _session->premium(); } +LevelLimits::LevelLimits(not_null<Main::Session*> session) +: _session(session) { +} + +int LevelLimits::channelColorLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_color_level_min"_q, + 5); +} + +int LevelLimits::channelBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_bg_icon_level_min"_q, + 4); +} + +int LevelLimits::channelProfileBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_profile_bg_icon_level_min"_q, + 7); +} + +int LevelLimits::channelEmojiStatusLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::channelWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::channelCustomWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_custom_wallpaper_level_min"_q, + 10); +} + +int LevelLimits::groupTranscribeLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_transcribe_level_min"_q, + 6); +} + +int LevelLimits::groupEmojiStickersLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_emoji_stickers_level_min"_q, + 4); +} + +int LevelLimits::groupProfileBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_profile_bg_icon_level_min"_q, + 5); +} + +int LevelLimits::groupEmojiStatusLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::groupWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::groupCustomWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_custom_wallpaper_level_min"_q, + 10); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index bc3c86d9f..5c50d7a12 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -91,4 +91,26 @@ private: }; +class LevelLimits final { +public: + LevelLimits(not_null<Main::Session*> session); + + [[nodiscard]] int channelColorLevelMin() const; + [[nodiscard]] int channelBgIconLevelMin() const; + [[nodiscard]] int channelProfileBgIconLevelMin() const; + [[nodiscard]] int channelEmojiStatusLevelMin() const; + [[nodiscard]] int channelWallpaperLevelMin() const; + [[nodiscard]] int channelCustomWallpaperLevelMin() const; + [[nodiscard]] int groupTranscribeLevelMin() const; + [[nodiscard]] int groupEmojiStickersLevelMin() const; + [[nodiscard]] int groupProfileBgIconLevelMin() const; + [[nodiscard]] int groupEmojiStatusLevelMin() const; + [[nodiscard]] int groupWallpaperLevelMin() const; + [[nodiscard]] int groupCustomWallpaperLevelMin() const; + +private: + const not_null<Main::Session*> _session; + +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c8235eac4..e7ec13242 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -239,10 +239,8 @@ Session::Session(not_null<Main::Session*> session) _session->local().cacheBigFilePath(), _session->local().cacheBigFileSettings())) , _groupFreeTranscribeLevel(session->account().appConfig().value( -) | rpl::map([=] { - return session->account().appConfig().get<int>( - u"group_transcribe_level_min"_q, - 6); +) | rpl::map([limits = Data::LevelLimits(session)] { + return limits.groupTranscribeLevelMin(); })) , _chatsList( session, @@ -2185,8 +2183,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([folder, limits = Data::PremiumLimits(_session)] { return folder ? limits.dialogsFolderPinnedPremium() : limits.dialogsPinnedPremium(); @@ -2200,8 +2197,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.dialogFiltersChatsPremium(); }); } @@ -2209,8 +2205,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue( rpl::producer<int> Session::maxPinnedChatsLimitValue( not_null<Data::Forum*> forum) const { return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.topicsPinnedCurrent(); }); } @@ -2222,8 +2217,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.savedSublistsPinnedPremium(); }); } From f56b16c6efb4162c0e72a54c053b921843d1a8ad Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 7 Mar 2024 02:27:03 +0300 Subject: [PATCH 034/108] Added initial badges for minimal level of boosts to channel settings. --- .../boxes/peers/edit_peer_color_box.cpp | 197 ++++++++++++++++-- Telegram/SourceFiles/settings/settings.style | 2 + 2 files changed, 187 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index e62aaac54..8fccba41a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_box.h" #include "boxes/stickers_box.h" #include "chat_helpers/compose/compose_show.h" +#include "core/ui_integration.h" // Core::MarkedTextContext. #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "data/data_changes.h" @@ -42,10 +43,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/path_shift_gradient.h" +#include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/vertical_list.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" @@ -145,6 +148,28 @@ private: }; +class LevelBadge final : public Ui::RpWidget { +public: + LevelBadge( + not_null<QWidget*> parent, + uint32 level, + not_null<Main::Session*> session); + + void setMinimal(bool value); + +private: + void paintEvent(QPaintEvent *e) override; + + void updateText(); + + const uint32 _level; + const TextWithEntities _icon; + const Core::MarkedTextContext _context; + Ui::Text::String _text; + bool _minimal = false; + +}; + ColorSample::ColorSample( not_null<QWidget*> parent, std::shared_ptr<Ui::ChatStyle> style, @@ -436,6 +461,108 @@ HistoryView::Context PreviewDelegate::elementContext() { return HistoryView::Context::AdminLog; } +LevelBadge::LevelBadge( + not_null<QWidget*> parent, + uint32 level, + not_null<Main::Session*> session) +: Ui::RpWidget(parent) +, _level(level) +, _icon(Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::settingsLevelBadgeLock, + QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0), + false))) +, _context({ .session = session }) { + updateText(); +} + +void LevelBadge::updateText() { + auto text = _icon; + text.append(' '); + if (!_minimal) { + text.append(tr::lng_boost_level( + tr::now, + lt_count, + _level, + Ui::Text::WithEntities)); + } else { + text.append(QString::number(_level)); + } + const auto &st = st::settingsPremiumNewBadge.style; + _text.setMarkedText( + st, + text, + kMarkupTextOptions, + _context); + const auto &padding = st::settingsColorSamplePadding; + QWidget::resize( + _text.maxWidth() + rect::m::sum::h(padding), + st.font->height + rect::m::sum::v(padding)); +} + +void LevelBadge::setMinimal(bool value) { + if ((value != _minimal) && value) { + _minimal = value; + updateText(); + update(); + } +} + +void LevelBadge::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + + const auto radius = height() / 2; + p.setPen(Qt::NoPen); + auto gradient = QLinearGradient(QPointF(0, 0), QPointF(width(), 0)); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setBrush(gradient); + p.drawRoundedRect(rect(), radius, radius); + + p.setPen(st::premiumButtonFg); + p.setBrush(Qt::NoBrush); + + const auto context = Ui::Text::PaintContext{ + .position = rect::m::pos::tl(st::settingsColorSamplePadding), + .outerWidth = width(), + .availableWidth = width(), + }; + _text.draw(p, context); +} + +void AddLevelBadge( + int level, + not_null<Ui::SettingsButton*> button, + Ui::RpWidget *right, + not_null<ChannelData*> channel, + const QMargins &padding, + rpl::producer<QString> text) { + if (channel->levelHint() >= level) { + return; + } + const auto badge = Ui::CreateChild<LevelBadge>( + button.get(), + level, + &channel->session()); + badge->show(); + const auto sampleLeft = st::settingsColorSamplePadding.left(); + const auto badgeLeft = padding.left() + sampleLeft; + rpl::combine( + button->sizeValue(), + std::move(text) + ) | rpl::start_with_next([=](const QSize &s, const QString &) { + if (s.isNull()) { + return; + } + badge->moveToLeft( + button->fullTextWidth() + badgeLeft, + (s.height() - badge->height()) / 2); + const auto rightEdge = right ? right->pos().x() : button->width(); + badge->setMinimal((rect::right(badge) + sampleLeft) > rightEdge); + badge->setVisible((rect::right(badge) + sampleLeft) < rightEdge); + }, badge->lifetime()); +} + struct SetValues { uint8 colorIndex = 0; DocumentId backgroundEmojiId = 0; @@ -722,6 +849,7 @@ struct ButtonWithEmoji { not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<Ui::ChatStyle> style, + not_null<PeerData*> peer, rpl::producer<uint8> colorIndexValue, rpl::producer<DocumentId> emojiIdValue, Fn<void(DocumentId)> emojiIdChosen) { @@ -823,21 +951,33 @@ struct ButtonWithEmoji { } }); + if (const auto channel = peer->asChannel()) { + AddLevelBadge( + Data::LevelLimits(&channel->session()).channelBgIconLevelMin(), + raw, + right, + channel, + button.st->padding, + tr::lng_settings_color_emoji()); + } + return result; } [[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiStatusButton( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, + not_null<ChannelData*> channel, rpl::producer<DocumentId> statusIdValue, Fn<void(DocumentId,TimeId)> statusIdChosen, bool group) { const auto button = ButtonStyleWithRightEmoji(parent); + const auto &phrase = group + ? tr::lng_edit_channel_status_group + : tr::lng_edit_channel_status; auto result = Settings::CreateButtonWithIcon( parent, - (group - ? tr::lng_edit_channel_status_group() - : tr::lng_edit_channel_status()), + phrase(), *button.st, { &st::menuBlueIconEmojiStatus }); const auto raw = result.data(); @@ -922,6 +1062,17 @@ struct ButtonWithEmoji { } }); + const auto limits = Data::LevelLimits(&channel->session()); + AddLevelBadge( + (group + ? limits.groupEmojiStatusLevelMin() + : limits.channelEmojiStatusLevelMin()), + raw, + right, + channel, + button.st->padding, + phrase()); + return result; } @@ -1032,6 +1183,14 @@ struct ButtonWithEmoji { } }, right->lifetime()); + AddLevelBadge( + Data::LevelLimits(&channel->session()).groupEmojiStickersLevelMin(), + raw, + right, + channel, + button.st->padding, + tr::lng_group_emoji()); + return result; } @@ -1075,10 +1234,7 @@ void EditPeerColorBox( verticalLayout, { .name = u"palette"_q, - .sizeOverride = { - st::settingsCloudPasswordIconSize, - st::settingsCloudPasswordIconSize, - }, + .sizeOverride = Size(st::settingsCloudPasswordIconSize), }, st::peerAppearanceIconPadding); box->setShowFinishedCallback([animate = std::move(icon.animate)] { @@ -1131,6 +1287,7 @@ void EditPeerColorBox( container, show, style, + peer, state->index.value(), state->emojiId.value(), [=](DocumentId id) { state->emojiId = id; })); @@ -1146,20 +1303,35 @@ void EditPeerColorBox( if (const auto channel = peer->asChannel()) { Ui::AddSkip(container, st::settingsColorSampleSkip); - Settings::AddButtonWithIcon( + const auto &phrase = group + ? tr::lng_edit_channel_wallpaper_group + : tr::lng_edit_channel_wallpaper; + const auto button = Settings::AddButtonWithIcon( container, - (group - ? tr::lng_edit_channel_wallpaper_group() - : tr::lng_edit_channel_wallpaper()), + phrase(), st::peerAppearanceButton, { &st::menuBlueIconWallpaper } - )->setClickedCallback([=] { + ); + button->setClickedCallback([=] { const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto strong = show->resolveWindow(usage)) { show->show(Box<BackgroundBox>(strong, channel)); } }); + { + const auto limits = Data::LevelLimits(&channel->session()); + AddLevelBadge( + group + ? limits.groupCustomWallpaperLevelMin() + : limits.channelCustomWallpaperLevelMin(), + button, + nullptr, + channel, + st::peerAppearanceButton.padding, + phrase()); + } + Ui::AddSkip(container, st::settingsColorSampleSkip); Ui::AddDividerText( container, @@ -1197,6 +1369,7 @@ void EditPeerColorBox( container->add(CreateEmojiStatusButton( container, show, + channel, state->statusId.value(), [=](DocumentId id, TimeId until) { state->statusId = id; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index f45d68c0b..9716a383e 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -562,6 +562,8 @@ settingsColorButton: SettingsButton(settingsButton) { settingsColorRadioMargin: 17px; settingsColorRadioSkip: 13px; settingsColorRadioStroke: 2px; +settingsLevelBadgeLock: icon {{ "chat/mini_lock", premiumButtonFg }}; +settingsLevelBadgeLockSkip: 4px; messagePrivacyTopSkip: 8px; messagePrivacyRadioSkip: 6px; From 02e1c03ed911d5bb43d6ded06f16cbb3c22b91f2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 7 Mar 2024 16:36:07 +0300 Subject: [PATCH 035/108] Fixed position of connecting state widget when forum is opened. Fixed #27548. --- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 6ce80f8a9..6d85f7cef 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -2964,7 +2964,9 @@ void Widget::updateControlsGeometry() { if (_connecting) { _connecting->setBottomSkip(bottomSkip); } - controller()->setConnectingBottomSkip(bottomSkip); + if (_layout != Layout::Child) { + controller()->setConnectingBottomSkip(bottomSkip); + } const auto wasScrollTop = _scroll->scrollTop(); const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0) From 27bd9e3ee53a1f8688a8a5bf565beb849b42b3a8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 7 Mar 2024 17:41:30 +0300 Subject: [PATCH 036/108] Added icons to buttons for privacy settings that require premium. --- .../settings/settings_privacy_security.cpp | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index feeb83a66..3f80fe54a 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_privacy_security.h" #include "api/api_authorizations.h" -#include "api/api_blocked_peers.h" #include "api/api_cloud_password.h" #include "api/api_self_destruct.h" #include "api/api_sensitive_content.h" @@ -24,31 +23,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_privacy_controllers.h" #include "settings/settings_websites.h" #include "base/timer_rpl.h" -#include "boxes/edit_privacy_box.h" #include "boxes/passcode_box.h" -#include "boxes/auto_lock_box.h" #include "boxes/sessions_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/self_destruction_box.h" #include "core/application.h" #include "core/core_settings.h" #include "ui/chat/chat_style.h" +#include "ui/effects/premium_top_bar.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" -#include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/fade_wrap.h" #include "ui/widgets/shadow.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" -#include "ui/layers/generic_box.h" #include "ui/vertical_list.h" +#include "ui/rect.h" #include "calls/calls_instance.h" -#include "core/core_cloud_password.h" #include "core/update_checker.h" -#include "base/platform/base_platform_last_input.h" #include "lang/lang_keys.h" #include "data/data_session.h" #include "data/data_chat.h" @@ -62,9 +55,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" #include "styles/style_menu_icons.h" #include "styles/style_layers.h" -#include "styles/style_boxes.h" #include <QtGui/QGuiApplication> +#include <QtSvg/QSvgRenderer> namespace Settings { namespace { @@ -73,6 +66,53 @@ constexpr auto kUpdateTimeout = 60 * crl::time(1000); using Privacy = Api::UserPrivacy; +[[nodiscard]] QImage PremiumStar() { + const auto factor = style::DevicePixelRatio(); + const auto size = Size(st::settingsButtonNoIcon.style.font->ascent); + auto image = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto star = QSvgRenderer(Ui::Premium::ColorizedSvg()); + star.render(&p, Rect(size)); + } + return image; +} + +void AddPremiumStar( + not_null<Ui::SettingsButton*> button, + not_null<Main::Session*> session, + rpl::producer<QString> label, + const QMargins &padding) { + const auto badge = Ui::CreateChild<Ui::RpWidget>(button.get()); + badge->showOn(Data::AmPremiumValue(session)); + const auto sampleLeft = st::settingsColorSamplePadding.left(); + const auto badgeLeft = padding.left() + sampleLeft; + + auto star = PremiumStar(); + badge->resize(star.size() / style::DevicePixelRatio()); + badge->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(badge); + p.drawImage(0, 0, star); + }, badge->lifetime()); + + rpl::combine( + button->sizeValue(), + std::move(label) + ) | rpl::start_with_next([=](const QSize &s, const QString &) { + if (s.isNull()) { + return; + } + badge->moveToLeft( + button->fullTextWidth() + badgeLeft, + (s.height() - badge->height()) / 2); + }, badge->lifetime()); +} + QString PrivacyBase(Privacy::Key key, Privacy::Option option) { using Key = Privacy::Key; using Option = Privacy::Option; @@ -137,6 +177,9 @@ void AddPremiumPrivacyButton( container, rpl::duplicate(label), st)); + + AddPremiumStar(button, session, rpl::duplicate(label), st.padding); + struct State { State(QWidget *parent) : widget(parent) { widget.setAttribute(Qt::WA_TransparentForMouseEvents); @@ -246,18 +289,22 @@ void AddMessagesPrivacyButton( not_null<Ui::VerticalLayout*> container) { const auto session = &controller->session(); const auto privacy = &session->api().globalPrivacy(); - AddButtonWithLabel( + auto label = rpl::conditional( + privacy->newRequirePremium(), + tr::lng_edit_privacy_premium(), + tr::lng_edit_privacy_everyone()); + const auto &st = st::settingsButtonNoIcon; + const auto button = AddButtonWithLabel( container, tr::lng_settings_messages_privacy(), - rpl::conditional( - privacy->newRequirePremium(), - tr::lng_edit_privacy_premium(), - tr::lng_edit_privacy_everyone()), - st::settingsButtonNoIcon, - {} - )->addClickHandler([=] { + rpl::duplicate(label), + st, + {}); + button->addClickHandler([=] { controller->show(Box(EditMessagesPrivacyBox, controller)); }); + + AddPremiumStar(button, session, rpl::duplicate(label), st.padding); } rpl::producer<int> BlockedPeersCount(not_null<::Main::Session*> session) { From 0fad42b5b4a7b45e9dc346df54ba7ed20c3aab98 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 7 Mar 2024 19:07:05 +0300 Subject: [PATCH 037/108] Added ability to open box for voice restrictions without premium. --- .../SourceFiles/boxes/edit_privacy_box.cpp | 59 +++++++++++-- Telegram/SourceFiles/boxes/edit_privacy_box.h | 6 ++ .../settings/settings_privacy_controllers.cpp | 82 +++++++++++++++++-- .../settings/settings_privacy_controllers.h | 11 +++ .../settings/settings_privacy_security.cpp | 27 +++--- .../settings/settings_privacy_security.h | 2 +- 6 files changed, 158 insertions(+), 29 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index f09b10448..7ad5798ec 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -10,28 +10,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "ui/layers/generic_box.h" #include "ui/widgets/checkbox.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "history/history.h" #include "boxes/peer_list_controllers.h" -#include "settings/settings_common.h" #include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "calls/calls_instance.h" -#include "base/binary_guard.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "main/main_session.h" #include "data/data_user.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_peer_values.h" #include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_layers.h" @@ -67,6 +63,32 @@ void CreateRadiobuttonLock( }, lock->lifetime()); } +void AddPremiumRequiredRow( + not_null<Ui::RpWidget*> widget, + not_null<Main::Session*> session, + Fn<void()> clickedCallback, + Fn<void()> setDefaultOption, + const style::Checkbox &st) { + const auto row = Ui::CreateChild<Ui::AbstractButton>(widget.get()); + + widget->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + row->resize(s); + }, row->lifetime()); + row->setClickedCallback(std::move(clickedCallback)); + + CreateRadiobuttonLock(row, st); + + Data::AmPremiumValue( + session + ) | rpl::start_with_next([=](bool premium) { + row->setVisible(!premium); + if (!premium) { + setDefaultOption(); + } + }, row->lifetime()); +} + } // namespace class PrivacyExceptionsBoxController : public ChatsListBoxController { @@ -363,10 +385,29 @@ void EditPrivacyBox::setupContent() { content, _controller->optionsTitleKey(), { 0, st::settingsPrivacySkipTop, 0, 0 }); - addOptionRow(Option::Everyone); - addOptionRow(Option::Contacts); - addOptionRow(Option::CloseFriends); - addOptionRow(Option::Nobody); + + const auto options = { + Option::Everyone, + Option::Contacts, + Option::CloseFriends, + Option::Nobody, + }; + for (const auto &option : options) { + if (const auto row = addOptionRow(option)) { + const auto premiumCallback = _controller->premiumClickedCallback( + option, + _window); + if (premiumCallback) { + AddPremiumRequiredRow( + row, + &_window->session(), + premiumCallback, + [=] { group->setValue(Option::Everyone); }, + st::messagePrivacyCheck); + } + } + } + const auto warning = addLabelOrDivider( content, _controller->warning(), diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index c715ef473..cfdc14ad7 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -88,6 +88,12 @@ public: virtual void saveAdditional() { } + [[nodiscard]] virtual Fn<void()> premiumClickedCallback( + Option option, + not_null<Window::SessionController*> controller) { + return nullptr; + } + virtual ~EditPrivacyController() = default; protected: diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 6620c4bbb..503ea62e4 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -19,7 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/prepare_short_info_box.h" #include "calls/calls_instance.h" #include "core/application.h" -#include "core/core_settings.h" #include "data/data_changes.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. @@ -31,34 +30,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "editor/photo_editor_layer_widget.h" #include "history/admin_log/history_admin_log_item.h" #include "history/history.h" -#include "history/history_item.h" #include "history/history_item_components.h" -#include "history/view/history_view_element.h" #include "history/view/history_view_message.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "ui/boxes/confirm_box.h" -#include "ui/cached_round_corners.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" -#include "ui/image/image_prepare.h" -#include "ui/image/image_prepare.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/text/format_values.h" // Ui::FormatPhone #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" -#include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" #include "window/section_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" -#include "styles/style_boxes.h" #include "styles/style_settings.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" @@ -1377,6 +1369,78 @@ auto VoicesPrivacyController::exceptionsDescription() const return tr::lng_edit_privacy_voices_exceptions(); } +object_ptr<Ui::RpWidget> VoicesPrivacyController::setupBelowWidget( + not_null<Window::SessionController*> controller, + not_null<QWidget*> parent, + rpl::producer<Option> option) { + using namespace rpl::mappers; + + auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + parent, + object_ptr<Ui::VerticalLayout>(parent)); + result->toggleOn( + Data::AmPremiumValue(&controller->session()) | rpl::map(!_1), + anim::type::instant); + + const auto content = result->entity(); + + Ui::AddSkip(content); + Settings::AddButtonWithIcon( + content, + tr::lng_messages_privacy_premium_button(), + st::messagePrivacySubscribe, + { .icon = &st::menuBlueIconPremium } + )->setClickedCallback([=] { + Settings::ShowPremium( + controller, + u"voice_restrictions_require_premium"_q); + }); + Ui::AddSkip(content); + Ui::AddDividerText(content, tr::lng_messages_privacy_premium_about()); + + return result; +} + +Fn<void()> VoicesPrivacyController::premiumClickedCallback( + Option option, + not_null<Window::SessionController*> controller) { + if (option == Option::Everyone) { + return nullptr; + } + const auto showToast = [=] { + auto link = Ui::Text::Link( + Ui::Text::Semibold( + tr::lng_settings_privacy_premium_link(tr::now))); + _toastInstance = controller->showToast({ + .text = tr::lng_settings_privacy_premium( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .st = &st::defaultMultilineToast, + .duration = Ui::Toast::kDefaultDuration * 2, + .multiline = true, + .filter = crl::guard(&controller->session(), [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = _toastInstance.get()) { + strong->hideAnimated(); + _toastInstance = nullptr; + Settings::ShowPremium( + controller, + u"voice_restrictions_require_premium"_q); + return true; + } + } + return false; + }), + }); + }; + + return showToast; +} + UserPrivacy::Key AboutPrivacyController::key() const { return Key::About; } diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 2c214bfec..25920a1f0 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -18,6 +18,9 @@ class SessionController; namespace Ui { class ChatStyle; +namespace Toast { +class Instance; +} // namespace Toast } // namespace Ui namespace Settings { @@ -280,8 +283,16 @@ public: rpl::producer<QString> exceptionBoxTitle( Exception exception) const override; rpl::producer<QString> exceptionsDescription() const override; + object_ptr<Ui::RpWidget> setupBelowWidget( + not_null<Window::SessionController*> controller, + not_null<QWidget*> parent, + rpl::producer<Option> option) override; + Fn<void()> premiumClickedCallback( + Option option, + not_null<Window::SessionController*> controller) override; private: + base::weak_ptr<Ui::Toast::Instance> _toastInstance; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 3f80fe54a..ed4894275 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -164,6 +164,7 @@ rpl::producer<QString> PrivacyString( }); } +#if 0 // Dead code. void AddPremiumPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, @@ -283,6 +284,7 @@ void AddPremiumPrivacyButton( }); }); } +#endif void AddMessagesPrivacyButton( not_null<Window::SessionController*> controller, @@ -328,7 +330,7 @@ void SetupPrivacy( rpl::producer<QString> label, Key key, auto controllerFactory) { - AddPrivacyButton( + return AddPrivacyButton( controller, container, std::move(label), @@ -366,12 +368,15 @@ void SetupPrivacy( tr::lng_settings_groups_invite(), Key::Invites, [] { return std::make_unique<GroupsInvitePrivacyController>(); }); - AddPremiumPrivacyButton( - controller, - container, - tr::lng_settings_voices_privacy(), - Key::Voices, - [=] { return std::make_unique<VoicesPrivacyController>(session); }); + { + const auto &phrase = tr::lng_settings_voices_privacy; + const auto &st = st::settingsButtonNoIcon; + auto callback = [=] { + return std::make_unique<VoicesPrivacyController>(session); + }; + const auto voices = add(phrase(), Key::Voices, std::move(callback)); + AddPremiumStar(voices, session, phrase(), st.padding); + } AddMessagesPrivacyButton(controller, container); session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone); @@ -873,7 +878,7 @@ object_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox() { }); } -void AddPrivacyButton( +not_null<Ui::SettingsButton*> AddPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, rpl::producer<QString> label, @@ -883,13 +888,14 @@ void AddPrivacyButton( const style::SettingsButton *stOverride) { const auto shower = Ui::CreateChild<rpl::lifetime>(container.get()); const auto session = &controller->session(); - AddButtonWithLabel( + const auto button = AddButtonWithLabel( container, std::move(label), PrivacyString(session, key), stOverride ? *stOverride : st::settingsButtonNoIcon, std::move(descriptor) - )->addClickHandler([=] { + ); + button->addClickHandler([=] { *shower = session->api().userPrivacy().value( key ) | rpl::take( @@ -901,6 +907,7 @@ void AddPrivacyButton( value)); }); }); + return button; } void SetupArchiveAndMute( diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.h b/Telegram/SourceFiles/settings/settings_privacy_security.h index dfa7f1299..4fedfce81 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.h +++ b/Telegram/SourceFiles/settings/settings_privacy_security.h @@ -26,7 +26,7 @@ object_ptr<Ui::BoxContent> EditCloudPasswordBox( void RemoveCloudPassword(not_null<Window::SessionController*> session); object_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox(); -void AddPrivacyButton( +not_null<Ui::SettingsButton*> AddPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, rpl::producer<QString> label, From 0c991466f5b1918178d0d878af6b86050b997b7b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 19 Feb 2024 11:35:53 +0400 Subject: [PATCH 038/108] Update API scheme to layer 175. Business promo. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/art/business_logo.png | Bin 0 -> 48042 bytes Telegram/Resources/icons/menu/shop.png | Bin 0 -> 687 bytes Telegram/Resources/icons/menu/shop@2x.png | Bin 0 -> 1239 bytes Telegram/Resources/icons/menu/shop@3x.png | Bin 0 -> 1770 bytes .../premium/business/business_away.png | Bin 0 -> 662 bytes .../premium/business/business_away@2x.png | Bin 0 -> 1190 bytes .../premium/business/business_away@3x.png | Bin 0 -> 1806 bytes .../premium/business/business_chatbots.png | Bin 0 -> 499 bytes .../premium/business/business_chatbots@2x.png | Bin 0 -> 928 bytes .../premium/business/business_chatbots@3x.png | Bin 0 -> 1405 bytes .../premium/business/business_hours.png | Bin 0 -> 426 bytes .../premium/business/business_hours@2x.png | Bin 0 -> 765 bytes .../premium/business/business_hours@3x.png | Bin 0 -> 1130 bytes .../premium/business/business_location.png | Bin 0 -> 454 bytes .../premium/business/business_location@2x.png | Bin 0 -> 845 bytes .../premium/business/business_location@3x.png | Bin 0 -> 1226 bytes .../premium/business/business_quick.png | Bin 0 -> 615 bytes .../premium/business/business_quick@2x.png | Bin 0 -> 1083 bytes .../premium/business/business_quick@3x.png | Bin 0 -> 1520 bytes .../icons/settings/premium/status.png | Bin 497 -> 568 bytes .../icons/settings/premium/status@2x.png | Bin 787 -> 1093 bytes .../icons/settings/premium/status@3x.png | Bin 1166 -> 1616 bytes Telegram/Resources/langs/lang.strings | 18 + Telegram/Resources/qrc/telegram/telegram.qrc | 1 + .../SourceFiles/boxes/premium_preview_box.cpp | 12 + .../SourceFiles/boxes/premium_preview_box.h | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 17 +- Telegram/SourceFiles/settings/settings.style | 8 + .../settings/settings_business.cpp | 568 ++++++++++++++++++ .../SourceFiles/settings/settings_business.h | 37 ++ .../SourceFiles/settings/settings_main.cpp | 14 + .../SourceFiles/settings/settings_premium.cpp | 15 +- .../ui/effects/premium_top_bar.cpp | 60 +- .../SourceFiles/ui/effects/premium_top_bar.h | 15 + Telegram/SourceFiles/ui/menu_icons.style | 1 + 36 files changed, 756 insertions(+), 13 deletions(-) create mode 100644 Telegram/Resources/art/business_logo.png create mode 100644 Telegram/Resources/icons/menu/shop.png create mode 100644 Telegram/Resources/icons/menu/shop@2x.png create mode 100644 Telegram/Resources/icons/menu/shop@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_away.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_away@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_away@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_chatbots.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_hours.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_hours@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_hours@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_location.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_location@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_location@3x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_quick.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_quick@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/business/business_quick@3x.png create mode 100644 Telegram/SourceFiles/settings/settings_business.cpp create mode 100644 Telegram/SourceFiles/settings/settings_business.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 64409ea88..fbfbb55f2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1295,6 +1295,8 @@ PRIVATE settings/settings_advanced.h settings/settings_blocked_peers.cpp settings/settings_blocked_peers.h + settings/settings_business.cpp + settings/settings_business.h settings/settings_chat.cpp settings/settings_chat.h settings/settings_calls.cpp diff --git a/Telegram/Resources/art/business_logo.png b/Telegram/Resources/art/business_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..25c357e50b1bbc32bf61df5498738adeba3a4a12 GIT binary patch literal 48042 zcmbq*RaYELur0wI5-hj`3BlbRLVy5C2yO}P?l!o)ySoiKID@;p5AMw1E;rv@>)!JN z&O>#zt?pIbFS~YCci0alSuAuCbT~LTEV*yezy9gx{{$8JU#U_tZ2C{VbNnSM30FNq zcJ!|yZLA?@qNoV><zF2Y?p=sE9O8dd{t3xHfrCTHeFul|Prv)GFBks*k3Q!j{7?PA zi9v9{3^+J(I5}yF-)`?tJ3LQ~HC+~6eQr+6kD<p$+0g6|Q(@4-Uabhi?iTJ3E2{U> zzPNGmsc`Vj47tyLslsuk-;LtZeU_v?M?r^=qoVpq_bU+h*AGOA_sC?Cy{Tcw!qFk_ zE_dI-<&-x*yH5|*H|wSl?K}0GierzPG21tvyUNBz&#I0)frD&MXv?U>5;l{ahF>o_ znc8Lzd<X%5W!GP%|6P|*b=L;DCD|8YP6D^9ani|+=31}Qp}0LEpuWCnVOO?59W#u; z%fr^z{qZqs@5tCzpg=;@$52!0+ijpxjKVD<0@&54g?WV_FNF@Ec^>{~tFW)ytv(M> ztz*OJq*+M`dW+u`ee1*Xgy5dlbYL*qHE&X0NVM7S3|u%KBr98Xzi_gECJ%3h_ks!U zHZG}IM!0Z3dBN~+li4g8F6kcLh>iibhtqpGS)v`!ryCm-Lb1KKp=z6U-C~jm1^wQ; z&kuX*I&GlmT*BKsrUuyjN!zvHD{7E?#Sea|`#1cj14MwlDI(zQba~IT0k7GIsYq%g zprUKbA7IzmIgBR@Yl7(c{J_{*w4m}SA{+ru{k#gxK!o%o?%fm<zD<4x0t)LB!LIvb zmv=am@Dv-q6s-3sI!_oxHeOFr6{EH-olVckEwA%hn2_HX&n|m&N@wGHkC!;nWZJK; zTY#&%vJSQlrGr=i?8`sfVY)1Ljn@m>e4+&f_pm9%xA6~;eVzW0^hiKn>oZHyt2!mX z3sw(Fr*pH@q-4E+`faHz@s=390ai(PXd3C?q7l=`YJs~4mze-$auA=_x_@IAK?~tS zFkY3)ok^F~^*iiu`@;5IEoWY9q{X~RFQjf+zT}2I)IZX0v*~uWKPxMXb+|$a^Fu0y zO8=b#IKbeHp}gP$Ot%4>jXP>GtIgR^hQ|6F5KJM805N<3D(P<l(T{J7S=#(_ssCNy zfyLUpYNk^Lh+xz6dDN%T7>hi>CH8L|8CWO07+6R9^>oKH1zgDVk^<crYpUM=w?}U+ zU?+-n(#-G8r|d<qBfCxNM(V;-#Sb_-mb17#m*ajp+@TJQ8n(q^`e%smj@RpR6wXm_ zs#tmd>ne`&Dvn0O6LWDbKd|=RvUL=uoGIFIxA$}k&FFdlxB57H=gVyrj#=It$2#|A z8+QvuThu-E6ZLO#jJJHz+vicOd%*KpS_e=h5O;%z6^H0qRH~myrEfd*^^?|N$?bd3 z!(B^tTW0ke&USh?Yt8BtZLY;Yef7=7>T<SH;0SXoioW7?=d`L&?cP9?NT)P{YaVQI zTBO^9TnO3PR=~&0R=~x|R^ZD<!3zE|QqesT0VIe2*?$uH)5A&D*0udEt31MfaKp>- zK}zcoAU#0=hGF9zZjKOmSDjtn?A2|~D=elc_%oj}>n$`GEWmFBrskMm$F16&Vz|Vz z5K^3%x}Y;Z;;p<R6Uzf`T);1${4@{WRZQAf7QN`ZgdA^YySRt25V$#gWqdCpU0U!6 zoPKvcX}&KuR{>pgcItFG8gRJCC%oOW94(6AYd5Rl=4<$K$NR`UuXU=Kev*F>nqAc1 zA(g7oG44qDt}LJ=iRWRFa#urkKd{ru^Rl*J)Q}+tb%9v&ngYGO_PO2vS=qVaO^h=9 zO?K*tJ2B>u6V7Ma^~cBdXn%lK!fgm5rM%)q4!BdNxodmj=QHQ%X9a~)n(fczB&kG& zW_%J{akejvq-M*SxlQXo(7Xr8;S{{Kfvq2V7EJ<rus3THWK&d1qM1vSPUbeZxFsLZ z_HIy|PoN4o41uzPC((ZaPX29>IJ0-BN2?ZM9y^bhJ6gX@VNKv=QCJH^6kz=~Prubo zF+xt(9BppqZ=0vtK2i%5PM(4yBHo3G?6o*Eu@E=rsz-^|PxBe%mjhC@Z&_^nLc4(c zRv7(UqC}}XVRk_|f8&d?Ho+K7IooveTIu4UTZBT^{b>{Gr~dMOXqo!Z5CmI9sulxI zm{R=yPWBh#bMkn(psnuQ+9`6dKqn*I=+q@2y{DmZZE<LkXU+BpsGrzaGs^0qxx2I` zI%CW1P<C2p>Dhsn!&=pj549F*58J4V5pgkcf3|0N<&q5n%02ny3s>&k9UHdiwB{&L zsIEdBFkaVMNR=($UQ2GHS-}bc4sVlOJUeWM+*vX{TW2S&2H9n}h3fh0v1YLWmT_F- z3A<f}MN}}U1@fNolrr52fe7$okuEgf`f>SG;CCGS)`XiJL=ZugU^DS;Y-bQt;Xu^( z0nSkKweIp|ax!~;=cgWo^Gw^K`ze2|?wKbF`UJZ}b(DD-=L53Xg1yHnljfgH+b1xD zf6j5Hr34FH$**h3vp|Xs_V)XQ*ua`4F1Tn&-Y`Rh(k&k$U!KadP`)QVPqyBGY~NMN zl7~#F*lXWU-5W>;TQmw43&_`XS}%6O_D!v|G^KT2rfNgvOS?MYBx-MvyZcR{BYHz_ z*Hy54!qN}?`lzaF46c@$+Z#tmqxIeN3qYpnNUJv2l|4(}AefccjyX(w<yCu?;hd+} z25{;N<?Gyl)5Lnxoor-2IlaULg<3V<jJ$q{TUv3GMrzq~y)D}rJ$eGvD!Z~Wzs>K& zu!+4^oo{5<J`~#smW!3pzO@!#0y}cTI&T&l_c93uWpC|$GCye$ioUCfHD7I*2Fj>Q zTG=k@8xKgXYKX0H47uD=)<2~Y-mNxV<Hg(<Q|g<qa^@j4738=-Z6g}WdZgxvLQJ=E z1nQqc!PWuICOO@HO;&u#;F3b0jOg37k~hWGefy_kg<q6SFM?68`tyIlY{Tm}fuVlH zBkUz-2NSx}>o#N4%&gI!7TL|;yPy%fXl`xudjx&?$9H?{cbr_<C8s7w*0*QIr>-kZ z@5`*O!qa)w?2>AcbsNte_Gj`MWHgi4tV#@4pZVE1jqNu~j6Q7HnNdS8g*<Ol*{)UX zP1gf<F5nAQi?QyKiDVwhAMc^C^asUexedtgvIqQE7VkL_^IMRfs}nk)P12~R-5z@E z0LAJMTO731JN#?{;`+qmHjDoGD*YzgY^cIm_UD*++uO0Y0&qMiXGsm?)XbyA&j}+p ztg}(37i>)ooa$ez^{q0Sv=V%#R$p`0R$KJZjJ~_wxv!Hth2#4zh->Ch%5*xKpiD7) zl8t?G8mIF7rd4t;c02ACt{em|OSjE$>3j~{pT^m^>f=`Ln3^=tI}x-c^iVwYhzXw* z41&s4%4s?9q_MPh{Hm+zIJf$vYyA;3M^z13BSv!65l>N~ozUIw2k(V*UT*=T+F(nR zXB5_*qg!&K<}W!nF0lqI0YM9%qjJ3@S+k6|<m>3FOb6$;N<xtcFcuWo3#E|MqZJyr z7t*<Gvrx7I4rmpYFVF0;%zxpbyAf9LQeU<8UsrnF4mufSV$;{G)@c!C$ywIYK1c6z ztO`EGx6vb>Y4jKtSjehC6xFe1cBwG_o4}&VZ*Lw}k$|*(Dv{y7*%0V3I}JXw8XXtp zq7a(GUie}$Ix?(ylMxjP&^dWIGL2>{xo#1ys$=1gK#sXlx^lT>8}7G#GZE}p-Y6Sv zBebY}lSO}JGxyJGPm!}5c^zR=f4<O)&h)#te>d1yd6X!pZXa3<P+l8CqMy1((llQ{ zp`3mHdu|_yi!f9c1dhrury12F#MAvVR{gRJtWwuFa}g!|yhETiJDOx1Vo8l=OA11| zm<Dc;4O=1j)gq)4=jVtPK^A6uz>AQ5m2WA(L6i&mZ#kwZFtv7tXB#Z=_JYCNOZwnj zmWM2-rZ0*)K9%4Q3QG3u)Agc$TkG29HMt2N;lvj-HJ#c1a=GP_N<FO4N`#UMH=0P? zhYeX3lANJ31O6Sg!uRY;&4h8U^pk2ep6?M>?aYN<lT2ODRz@_UGw8`adkqUde@xr- zaf+yvs3yrn;x$#)-|;>!6Y>Nof2n`l>?+eusd)Py@jCgv!IiF+#|hR+c0E+~_BG%l z1c&570y)*Ttm6_(38Fc|bA3gF!y>BNm+i&#cfVh6rn7DxBaIy1I!Y#0h;Ov+-Wu=8 z{0iMNqn5{o@QT}pcut}Exdi3(obD$voNEUVqKv%?DpxduH^d-bi9VOn{iMJuu0>O> zXDx+6rqsV*(FU8I&R%tGTJ-_dI<^%zzZFuH^)D7f8d-R}7wK|k9Hk-JZxQLg!KT=+ zS(u+8Yyef*EM(>d%3P0MLa)kV;1$$q(DAvyb|dwOAnHa&TUk~}es`wsa})J*+E>6r z0gSiUhb?Q$PhbYfx#dE8L_+_ZrruB;_z{gU{0Qgs*E-S$7fKH&+u-^GyEf1fcC!=+ zZ!@~%uKggjvo{91-l(BGwg_Z-+rcwUp`4yL*M0DGAKrnMU&J;v*eP|s>|cTpF-)H0 zM_E>HeT_$KmEod~A#O*P4yq8DlKHap;yH^7ddMmNWt-6g&0XPq;MlgZF!T1*7dUhC zqErZ;8(IHX{dhF{sRh(AW(1Ye)d(=n=c=CRj2Nh9Yn-Ng#j*qC%L+s7l)0nXzp)R@ zH~}HiJi72Fu1)<3hNWHA*5A$&Ct;AMjT^HZRH24%koSp4;3C`gm#6aHBB5Vih=D4L zvQ&uyL0vH-WMZAq)^cy<x+H1na;Gi`k!vR&z}J-ub+5S|6D#~qt#oVaoNd{X!h>N3 zd9H){U@atj`q6Zm#!UsEfv^CRm3EKtcAI(w`6XV7)tB)K(|zjZ+RQK7Yb|9NS05i8 zKiZR`0{Y}jR>yFxgSh|$2@=O*Cz0a3p~KSBNb;4+S@W`Pt@fsEMZ9v38I(C)Q7(oL z!%XTakb9e_xX72wwV|`0N9W>lih<yQ#XI_QZ<Jj)i7TTtAYkXxK>{LIviDinO_7AF zSdr!W`)e4rlM0wn<O69!xjHU}0fVyBK_Y=MwWfHdbJf!0l!bkN`eyK{PF{GY@Z!RB zqt|xjn95&I(u}|#&?YO_)Mz6V3f+5^`;0AK+vLV}UV`_lJOS5ZQjEbrS1e+VwB08~ zJ=Z1=R}I!x)uEy#i$DOM2>+~eYlRhM%`Y*LTL#t5n6YX(s#$E0^Yjne5!SzU9i0h< zaNMOWlP0HIm~Ceb4Q?>h2+=cc)JLx-;2vtP6LH5Q2m^iCx~3x5oSZXvlGiF{Ka?i1 z{CVO4Vv3Us6&&I!E}i68yHyZ2R(Yw_Ynrz8)aCGad2U*FuN5bb0%UntWi~b%HRti{ zE1kvsFddM(23?pRb*6eZKtZK&y$Ca)Jpu@LZXEzVUavb$kTt#vwRz%Oef2AU=~zVq zXT(0lM<{RM<YoqG1&tU$!x&ziCGl|*n5sOSzvTQ%=&q#9sDG7bGMpL4oE%A2@2N7o z1zD=tTYdH9VrlvF*AuHsjSq4aljWC`c=Cb@a26$`rse!r#KMWP&fi4|Il>ovkYRW% z3$*^8k##9JbHaD-bJMnUE$XhsD56yw?Zq&eqyJi+J#l<@DuhuT%O&2l4LQcot~}@` z_gtM1ebMYZUoo)<9$=otr#tK1XZ!LDOV`@p@E)2e;%T-El7@OYPA^-KmOSb?qkgMa z&s|p(Tzwg?DEyk(Lt?AKG5wd}IZx&JKSVa2;fk8~E)TDI=R-EW`on*itjZ>V%f_Gg zZW~{W!np#boc+()Zk2tJ+!&|)T}%=KHy0?9Z#wY;^w?i(79Z@I_T{qKX5kfXd$H87 zxI4l8z!KrmM%i`>H{;)nA$s*C+l){;8myS5Lze>jxbIHSL^5t<X)~p*&$;#R#r>v% zpi!OVu~EJzOQwjdPSVO3F<k9dQW{hEC0qq@_RB#yP-&3P^yhO!;th+Qpg|yCPLI&5 zOTZ-d;9qRKZK*--hM`UK{xaf33j)9nJb~kl=^HN<XAH6D7ShH-oaKXL>+C!7d!Og# z1K-`2askVJS*NRSD1!&5BvX*Yd<r8tm-&Y}C;+fkk>W)u{JgwT?L#lmCWa&EMlX3z zY{2kLAO$bNk90x4;wxggLu9`iUt=uXD}z@!$$W=sa~Q_DF;GZPMFN^GSyqy<<EnZf zXL(O0huEDU8${aq=Y6mV5{|b6n=lmbS9g3s3i}yIDw??=4|1RDS6cO0R#>6>9kU3D zsJ%R+LgRWYZiJh7G<(-{C9?Mf7bUR;(w7;PA^Tpm4x<y6`y}G{6NeM-D%aQ19%`wt zDW)$rL}ip-Ia}ON(*z1<<~CA?FcG+)N@$){t69iT%Yt3e(*;0OL=(&2KM`#$XjEyp zyA~@a;Tx<TtF#HVKHVBnaR&EE&Vrs6SP&{ElO=c``T>Mg-g6^fq&0eG^L{bWH#;Tk zDK^7f5g>E(vq7<1bsaiz86Hyvpy;5V2zc)bXMo4QQ_DVUBJ9=w=9$`dBxYeZ{`fxp zv>h6?$YApJQXR|bi<X%m0%8H^;Y2+TEV3!$M6V?;>GQ?cpJ~kz@5iT_XSgY*PJP)} zG5H(#RqS4H+ymtYllTO!U+j7clGW8DVh9hmpW+>Qgs2@ivHezhauV;o1ao4v_fiLA ze{T=IVi1GY^kVF(xkHHB#5W7SFQgX-Km1Xm#m^b<<je>7j0B^Tl1tC^I1FfSgz}m0 zCl8Z#vMW-A3#Rjc#kbvRZKITK_^=K!Gm57|SJ?3D!fMnWu+&cUX^GVhJ^kHsG}Wc; z(H1@%*vlPr&BtN&I^ep(tdZ&yCHT622m?4w&~omld6+b7!S-06gY_gbYnfjy6s*P1 z9O(QZ1}zGkDPZBzaNekY?0G7d(ybh-8?^&8Rw1UH?b8<~?P3gFX`hPtpw+_mTK&T$ zon_d120f6%s}Kz<Sc`^Sl1q_aa8kHR2Ds_~E8Z&cKFkXqg?Y_&8m9=ZDcaoWguv>V za=}&FE6VgWn7DT88R)`G!qS(j>jJVBG6a@VM*6h3xJ!MDNW^>R2nwc2+6<x~kH7Mt zTPhpseze$ZJ6{{oghPqJE5r#d=cpv!JVrqWht3J7$vRAE4L-$HXX5>^vsIVRjd<}c zJvrY42~jaJ7q*0aJe4)@CLLA+Rc`{cc!DW}=$w5q1wTT&4<>|HW3#L&Yn_-nzuTXj z+0Nwyj40BQgz*=9t+h^{*Je!#Vk!0Fx;R5o7}<&2%VE|3{5Jx{OpT>6%QtdyVe64e z5oY>mMGuyqG~S(QZKC_)EZu8W6a~%Qj2iwL8TE^rfW7hW(zD=)IE+3x7H<hz6NF8b z(UE1}^Hs}3wH8Kr*~V=5LcKkNrZDhv+p!Ssa^wRKj!?P_ZiXrLG!vt#>U^B}6Q1EX zyEDs?8{k*dPUnHhpaKvp4^OmkQ+x$m6leNAlJMoz1($kEpNtR|4Z$1fmj#Vee0CX3 zCz&Q*Vs0wFxpVT{7eb+O9+9TVW>STWc1=<HK*A_{{Or?^5UXpNlrm(TmNbghIL(qD z!^_pYT_Y;Dnph1qB<vN6jgqw*JnJmFCY`2l^0vcuL~Wp`VrKab-YHAVXh`3W)=%yk zO+4ETd4%!gVbq!z>VqaSEN1<+#E<zJP3vRV)XYZ3tI8)^jjAk4&z69_jy&14j=ZK| z#}oNB5SQraSo5hU+ZK3BK$MH(!|ONZ`=$-8*XHm$1bjQp?>lMu#D7M=q4I-2epK4w zdO#>UiJUE|il}^ivUVK~3V!1*mg`sf4N6kD={Bb$O4HCf`jJnxT$dE4fk8&%F0&Dl z(`o8HY&F&HZ$$jDr3*g=ZrV1jtI;PZ=;?O$a-!V5+8;}`T4|m%G%3S5jAtiz?F;i@ z44s$NlysozY$aq%vP2>xlA6Hcj%7PN$i&@}FvVjfx+wxS<Rzs{53-%ML7D!F%-`YT zE5JG<Wt+4og&A;nRz3##i7Et)A48%Y!{_qrJ+OFQ)Nh@W3Yvos{4rTRO>X9!W1d@H zu_5`8G@WT`bU-E*@pU;GF@dRN_?OAEl{PmU4QZ^N+E-$&r&Xu>yr}uw$0)(~zgdUP z_PVrp^qr%eKYyETN-8e0%{Jfrbo6ePS2FYP+>}BIw8X(7L3^%>CH4>L^oWJWF{|4Y zYN=6iW@H9+3;+R3umjtiC$+h|x)aK<C2i4)cf^xX5Kztx!xTOj61Z@3S>G*>(36}X zb%|t74VA^EAm<fxQHDzehONBT*04AXc4#tR^`<p+<WqJ(u^lRf+ocfsw5?U_F0~*@ zms9V5%B{mjRL_*MO(JiP|9hc_wC@t!(gz{}5SMBM*EZ*nW0Gt+StKQcWD=|ol6`8& zrM?0K`K3ORwkOL7y}w9YGtCj&3Na4my~>-mDxrQEA~|lkyL?Iuc`zixIZM<_h_k;( zo8R@u4~y3C_VbvAuV}Jy_dktb+jySNKxsA&WU#Any8s^Z{berXCLEAhDX(QDx4y5} zEv#ejR@dVWCwx1<I*{YKR;YK=&?0TvJ$`0H<$lprXe?_jHQgw;{LT$yMpS>5|L6H+ zP;hImTOGH(t}d%1Cz&7hy{3!A<!}6omf+4{CSiVbuj(|czc%$*W+va=LgM{mFpIe2 zop4LIF4*Sva-!}fn{7>9pl-Jr*}J+Q(WUc-wikRTWsYOreIha*Q$__2F8}e-mY!^- zng{{uB^^5EzGb=gyTE+x@DT8w;wHdQ&^jYoj^Lv}F44gEB@~9_2HgTwu?Ql8QyQX< z^TR<?mhXiH7<44Ye~a)#&We3x5?1i_k*O;}r>e|D3#y4<&_bYu=|Y_F9qy`PH}&DU zD3o!AB{Zi)bAv?49mIzF&18qi#k+-U#(LLTQYVAP({a>;@%1QMpJ;_`E@I1?`w)~- zz77ude%GHNcURx{goUBJ=f&GtubU^VsaOR5g<^Pr9<LQOn|uzhAbX>{te`|$Tp)mc z_D7fpwGtk1{AN3hV80QvVJUe(=5NBz^@oS}NWJ{LCf&yz^2DOjMh~sBUi_dzSI|T$ zvanF+thZ}q^5E<<!js%|Mn0=+e9EoznazQD>6}+LxJ;iwnRn5Hp5p44)1V}o1qEho zcy#CrT%UvKn3^o4nAw7f0_ehaL?V`9Q(vE9-TG3wh2!0JHeRVhi&^18ut3%UfI(ki zV)nTx=7~u>PcE{1Z&@K16^**p*gNmwd~$SgFz++bAAHuVi3(3n1ZpYYWI9a60G*<& ze+@kANb=)`?apP$UuI0U*4pZ5!2(3*XS}21?anB0jYRb|TF8zs;p)Hofy^V9vy)`n z*`C=4Acc~Tp&pV~iakX7bF%5e@!Ab5n%Cbnblno`INLuk>wMjW<!>38wh07$umlr? zt=nW|jjOs4JMuGil3!LfS<h~zcs$^XCYh`8OxUi7y>;R>Gj$i%)3s1)zv?x+SU+r_ zygXgwJSXbmL6ytC!AiP_W4o%f&Q32ig(9+oVhc2ai>(&ejb%6A#TAhqaR}~QTcb!@ z>z&r?L?_ZUdbU~mk6ry4u)XAY$Uf_1Yu!9IC~-m7$e)icueX&y@_J5)VWGU=yx98) z9^ocU!T0vbyOE+4?DV?ciCOMUvi_Of&S_C%p@UwnKp>QlWJ=QPEVNR|PP@9Vbs}*5 zBk$(@!U6`{aGg2P{uWb95~9&|pK4Iu3Od%~+F>v<lwJD|DL6Nx|3D8mc_<_!9RTmX ztkk=NQr-cv%sWQ`SwnCJAP-l3o%%5kFCkw7DadrJS<w?oG8@kw=Rb4Gx)P~;ij~pD zwdOxuC>)cVfa7|nwo)wWLF%<UG8fRKKg%r`tHUx8txR%QG~Pbty9cH<P1{^$Z=EAN zITw2kOBfBaFL0o}4H53RBLetg<XKkLS?oWqNvl|oGDH(dghy9w`nf=6^4;5!u?jg$ zpU2D+vXaJ|^sfKXorKwzyFG<)KQu*=*KuxZoMJ@q_<Y|!$Puf}oV$XBEnyY*IPoze zm}{S&$VRii?H|p0im9$WfxZ98x+x}$u-u9P69)_!I`GjC4cPg67=MH#?wa#Jrnn9r z;;IQ7jq01HO@H$-8w9)~AXXaXq@oj=PPdgf|I3iLexB=a1)pPIX>(_Y*D6`%ftqnt z9cNyopXQoR{-gpEaFC72(r{Z?#cM5^OBP0EB~&UmA^sSCyh2zwRL1Bch&bBCq%|5{ z8JpqiGeoCw*s!q;`KpSS&}U0x_<i*}rgA9xA48$ly=4Cxqyj>3jGRuF-3zPzyb77Z zN#3Mv)xWpWb3zcpJXtARfVcE-oPK%2rdaX^+x>m`Y5YhWK5e+wGsS~tq<-?N{n}Z# zaJCn{yhxPT>!-}f_5`9hZ$xH(%))V=ad?r~NdecLsn=2)T3Xh6-<sUFuk4nOvA=#A zvRrf43`>!S&JBs&tzCTR=smldq)fu%A+8r8`ikG($N7B%aaW_OYfAGJN|XoED3U4l zI4T*S+e5X9-)P8{5A!9}x+eF!Tf_x<YxR>FjYMBs)h>~1A(WZ5oDUB=n}=bT>12rG zR`6M=<V+_x_;5eok&4D5O%fS8`E3vC{xHT*l|(f0MW*#s-OKF6>@6r{eAPhYgvTB? zh_C3^<s@$2u67Vb|CTJr;pDkpl=DdjC-f{i>+&yP)~Lgs*y+d5R-{PUPJdalHm>mp zUM|tRY58OseXPd|ym}WdY&fOtUHnF(>hND`4M;E=_n&?z5K)m*r&7z|Xw`tpg$SPu zT3LmuGse1MjwHsTm0)#~`GhDJ>VUqZAL(k|W*1^HV(+BEo8_?kDXzN58lBnH4BPfR zn4NSnHC#*AA);3H_`l$T<|jILKogEpH&c*Ul|jx8RVkhZUXa)Pl9N3$ei*~TJ%#&H zIX+-6)DPhxXTP<jm8Nv(9l6vG$px5&HsrEsGMSY#2pGY_Jv-iyL{TyBX4Vui&1g#} zvNN4yp5(pPdYlq?1vBV!v^Lmf{d3%&tbsp2(D>s__T@t^iu0vBdER&xx=vzp&N9VV zHTdG^j>E47A^lbQ?0Ly89vj24F}9Xw*~aY{)`z+K5dTVDaWl&4kcKiKCD$|q8IjXV zKrD;W@ol~2Q;p5Ali5(t{JwI8Gi23U|J-Z8n%QSa^@j<oK`%actW^L4aRcxqjK}h# zA{^6e+QMKF5vaw1duwcH+?zDW3)n8OblF*8jDHyCp$f+G8SPLJ$8ICxD6Z1zrw~j( z48e(PoB>i4HD~Z%mnt)33}!ar+szf7%*te02El625C5d9kCF_EHZ%~2m@NM;qCpk- zoiBfN-};HQnhdZN*G&ry{D2B5c~T2(8i{x1ko1%RD-1Q#Cm2Jjn{u63?kd@eD32Rw zHlR4oKn%TraoeY7fsWgFJ{dU5epKu=3$_!QET4(NgfWasFLZf1o<tGiHC4)1l-U7{ z5H$qJq#`=MYxN8!a-z6*lZdSP{Slb#EMAX>ae?kAb_>o~T0|a`z`xdeK(bx6N8Ap> zKEkpj?uzO;HX*o8EsV$%WjTcsi=WLW>PWJcZbw`iS@Tnjc@lSj@=_`*4f7-%E63Sf zWK{U4Q}ovi7%zMAjSdaOKP7K=Z6<#Zd(ouKd?MC6^CUh+^!&`jU9I?s+6C+9uhE<Y zSOx$GgY>R+eGv)B;Py&wd?9B*N2+H9kAa3j-_JVql+X8M8K(G4k;D=~Z)_nf&?;k5 zG`7T+R|qcl!ylRjqrQW3{S5_^;!hE&&NUxot$m=2_)H2gC!tmO#5|;8R%voA<8!6< z#e5}+T3Hb*b<#h3nXM}VebfnVviQ>>;xE5?(`@uCt)?yx-+g{sp<mnum(wVm1X;EZ zC@Z~84p39=i7S*yhIbMS4mgajpaX{5iv&EwRGw^V5&y3te^X!O7QQ>dP}*%V^hy{e zkD=pY+gcU~nHRVwB>eqwm%@U`a{&ThbOUrS-YdQ2_3ugn>5Y%l=yhK$>?~vB%G5_A zj)s{K7SwT(EaQD=Ya|Lt)Az#H+T*LNInKfBF4G|6Ls&cp3$DPIfRrhl+t9ggQWvZG z6LpHI68f_u_r#)O2El&wG_3Rp201E^AtcF+2qtg0sZ+EyMW@E8?>sTz81xiWpNoF# z&@jz{BvBs=oHe4E+;Rss!mkTpB)PzpBlfw&MMvVpdw{qzicOQy{iE6jrO6qUhuDhu zzX;83%<*jhpl%53Tx6TW6g4k*HI&rDVtjA(CiC3Dg9*D=YFWN4m{^BQwYA1%)-edB zv4I6eaRoj*aUthAyn@9G!PX+4kj29<P9E^9SLI0d8+${TlY$e74<k4`3<IWQJ$cJ} zQC4j5L+SYs_!<2cu9Y`0BCXkEjN=x)7U`&1Pmi(w>gWCZui@+a5Xh92^$QOuiLjSX zo$*zO8BOW+E6{968e2q%0rSu%U$@h%SN)8`cs!WP2H7bTD+;2Kjk_0<T<Zz$V8h6! z^qI>(mURiv=z(kV^}~;18#vc#B>|z%NuOM*z8AUq*u@#>wdIVM@3xhvE`(TKVM0|s zj=&QZhZ24^y!x^|9hV+BVlvvm>|(aS<YZ+^0)l}WF)AiqByplV_i}_ijUF~poO14j z%u1XPutDyBGurDD*|potv)or3^ME~x6zFrQk40z!$`@{pUO3Wdio%|WW9)D-Cj2|0 zbNtEd?e|sR*;Rg(rc^8PVVC+8#xjwlb3a}&UEWersGqqj15_~3N{M*kqbiuR#d=6u z<?bkOp`%Ff>mt{K_}7es>O95!^WWIHrv|X*0sXGCXw;r=2AKA<b%(hF_)_h>Y(DD1 zl2WWn596=WgEWuDb<YwNXo8hAqSFpWsALXN(k}b|_$$wnm%yjp1lgB@&=Musk?4bn zx95ox%Ng6-bW9F+0?hB~gqdgpqA_t)Z<2kbhA|>j^{&ttO<t?OM`Nf9-T5_={!(!; z{3+Cbwy&*4EyH7)a>`?2!1rdW-y~=dG_cAsQxAOakA+L}8n@2lqjI0zR9d@|tGuo^ z>vy8L?xB5Ei8Nw0EiFmdNlkN}vWIW^?fkCA6^E~EP=u@OX-}*W{@GtIX8BOfUt1UF zo0(4O(+UPro476-spbXdR%>7X6{?%tZ(#Yyj^DQZ#9i`@$oRqz{T1CO1wS(+O+Ddl zMy#P^Amh=U@zvc9*4zwZlM%G>tb^;hB!+C^Z6whv3R)#-3hRYN?J3+y8&ODVlKMQ4 z+ur9EXSm=sWFWe>!>XH;y(^QRc6Di;BJF&M`juhM#bW#CwD?J0YRo&(3`j&-e#Y_Z z;Zwt=@=*wjK_#uMYr1z4QRhdZMufL!X#JamJ#;+T3T8fw6auCy5Q{euuy_7d%0Vb4 z4h@=pxlCbQIIAx&zH67}{gUCw_`vyDAO+K#`S&DVDU;syHsT<2{K6r+TbzNy+m%03 zQ~N1F>+SgAcxAm}jL+fIaIlz^(ln$5_&3}8xD>o1ctwGfnSP#qL)zMLeY1Ff`_cJ~ zJlC!4p`D8gdOg@@XJ7Hk>ZJT}u^fLu=MQ2fjtJI^dAzY#Sgsb%wHlykrCbqHg=YLh zNSgHIL7;_9aC?&9RFCo*l;CKVXvS6Pp!-)zyXcDOy6x(1SlVOI?9zKMm^~LV=kXQq zTH&#AaaIZ4ApRZHB1>3|;v&3ytc}C2nxx}5L!J$3q}493bGVjZz4RA7yaE?8Lpv_> zi>fzwxC9(-E4RTyrS3G|aj~H$vtiV@P3G$uuR%F6{tLqLr+TuSB{>qe98#VwgA)uk z9qo;=s$$~N>Y|8c3DxQ=?EJ4?5$>#ykqjti4NEA$L`ZcLuuZ1p;m*|FSE`1PX@y~4 zIT?@TH6+UA@kom~c2mn8iYQZRPl&cmwjCg%7WQ}KUQg<`b$Z<oGu-Jh?h8RSdR@Ie zyvC>Za1J{nh=hVJQVVfu8%}~UU-bZr`r^5gLsP!AIet8WR9|NugkzL$)bWg4oKJ&- z6q(Xd23}U&@~QvkrH3}>kH!ty%$I#?I3%719iL|)i?dpbk#(*&ZgQ|(!=5J+MoT&m z=B$Z?_OED)HeG~Bk@Tg^mG3BGx$d?AyPpQh-?(5RycU~9Yku=}N-rMYLzR*DYjnAs zq$&B!xW})8xP#+y{q67)b-V~RDp?1PtDNMWi+*v3F!N}a{|Hvx?HzQu4k3D0ILHKg zQq}D2wacJf=JO;%&8x~F`hA}DRo`KLELc5q-X{kNoHuL1EdtaZ-7v9!^Xm({X+nZY zS&}Nzlc#d2=aX!-SagU<Tt?BPP|&_^(fwFUGVz8#B3x!Rqt`GJCO`GbXN>;EMk)o` z2~p2W#<l^OlTC|V|LqV1_uHl?#Yk(`I4t+^nMF4~YnRK{nD2_-HoM(40`(Sv?~V-B z=)|q?4GW4@GtKYSI;BnwWK8)K!&DzsPNND%imgkIXM?Edw;2mI<bTF$NW~1#9NF;= z;Lj5HHh?U!V<^jZX)H5XLxbJo1|{usGoY+uSNE5H`-djgI(A$pqt89qAvhIZCtDn7 zbJXXnKV)8<(|JTg`~vvuzUzIEmtjA&_|tY#%<<kT_hwP+Z;4xD6`Tcd^3QE9sFR56 z3vX^z;s7n49$r9)i*o$b8k-N(sI<(_x{uc0@EjD)B^W}^(rdQ0c-d0SNm14rTv+X# z%<ogA`oat6_a`dXPCc>icQ;o>oeUm!U317{+*}kx!&{GHM1TH*a2wKOq}U@Be~i*D zxe*aFpaG>H{8lua8_n?!Q}g!tY`R;w6jr+y955^)1re2wQ2y>*Um5ryLpzAOD1N&1 zqnkLSq-o&!^1`w82^wMm%j)q2Z4wN{#3Ll3I;o~3FU`9F2UWxsezGSu)CeA}mh#C2 zP?B&qVA>$(mMX&hKuW>Yir*$|tk@AgORQFPHm<&Wg$oEKbwNaux>b_A#tx&xOA`C~ z6i6X<b9&VVf#lU6V>N2iXebNV8QUi5EOSaolKMAsyDO*av~L%5;&|oyRA3bl$fL|T ze|lu5`e1*^82Cp{YJFO#azS~^LY#KZ2Qwei@VET2H=^Ij_S4LQIo@7yR+4Zil6xP- z<>)F+xG5Y`{WqYVM$dSa1aR)+fwBilKmsydy(NEmrIl035MK;?90MljamKbtbdS$l z&;<UTqB|9BK01~a4K^9P$L&By&pP%@bG6$_3lgxL!cfB-+#W}m)LV;Ky$CH++sakp z#7>CTRz#gT%n^0$gE=an-cJZxNHt=vP4nvcO_ZVy+w^J#@`c_3d!HVaeU5AQhNcBu zJ%#N{0xNi(rGubBpT4anBb3xcG9z&+;rx2EM$rq`K#nC5?6+Li4S-S`O_LZhwdh0D zvIU?Yl+##M`UHY#J+E5==Dc_hHJi3&0hV#AB?Q+y1RnP%cSKuR*05oJ^))aMp@Mcl zFka)2oBwmLkQn_IG5|AHu)9K<JtBFeI6nX)xKt7^1^%74Km&xcu#13?NzRt4)jbUi zkwZ>Kqr+Lz=zJi+&Z3z!>i%F|Bw;!OSaemSoKK9E%I>wf6oMzuw)Pd`qYh9CcalW) zlGU@V8k>G?6LXfI^p9}dwe(vRX)KzEm0`#FOu~lTg#q#VRH%mVqh-ogV2X#2l!NgR zWQ}gX1Wx>DsAYQw+}3gxF3N1BX`2YzUatFo-dEfNnfbZVlfVD1)15nJ#zG_2>UA(9 zvhAd$*tJAU3)tzgb2^+bicv1Jt|@CRNUeYf**a3tZ*9<NX&}tqrAzfYl~~KQU9!~N z71n?sIQkNM7%>ii=d09hJ#*gkr#hhhlB1ut5Bpb=QJSf5gsFGv(6{Ep3g+v(j<;r? z600kG(W4%4PE~~wQ8&3wQ1g)ok+sTp*4^$-gnoNv%=yxhtE%qYagk4T=@A-wDWTR$ zt}#hF#ar!;xJkp3UriW<`_-D^hJ-%wUrdONy2@=iWa9mEY3I+|Je)<{$RW(T&}jqA z=H-n}Cv*3=`blU<i(F^@hLC7a4u9)&jfKOAUx|yK<M~uEk^ZM^1J`yHm#b5p>@Otp z)a+hXeGI5h8JI>Apf2B1@d&ev2;QCVeltKPi;_QQOLa*bn%^4fHDOfvmO%iiNe+Np zL>4a7s<V>wl8ozGpUrXdk69?s4+-$gCuar{8_S`O?<~rS|6r<$$56(aVMWAzWN~t$ zvGfj~tiW4MMJZmEx~h$-4IkidjlCus3%t}QB~G^C<C>WKy*`x{)?UqK3$ALzdqA_0 zcFQBxz;4AgUJ;%m9ZegIW&>B1xngZx_(Oz1&rpFsgOylz11NdK2ls0ow29vdhBTf0 zdec!XRj_YdxUWMN>sacU7wSeA0`tE$xur?Jw|qEGei{%dn*yO?v@p-n#^Y9&l2*HS z+%504vfOTwEek!r`#?aAtgJ=)mmww39S{q9uExV?!czac(?CRtRP}fE+;p0nQBb(U zMDRtUnNjO{I<K+%f&;MD>((?+*%t|@RTm=$9By%_ySI(dnATp^j5^dGu!U%{`@mnM zzk4^quj^fg9OpW-G3~$D->S?QrVK>-^@E&t>K-wn%a~v*U(fQZBo}V4`0NZ0rP)rY zAz8~|6W8N8?MZh{)Nkh^cewH1z$E!jdo!L?%=Rq&K|xF^!Y{spFMZa-Bp^Vx_Qd>A zOXF(Vb(cL&Ns-QGo^v?fJLro!=Fy;-y?jZV>Ldl)qF2E{Gc3IoItRI7Z54#d4meum zx5PwE4XHIBAx;acBev3n<`^V@H&d;Mzq(gAET33ynNK}f)t$XDQ`%uP&~9SNewiGt ziu|25`g`m1;a&|1`rD$@1=O&}&2`naVe@9K9CAb~nd|=E3?7!7Wo=gr@u~-oO>(q# zDK33}Wu{$2;2yRf+76`W&y$%=ZeGeY6vZt>0o{y+-?RU2rLdY^LYm7F2xsyLDh*+J z@xCsY_2fMj#BQMQ`LrL1_w9KiC`u29MlX1)fU*LntvX-M8;y|2k60(g(ptW{qkiRY zp^lMhohP)LZ)(<?5uRtrF`$E*@2l45w(Cz8abIT>vxQGV3@XsfB>(lZ*#Jhk+C|I; zja5Eu1#XF1E$I6FGa}{FsBPmSV!6`Pd+-PDDCrxT^CqvjeZmVGZuAX@vc$GCI@K=T z%Q%kHPWNr8`tPUjWa_Oj8bu>okvY9+wZCfZk;}hy%ZPGjCpIrq=7>z{Yg-eM_w}L} z@$fl(`g#~FnF%D8secA-TkW#i|0Ac6#(xg_ZSfh>>{PQT(K5E~@qqNEexS7<NG)^z zEHr;l-_n&AbD_lAUM{|R>M_KG@l>a@%BHh9TO8j1Q*M%{;FAFNh!H{DRCC*0!`%Dk z6V)$!zwe_gM*+U`ZPu<Gn>S=<lb^ek`Rhl9B?NrnRiwFW1g0NaN-G|}cC8GV$yT5b z7vHz<BeOMhUG9GztB*_^vn9==UiEMeOpV&H%o0Agyoi9NrgEow;n{L-^|LKA+ytTd zIN5O%8C_F+lq^T_V9XcjE0+#hF7y1T=;uyJLd4q=ughaX`vrY*&N)Q|NyX{X!V0mv z(@gW30-{8b{zO~<GgjWH5dUR}c5-2dn#K9O5^UT%mm-}-^J11-aP0S|Zw9lISqhWj zxTW?jdGArF7(J@5x5=<s?IRd+M+WcsfRNf7`9Ct1guYr_drTbe#)GSbvo|X=j+hv` z7^R6&YxWm$`B%~({K7*NAOGA-O4j4-=3eOIjwN(^PwU)lSEvsZezn2Ozwsa~J2VYB z-?F_+`fS-0<Oflvr9&64VY|%G+sz_!kNKr8F@|~8C2I326i~gkn)9NLxcqLe`S%NX zFnp=q!o^h&C<uFneo4-$7tMpb{T=C{23~gU_nBv2VdWpU=4~4VPYt>bx7G+eY$FJB zm&j_T|8i#cWFTxBwtcl^Ba&Sm95ejfJpUNy&1&Tn$guz0IO0?b2?@g=`0)zMZ?B05 zXIJG~AG+bHEIt_-G~e&r*sxQS*y(ih!g(Kt5t7w!b{%R@A|nHXaYWt<8jhV}hvuZ; z18Msh*Px^aSGOqMUtoz}@CR>|_-a#T`YBjB5zoICbl&A=?fbp2wpeWR<nf~L3p+71 z8_#rq{;6uW@ORaZ)8!w)!t-E(B2s*LdHhzz%IWA?*$LP8<jBC*LTmd?$GqY!TO5S6 z_^KVBVP}P-5@+Ux%Z`p0YT3${3{c*zUfDm8M&Y*K>WQ*l{NyI;$1fIe;I?$Sm_BF< zThlcPdNGokj3im^sVVt}AE~2pgu?l8K(%}`xyZ?)`yXrFWBJQ>-z?f}XQ=i13S?&N zJn1^S{ED!|pXvP^D@ciRA>zDYs`q}k(?%&H<~W+q=gZ^na6$0Yt>p#B$QOwN`Ida{ zSbC}&#fVcDs?Lq4wV&Pr@j15gF9MQ8(ZkHZ%`J|F0W8aB;n!#xG6t$DT7MQJ`!5;> zo*tD%wjz9ew2Z`Lt#WnNE6ErQGb_LPXGlsv=#rSCdC>!IE335wBf~y0*KTe}iPU-J zWHaALh+rL5)H?rgQ^(MAeqh;6=iO07w3p0;r_iVHJwWV<EIJdt{<tzQ50mg$@hO@V z|CgHu`Ln&g3wFO)%v(EGHv6M5^)c)ACyli}4bRavUeDwEK@?}FJK;}nWwk2air~S! zAbk%@uV*g$#$6MyUtEuKDuXxLmnGb2e2$@O=dvv|^K7&8ay)CU9r^C2=N+vt_oQ0q zwHhvbL@y-oK#MX8Y_;?7AxxrQ@l&s4dn8Xj1bVABh1`?i?X0_~WzA4JGC%PA<GF(E zC)4Q1+_X&?9F;I$%SxJmEVDfN#WIl1+3V@P>gEC7-)1$mX`e5qK-g)%Ba-7IoJl%^ z7r%<2UYvDCxg36*=^?KRy2B8Y-(E#){R}&tbRt_q@oQANHstiiFi-{_{?Ydkohe|` zJ15PmeEL<ReUal1?7Vx1BY_?Szp?pjJzvfPb$>8-z-du&ATVtxImR1?gR9Ar*jE0f zp)blB$Jpw*S$T@&$d;As)g86Up?)K6tb|H5Wm=zy<Z-q@;wXs5ui(D3dV1BcU#8Uh zX;1dH>FkX$h~d$RJbm|5DFCq{6md(@1i?7c?v<KfV!nR>QkWMA!K!scYROLOD_9b$ zBnZj{5TB+<yl^q}<j5BTSbc6vgW3g$5F3yf;krAM`l0gqbN*e{TJ^jVzrOnPykCKq zSmc#ZqOSY+Auf5Ntb)jM>{{bF8jZaRQ9{rsACFIRGGwEVxCL0K3jx5~b>)HPrY%!3 z&*~kz%J9E>_g9k2ff)@yM$buYbu@kUb2>qp0*=2SrhpkRxmXKXQNaG=5A5^c`7|Ev zXgnVlXRDR+ztauAr+69>A)@iq_UeAPDK}wdgW*{p8Z14OC(MOhyQf{pJ-SA+6SKbG zk>L~0>*$l}8cULT;qH3AqN=HNB@L+eL9U$EQ;qKoTt9{qM5Ak6`;A`OT3W?0nkWTe zuR-tgvk0GXMjr9__xkB;V%h%)viDwjDGZIzWSB8H*86bLCsEHXxcL7bMW711MySGH z<){|*8WK;KC-z3A*YaEs4UYVm=M_M?);>(1>4}!0X4QGWO~-b-a)H}P?-7zNm;L%j zTGK8N!o8Yye_Hu=qzndmy<XWu3YC}-2a#>MRTz8e`Gc_rc8yRx_MNgAYps4(SR<Oo zOf3Y7?c(dS*kI@)R{ymtIiRZs#T0nVkGp)WU15FkQ58KCYN4e)2{Y9YqEjz;B3zv+ zo5-+Lq*P&G$<>4MA`brUt@mIb07jk!^a$ylb3R*gM5}-sh;b1;r0n2#o#Sl#r+a7@ zhy4Blvatr@7r6oS`WfEp&I&F@Qx}@_ktjEOM7M3u?C4z<hF4~wuSgLYfShhZxHB1_ z=&r4!u>Un8XOXCMw<7un64Ns}oJS8R(m$Q(CKAuq=!55%`<oP2*0*y8{fmRaEn*s6 z>O8lij1MK)VG@;C?su+MO2-3jT6J_A^B`J<W}OAlqw`##ztx_q7i-sf?MhL^+KwJ& z#WKj>Aqn4TPj}~AcO{#%HkDfk^QYzQq_c8Pr3!uaSE(HL{Fcwr;*-z73_E6OLq9~d zS^~sDncQ<q+@t7~_4bfEy?(#VkquIGeyZtsq3N8(_dR#`vSE>KGU}zPV>#|z6w809 z<k?Nyk+@dJY<&E=*tq8klvM+o$gM(XUq)0Alqqa@x^!+jb6E0r0z5&0i!0kCp%9&S zoKdN<-zN%WV-DeIfZ3Ju{-RMF%d-z>JnN`3tzDLj)tWd8!$t$sPXM3&n?0e<ok&*b zo|;A}&;F`$Md%+jx!SNuYuEvtp#@PRzshx;iuPr}JPl6Oqe-Z~@ZM)rz0F;X@AhR# z_N8B`^Jlwp^hQ7Za?qfgjP44fCE?HlNBw&!q!o?2>TCTcY8|7YsqrfXK-iSK2xJ)n zNZ*RuQ&)v;t_o?Le;In8HS>l(6Kc5atYG8xB_!W+lw77jhM0fny)Q2xnf2@#<@>Q2 zmbG@0mhH*=YQHq|VVec&&@t>`8*oesjj7EA+(;D_OzzI{?K$&PRRK1=B@N{~9!CtN zSUwR9qrx}M^IOz|tn9c!kjG5@A=^<TW-7UCJ<5F#(Z+3m2kdnY&rq-T`!J^<vB48Q zIA6jp-0<N3xcd9c%D6=dA&01>aoJ7$a;%hpOndcl=){vFN!_>E%1=)7z|Z8tcr~dB z_ODI~eHe{|AXGUQR%m2FN?$jJ0Ylgd`E32>j;Ki}KR2^gqyBLE+`Tdk4MYoSJ$X1F zG*rL)_l#HnXQ<DcQYrb9@0?>X(gBgNjwLE1T-v!jMpkFNRd5tHOFJ=W$-<H|e4KkV zrj~x<S(DWwY?uI+i)n<ye^LRsJ$>mA%#jl>C1TT?jdtqL>%|u=Hr2@yeM!}OY&8cO zP)4CP8CD_byhNS11qcg8a+xX}qqW8yNoGMkGROw8IofYm_V()EPH&<(#dG==|Gv+P zFWO=);23-_3afu8pjKBX<PP4bX`Xrb7S*<One>}qbLo%9*bH07-k-kWKghcLb+o(f zrlCcQ=jVA?WW+>Lyfl`nujj3mi_Q3xYc%kg7x4lW2<LYb+fBVNvu>sZ%2aO>2A6i! zM*_0a0Y(Bo?`ABC8ycL<BRWm(*$iJYYf?hzq7uqxj;EDBW_}ciR-<la7lt8KL;_a5 zO_j05lXv5j`Js@L+j*c)qNlQKhN$~xGtp74p-5pgpx*a;pP7-YZym$oXJ&J{hfl4H zC~Yw;3`)mFxPSn4Vtb><Bfw>PwcBK8Ik$GcTGwT)j)M9?@mUJkD{s$H=PO$}0cFcl z_T}~+%nid%Mp@{<q&dDTkj~0-p3TDi&cMcYSEK3$)arLaRQ-AASk*a7OVgQcYlab_ zXh#%L&!Wy#&N2`qv{VD1Q`i~4yJ{L&r`**^3m}W3*YZM2j?gv!4i!Pr7wv-?`FhZ} zKRbRI*SoKpyHSybxM?k#V6*@Nl=|FrYu%+hBuT^b!;s*02}>_2rK};LR}qoEzd0YL z+6K>ph>q)BEL@N0<S4_KGPUNi1G6Io8Pj*$HIR<3@)zO)l({<onsp5-Wou9@{>&+O z3(kzJk$M|^yH|Z%iA<X<ey&CwN&kwv>gCpc@VcVjxEVH!M?(@%L_&hAS>5dTIMLmI z;?pDh|Na7apbD^lp~fRyPczBBFBV!pOrf~xp#T)2rJD8TFy}&EhODN}{|^9VK$^c| z9~E5HS#}*9QRy>z&V-9?$cETxKwcF|L2#+x(Utl;TJXAq>wDy5qv4ch=Yn%k;Ppj+ zFq)(gbXObTC<6ScMJ2MZ;>>z28@a)RK8P)}A8n#j__PtzL<`RY)E(p|SuTt{h*qK2 zBsq2J#ByAx@|oz)-C<NLb3~rJN{NY_#c!yflYaVvl~3D}9;f;o^}{EMw4eRib9zSI z`E)m6O0^9)gw)|eA|C<LSiouV6AF(uzsNT)wa^h8`8ynK3|qQGw4CP2DpEDXM=ih> z6fh?BrWmm`85vrsV;PW4qMY?sntc$&(8E!AV5h=2N$Ei{Brf_5XFPndKCRb*+kqFn zg5@hr-xcSRLkF~<$BX)|u$!wD?Usg?msu7n8X_A!^=I`KwG6Vf&{m}An@sgEloq9l z)`JoWL(@WOS!jNV?m-Oy{z6=m?C}%F{Jx#Z!UC)#5h2kpn4=RqwMZ*pWS|vX<!u=T z9-5CWnth1Kf<JYd`wj59Hf()Gr{e$kAHA~kXunGC#yDED*vEyoIF>kh;OmM$nYegC zr|^P_4@Tu%cAbA>>j2SS%~e8Wv+S}gErTx%cfT+bMd^I0Ev_Wz`gH)MSM&rl)3TF9 zwS!-wY;$oNyYq`k6T^&0M(ppFkGg9|o!hkoU!~UtyYG|3<8cn8%|X!L=JQOiAr$T` z6ay$DNS{X0*AZH6gue|-Yu(0&DF{PIoA5DHCK_TMbiomW_z@YgL6B|BhKch1ZG@y> zgV)02g4)gQvoMhw;VUF2pC_$CT_+y~Hsg(c$%x<0`fZT2ietK{_x9VDbX2ZW(b&|* z{pGWteO(t1c^vnd<?$zu>8-$OC-wrlqW)E#!hh+D^}fC6#NImo3|&Wa?GPw;rV(<r z2y4-n9=TPn$POD)@-V^EA9^uV+G`PF6A3$P(b7XL`sK@phq{FIpIRpcERFgR$jpGL zcBv1J?XHNo*-G|=e)WkjvVHD0qGlg>!Ats=C#S$&4hWV8<eORkcAP0qjew14<IlGm z!i#Z_S}TSgUC2YOad_dw3odjO28R)4&<U@Zg3#J#KGY1<ptrt2SgZ2=!SHiJ-*DqI z^c!72qF{0pRwf&?6}N=bcLh5BOm4=(ivxZAWuYL3zk+CPw$nP&pFe-mAA^NsPg3GZ zFrWL}8z%FOJZ>L7ed?&5M_+G2F6onouYK*3uBh{A!=~-y$0eiv2v_U*T56gXs`D<2 zzubgFHRn(CiCHxAA44t|)`zY?{{2xaX!A@LW7Gvaq7tuSwEn6t`L3D#jaq@&hl~r4 z?-aCHj2S$=g;C8e>w%XiP9l#p0T`G8s&&_pK4M3%FW)C{j~D#)<<lN`S&mboiEBV; zY23QFN_4~&!HvFNCTkN>`m@L`Isycf7M+j`n>gFDya>=DJ7?qATF=Qw8F|s+C;k>2 zafl9U4lCi^5BTA4KfS#4;+0N=>DK<`%uy@8OjLNaGbkD2sXhq*HR-w|10<K$b~`aI zAhFP|*W#=A7cO4W5x71k+|ssvKG%OyKO;?ZJUAY0#>LnAKBC7V`$4A}0z_Q?CUoDL z+IVQlv&6Jrmn?c7K6pKT?n~H<5PgKKU$s<cSb#IEMPC%;F<f<)@16kph8{fo6P;Rh zkoM%-#a4K-$anAAIof=A7_qB+QzHt=@;?5r-^AQOIPiOTok4yQeLHA4<FP%{2);C$ zYQQE^LS1JH*A`r~&^JYgj0@B<sMUzHEeL8|OzXsUaaLa?7-~a|sSW*Bc3hL87OO$5 z`27@=(!M|2_>}c$o~e^FNb3RugHjXC7`37Z>Uh!#_vCecNRaBhLjW%tyjs+2-sz)m z69}oD*2Tle^x*XEhOh0mi-rsfox2CGUq9-Lcb!V9JDIkG$qL=TaWpMBIa!5d^k3?Q zr(ZTCC)rk?a5SL9p%qTAWXnD_z+6+LIsilQTrbpV9*d&Q#j|V`ecXax-s7uaVhoIo z%J!*paQp3n7rgTLP#nxK&Ve(I44*BoO%#Z3jiBHr1d{uMD8i&eAtM8h*er^jnFfx0 z+D?3zfhaWe0Pesc^#GbH`~6n7h3`F>Q{d;Cp40d4lO()#LF2m$P!kZTp4cMSRMiw! z{3AUXsL5G1@dOq#{7hghgOhsFFTZ-5aYCo;I>pbeyYU?zAg%lt?mi2hA8OEiscRF{ zJIy(1Cusv#w%>`LI4XmZ8!<=@zvi_L@<v<1i`#e+$UQp9Rotb6MEPrD*p|Jh11;YL ztolh7pQcHI0#ZxKx~b0%L$Mh03{jy0C;THO9$<6e1+Uw$KQuYm*K~phGKkfHP`YnJ z!rj#-N?n|lE=G89jzRD?<r#?#{k)I?k8W@vE%FG3X2wRxl(E_NKNv!qH^0}M+4l?Q zfsc>zUcM8miIfS$M5;fM%wnL#yCy1`qrYnR1g6wXO63@y@gA%4Ss8-!IsN00Kc>^t zW71itSLv5rFAE>h>xUY2(}^zJ9XrNDKc_qUGJdE9cl_<I2hZgfJ+Y7r&Ga5?=ZuW$ zBxMmp;-MRT6GcF$)m_QbsJdER8{n+JLGJ)8BRssSg?mLqj@s*Wsh?Mm-_>{dL4&dn zSSFx-N5yvg?K&d2kkwN%pVjHs89Wh0;Ld=G?VxG_5S6tWs_#H28d<nC%|IK-yy(wx z#$BwIX?68hmyr#Pe()I<wYRgOCXE=?555Jtz;wS`T;hk+KlTaTbmJ%EGVLnvL{Bs< zQcp;AieD3JrZLgb@uFxODWG{cYCe5pDA|J0!f#Q3g7Cx>r+h;Y3z=X?Tz%@!V!v^H z&kLP1gHv0#@p{Z{SsPrmfp#}&qu=~^a#W7B>i}PU$1B{m0gm|NOhjZY0ZM0G=%c2C z*7Zw(LmkP|6L8JwQ$W|RVfmLA)rd7y-tbsq1VZ0_i3IHl{XoUj;J0hjxzqllitA&! zj+0=3yIT0yHTii@=5Q#e6*!#{I+W*BknKJ~D8yL$kRt<1Gp*3Uiah$Ufe82z0^g@O zNO-gIw(J&9NpX|WlTVf$IXaAOwR_Nac+N!;3Vuw_zJKNAYx-zQzk;BcGodR&jx1HA znwA70j+r$b9g_O!F}bq<5Ssho)-F);9?T+GU;RK>Cv>mCaetV5!bkB-7q2dS+N`tx z<;cnUCZA2%a&0OqDPwkfd`*~m;YU_5@jG_OXB;Am-Q=~->$Psy$6g1p_GLl;oAJhK z)!AXRLl!U>@c6T}h`Lv)!H+Bau-rLt@VvoY4HNd!%;3lBDF5m%otbu^BfP$c@tqiU z2-*aK1PAg;K$m^A>kM5XOL{L>0Ys8s%at|tOLiDb-%i4lj{B`2bB1ETi&;ckZH(5; zoAFt8!@veZomR@|Yp`B;fd%(qn%r$P9;@{|-+Ov_>OEZHt-{p8iC8Vy+Oi00aZllU zY|5;p#8@YK^{HXel|A}V?pmZDm>BWNXXw1e+uPIU_d4S9MIXMpiwXNln1U6fkgXhR z40|BM>#k+NCwj%$hQ)JSEtA~h%e6{A$YQE%M29}Ij(6~|4F|VXPuxeg@+uGBO1XNo zZFUqG=z0or7vX+wgh))~+ECXyKeC_BOh3?qzj9^yP?R!=K)=KoP&hFVE>Ha&#fXwQ zNhXpK8x3Cg4Gkt7Td0o8xAHJI$wHeOtY(iuXFhoNfgAjZm}-yQbQ>{uN=+ggs`sI@ z?Tg#oKm6enTKK2*<Q;y{VND13Ov+45Cck?fughnHOgzaNPXQ&wzi3_rB3pkefbi{0 zJhWNp+(G7VzqJkc<sWRb5!1eHW=)|D`S9U29#Y~rz2i46f~b&>{;iIpeH4vu>g8qY z`ia>3(AL-sxB8H%+Ler?yZ)_CL8`uzA73`Z{8mfJ9cqOpJzp^X;Fjpl?txC}rTI)H zueivZf&q85W_zGvXM&XITmta)CkSfWLX5}ZM33*tdWMz<3b)Z*p{9kEAkz;-F?e8P zE11=ED!)=Zh`cX-Og|p^pc-Lpr_4fsMo)d^#X{rp;IF)_pDOm(X?tSoTsD)mU`5PC z%p^3LJgJ9udQ8!lK>1IxpdrUXuLo*yF%J7Q^BQ;GROr#C{3{Nd1+q=E8soB~VJacp zRLfPY6iZ41wj0^WrR5M%!>@M&;u|LxbY?y@<R+ROw1v=7%;)p=O(-2K`ULGTMC0~h z<F-*DH+mCj6Z&LCYxDr^Knq@HUhhp2;{Zsj0WU}&GUd9)cNEpeV76fG03_wUB`mkn z>7t7a_%287C2znG+t8WxN1$zL1Z<^Yek3xPvP8S@V1}F4o{Ut!ujyy7-~ax4u3c)i zDSPjGq(pP5Bi^F%@!eDJIi?r=6W#&vHBz33{@SZobl-Z(B{5HMlWH+zwen!H*8&Kf z$H(I*S&ryD2j9^su8e=icN-4xN@80Uo)<%e1ie@))0L{6<3F`9v+II5e5*mDMuL&W zNBD*)ql1v3#XCOxfrtKl)I&5n6OJwxtOwMe{b3uM#kM`?Dw@AbP>MyzuEbju&C-K_ zFW~j%y@1=b11)$NdAj{~U|RviDc&SV9sJ#15~2#G1GoFdDtd4WcL&4L6R_bxo6@FC zb}-#XlpR5M^wXkGAAZ_Q4-E+2@mruc*4s8-e4ZTjl`owi3%(Q{+}2Tas44rlcXj}e zKhCRu!kUT1SpeTWJ9mzq0GHHiA{tKhuf<S>^W^LrLg)MN>uI~V;$?CZgD1IsWj+ML zv)w7+nFoJv_B#{6v6}(i8X9a>+$D#&V<AV!*#2RtnbtM5UG&RA$7v}EQloNK@fY0` z?+6k|VCsPrs~4i}h|eZ?nyx%12J>O;(_;Q!9^n<>)8tIUJ}qzr-vk1sXnbw42CjlH zJX$M_WH$#GN60m_-_Y=^mTLG2-@K>{J<&6w9xw^S3~+RWCajqpS>y(e0BxcTo`zp5 z{^I#7zFREcjUQo^;se{JL^z}j`!#qxhmU*LpLp`<@@=|W!wp1yJj5fWUVH7T&jxCd zXe;?KK~?|jdcq8+@0`$;&l=B+>r6`+(a&O>$A8sZ=S6694lKfosfQ2aj0LZ>Io9we z+GdSv$6)oAopUuVx>>C_?&CMTcMLLM*%6-zrnOO7Nd5^HP8ZfS<gCjy)3qU}5ZZq7 zBotU0vwL@hm$9cgdeQ7a9!9YUhSm`0@bVSM3a6Mga0u7LF-CRVkO5CWu(-9|mO)%{ zkjn*xGQrMZrrCf9<LKXVrnkOGqs@4T*b99d%GuE_5)YRA)b~75TF8w!V(7ubF;?w% zlzg8sNaMtbBYNqvT~FzPfEqgiZhPed+^hNskVjW@VHP_1c>&6BeVGT1Dv*b#^RJ1{ z*r#}s3f{F%KhTz1W?uL#_SK89xyGsaOtbV3xu!d|)~g)}8L7n<hzdDq&0_vR<<FvQ zhSE$+h($vxHFEW2yK>UmQokKWg0ZvR&Lz0rOcpd`_w+sdnM@ix>L46YJMDNzu1tg) zV39mvN{Hf+n2f9tK9<6xgSEiov0Z852|6(PwAR~DpJ~XK<PcBSCdG7cBD&wqm=A3i zl&GF2=#QFSedUtg$3E^OXUsoJFk?B#RAVqxHgR?Wk3F`h*Rkd4r;mA|^T_9O`ZeIM zzjQ_SATShF>l)h{Ppo(WxwN6JFk|Mjdv3t1VL&&wY)3-nf!@un7M%tMq3t%)GJd;g z(wpaI%s98!t9<ZAt8L~s^CmG{0=7e{h2GNA0;?WYvXjAu-x@(wtGdkfSZU^KV;6f% zA7P;G-wyO1UIxeS9^1I=CDg6~IaLz_obO1_dugaC1lr-v6K&Yokd0wryo0y2D?)#( z_2hHK!U}CR124%8o6tyPjmRcuy|4li5YFezJns3Ie(~+)6W{%~)iP-AJfa(yW-GSs z^k+6ZfK#XU^g6!qOSzne^4ozgy>Qh#0gp8W>G}XN{#Dv$L|`&r^{P3Nc>+87ljsIo zN5~?L#ZTAHC((>!53zHHmL~bsqiVp8Mgpx@airV4brN}QBjfZ(5oG}qw)I!BmO<N& zZ(ykwB{-sKGalNq)Rp43zY{yqf>!{(R|*TM9nek4xJD5g(5KY(Eb#38*1;p|N^|8& zr;D>Wuw-|WR~+aCn0Qr9h<ZU++bE7`O)h%m1jng<srATIXL8Zp)$SILn@MQj_{PQM zJ3o469OWI_Ml8e9Ba39xU(@v%&e*s$p7f5?S^h@cd=&VUPXBpbyLNJULl*$Q^u^0M z^6NSG`dcTt?5Jj55VA1FAE}+=Uv6r#Wbi_(ox<jPDc-#JH|9W0^jIWEw8j|C)55qm zEpNM;@X^crcuyZ0AkT`YR4VUQx9^G!x_tuq?~8!Y{cJvS6_X&63EL$Sp-W|E)5bx$ zRd<2|EqK{KvBQB4dLV?Q0P#YQso)q837KxzIwWJzJFDSKa<;#*N0ALDoD$d(lKKx# z5Y*1vBSt|(bHC*f4sYm5HaNWCLM=Q88J-|t9;tkm<)7&X8~)XQ{XJuF?9%?rgl*d1 zqq*UzoeOeKkt^|?_ui5I$!RV0zH~8?VDZ1_X&!iS$`}5=@|8<<_1`tF0;EPLjqx7^ z!CSR?#{i|~+2+w7{R20-XBLjx7ggwl=0pKnyv#dj6Fu?Favbr{ofmCeC^}{pBYyeg z-*dr1%aeSXt}TAObM3flu8U6$Y6;0p!US#L=^3hYZ`bN@bQ>uJblgVEa2()-{{g}C z@?(#j*eI=-g?oUML5jDrLvUuY(izrZ+Dk`Cf#?$qFIoZ%4yk#T#hz%xO!p&>Y<R(2 zebA9b#;?|o%w!w(@#E}A_01<e^kMumj3}c`@`LsWAxt>Frpc|a;M6i>#KZ*WOD}xp z&S&};^xR562fm(vv5^U<{U7`IW6NiL`RV0pe}})uug1O#IBct1ctH#?)-~Q`mf=jl zv{zXq%5KT-x2;*&T#UpJq{eMWxTjBx4xB-boa>=})2t^Ph=^l_r(XPGr`1hI5+1a| zwN;0gWgEtSpsk{$CfNg>(jRESD~c1?wQX$zjdY?;IEgfk;A;Ss%#=YDWgSSw-IPfN zlT1t3-Wo5l*sLl;8x|&f1W`%^Tc*@+WT**&<o+Ov);f@aV=c0iC}cO$_-d$|<X(F5 z;_~v#`rN9U2W!^D7OzL3ks^znheTe#)<FV#hQZIIt#HK@jd4bvtN0hBTMx0=Oz5N@ z+r*cAKK+@~st;Crtet!L!cFWm4;W{B(rU~%@zNr^V4L`c&aNV_YGpY6K>ID0^CT8{ z9h3bwj+Fc2TfOBH19?y<lcTTRs=n?J_@o0l+mS$^g>9gj!p$pq?~d@5jEu`L0R}o) zZ301DdlSHGP@SmoCY!z%qLn<j3~)&SYamIf@bHSh$`M~RXhrWx!`KdNQ_fX`6kXJW zlT5j9edubTful3@=_9IBB7Y?mfo1vmpF5``Acx9oHXEJ5FdqUW1$|#a3tgx1d<N{X zV${^Msk&Fv#^Fl|{yIhoOGcCR+EqQsK}UXe9P6EY@asa~r+?Y$<-^}m^Rwm#KFUvf zwS8<;E0?%^V#D!4RLh*$g;#7AQ)D{|jnB%mgvunuC^dr5M#T#rUH7r&BwF2a6+iYI z3o<ej+!o>K5Bi#mi7=`KTBb-<uN5!s_S=IU;kEOwhzM3jiAOMr2ihHx(^`%sk|2=& zN}Cq4c<_^3Y*rD9T6SETc0dg6XgD1fOzHABXj#W7e83JmPGf7+)yT<uaf?o?J9y}7 zC-w1_Tl4<OPrd5Tu!~sR4R`ScEB0pL`-F3Y6bqgEw^QrDbu`+!1MUP#w#O1KI#~b( znZ!JlQK$P_{C;aNnR%Zc-tj4Y$%m)eqKnlU%gKJ*O`YJ|kLpvh1uNc_hMVwjSIV1m zv@#pDC|9a7+GOb94HbU%n;)C5h<L;g?wHgse1bNBTp0jDYVv@1ZxO>87fZjnbNhCn z1uw_zIVs7}fDV3zGt0p%eo`2dA&H4bvwW23!*{ZRO|RtX7ooOQm?30q!(!PPG)FLc zyO9xSvD;S$*uHUW=M}+DJ7G!xh%RaSz5L|<XY|bVS6^96i46}LGQ83zozx+$dj&c2 zvd~$Osk?0}2ajFqEyx7+<kbx!{8daAtOSrDk!D4{`daKd>a*y1pFb~rZtwk!j`k<@ zy?F8`4x6!W8B?<@^(Ru*Ogz(C3gA2@2+gHj1&0j0{aWl;8KDYgg|lLKk?TC@df~gT zj%zau!)TL2lc!L-1M*@MXao}wfHSTVi*nm`pan1cm+b^cTdz!5pm9Wma}&xukt%s@ zp-ZL)7p0C-gOZ7682xq`<L0$~l-KbrW09$fU_VY~oG8@H67J$fP7u_z7NYML8#SCZ zYQZ5Z9tF@pKhNanyA8Qz`MLl1+`{j|&CTM-@WM`P&2UzD2RIKUxumJ#_vylGa?3Hk zI)KnJejY3F*1C28j#V3v73$bwajrXwOk`~HKA(lpSw-4${X7?^`1}wj_N%NtPezUD zV;>h4t9?LobMVDeKXIZ?OMk|_<1&}jTd@1Y?6yg^{}$<bVY?}kH69(pbRC+nK6WDy z&-t(sN(|0l;(Y3X8JjY%I}q>}IODo>BF=5wffl^%k5l>)2~KytMi4MtrV)~?fg<tV zA+#8kFyFE=O|iN<5s5w`L%-z<l>OjU9eTsUircp6VGXe+Fd#FHu3y@aBl<03Lh=p` zF;$mea)spvPp>ch(p$?f{EN3Yj&|@d>9B@^wbspKdnFzalP;(27kHnptLvl5SJfRF z^@BqoEis9DOvEVp=6BzTrDjo<?xc=&+vcJkv7|A<KmN(b`qy_w@0d1%5P6J0;i*vc z=Qj8vg1eTDM=m;JQJaxuH{*@YfRnp1H$!vGEs43NhYhFVtE{XqX4Rx<j!iTUmY8r< z+5{TGAOxWG3+m+S-W}oflWFw;bbD@x0&X)cfe4b22PK(z^+}}?%O0&4b`M?(8S}^v z7j07p%C&8_ek<(K?~+LZOKr_2hUM0VJ<*IJC?sSMNKM{=SQ&D37`!8XuA4jXn*a5e zE-e4>AH1yl(l<}>;S++xXj$vVADx+Q=B+o}I8#^8vq;K8)wYafidFrx<3z4(TUJ4N z>;%U+j&RCmDYU-jB%s(U{MFahzOdIr>3l!-34WzkAL1jdMh)!GMKMJ*Vi2xZ@O8`; z6-sUe6L%9QekdcwNo(b0L8cDjR2{m9s}XbjSNww>17Z_Db{(qo80wDCbj-wVVEc$o zyeD1lH1FLJUVD$X?F5H$-T}Gz!kt9(!8b0(NGXMPqV#2|29vNW;Y=dS;z))(fXXVe zhUv#S`upJP1i)kDjL`$>*0{lplc*Z@jZUz|T41eBWZ_GP-+(*e&*rgH(y8aq=?eZ& z|KG3qd3enkfWh>D;+Ge?v;L6Wjrj9)y?bD>U)Sg6)IRFY2XjrZX?0RZj|I~8FUfTb z97=MWZJ=)t9k}hSv0=OHW42<t<a_W-h9mv=e9vRUM!8@S6K5@u0g*zWTj(MWSy&nW z$Zo?~e-}<2+=i=xxHi!VwEY$t;Z5-qU&eCqAvB%3%y%-z8_;-*+6JMup1u$NW@5M% zJkTk<24a0U-c|(o#3|5t!|_{Up0TBrVk2mI@jS7TIdsdHk+@RN^5_pA$hGWDw`_!B z7h+jh?Yln&EwOGUdQjvYPzZuSXS!)&tQH^B6IoC4^|P>mjq`(>Kdr0ypZ|roHcoY; zom6>?5S;<%BDDf(bK2~y?>c>_rc9(7rv!j7co`T}WMzQAn(QmIdGn2#KgP7JMW@WR zeG=$6$M}{B?TG4g`|kQ@mOlBZQ=OiaMu^$rRHU9cvTM`xIGbHE8?klV@gIm^)aU+; zC$1IBc9~yc=VtmAZ?=^%T_emOt5@QzE?>ZcNMP-vA$(fkM(4=#iu<?O9O%|s9VuRQ zaJMmLHi0T4?ztwhHJO@pGtHGQaS3n9syY;F=nGGxgC<?On}UNEy(+e*E(Np9urO$m zckJGD0%f@z1c#3aMA-ON-j<)~_?peo5si=P*n3~Ra7F(4T)yLJz)d<Yy>L;Foj$6Y zRGwI#Jj=7)@wK+?K5pj51u?F?*VXWe>k#I8q6NPkxTcQVf@0O8hmFLjq3O_KOPB;w z$4!WjYT1F<4<sp>CU{++7_fuNGX%JR_uTuA>05YLZMVenxB9ZxuD@r7gCwKP@h5Jm zj6ZR@H_;Mrai%;(Xz`I-WMr=V*nd;o|15)nHD7V?JJB2l3Zjt%u}h(zSc|<~t!>!W z%)ha`Ja_lQ547Ord-DiSFeBP6)6F}ohbrt68Urr_8v~_8N79q(pkb{>&x2?Gl0(!D z9)sH5THtib0$2@h(^_zqOC^T|1lI-`A7hY*0ny;;Wtr%&sv}Cta6`_XZa2MfQP)9r zbQ>-DQs)aN|NLiPH;dnpd;bScjW6L0`9?B{>2`TEVbswg%H49Q8#3fu{erW83^)zy z)~U?;b3L&~&hb0Tt#yUZ*mlF{P$+#k?5H@U=YA#Hw|(2m<?AnUIUmi`NwY_2MzrE9 z%l2X0)z3`WaA&{CtL4fFF5MD$aW`q1yHI1lB(zP4#jO5Jiy$<NHw^09wbSFrrW5a3 zezu(eSN^>_!pr8%J8ssm6jq1T@9i;esF)}z7BJ0tch`xiS};L05H5m*U6d`_tc$(K z&yd;S@I0ui&FYGiYP)z@05dVMie|KV80g6}pjq}d2D5Z-rtuZpGpA1Kz4c{(mDcrW z(;G2K^-KTqyuNj&o7L4m@NK7-=bk;OEB{Azrhvj`BP=~q+%J+NL_Tk4;d8{#BdChE z)&nclErF-*PSGxEk)-B3YwW-=L}|#maB5Jti{!B_As9mz8SeH|X>8+|@_66-kLl6h zT-5Bb0otHQ@)0*!9x+9B%Qmu+8TN)u=O-b{*dtae7ihGb7P+ZAHl&|&hTSEhJ6NKH zjEm&sFI*0W6u>Kl72oC`xrax1xqIcd4z~izd-%CXwfu-#h1H*7irxv7nG7(AhC)F~ zkIG~aBmu)9n+z;4VNo&bx4qb%1P19Q*%(gc?XE3jkNzA2EXO1y{;&b9<&S7o&*&$a zuIkWsUXR7vFkpR=fK&V0moF?|`-(nSQ#*eAh!(`D<q7@v`kBX%`Uy3&D`e>Vh&j&V zYT>i^xpJ?s?bofZ9eagGX9}Rv)GlMV#uiyTO4qiE=IVo*Q)AcN5CvC#&Umiq>ODIx z+6O*(QolH>KrPaXU7d<k#17S^E){Rb_@jufn#(Rh&l~9(n%)AIstuIn8#y?kmDFvv z4r=v3&yTR6C03UWlTtaRdlM43y%5mt+r2x&>+6nR=6#kM&b!b}*%{n%?w+#fNX7ju z!Au;A7`Y^mgX|C(-Qdo<H_<DeLO`xYwZdz>oNg`lc$Dk-wC<>K+iJN=u#-;y;r07S z%h;zFV?@C%*c%7GniG>BWq3jh_VVSc`Y}Z<{K&UxCDV4AhIt+_^$S1$rs6MmTpM`i znUl+Vbp~+u>~TGq;)ra=$QlEu&2{P=9{0Z2!oQ|70nP$qli!~o@u)Vbc_x1P$d@39 zbQ**Ycdm1G_{fs3$B?9Ch<gN%6Sw|3%0I7%YrN>Ad`n_mZP@Pm*Zce08F<dfmWM%` z*|(Kt!#sJPc{K;hc5uu0W}B8gQjb~I2|yt1skyfFo4)4Z^c~j;?aafUR0}2lD#gmG zntXL7?2nCo|NbJfe`Wclg;OnBoNXmi%yoO*r<{q7^fdtehgAANnt2RP%R%1LU`8hc z(<3zo%5vyf7sFDl(KPOY!fSfMrVoi0-XIit+9qj{VjIVPAqFlPCZExUup)2&C1zRo z5tfQ>bU1nPxIVr+s=MH>YSQasl{vkc++WdxU{N^sQa8>~`{i$3@~gzLsb83V{xN+m z^@Ps^PU?C2$$2)<dw##~H<3lp>3{7AD*QQ`$}JPswM&>&okwkmQS?|yY#uKK(<w+u znB{b8FaJ5pKmR-no*O-TF0`TQOUnjo{MU<m6%ah)k1ufb%&ajl@8FSvIY+gfXPKz^ z6uG$uHeN>JYLGxGhKW=mSk^rCx%-fExZx1I!Y<m5t#;!7atHY)(t#GdcKn|omk|<3 z;7ulrz(qSAlCX@=f2YMPkxYTW8o*lK#+5xTG8iNrJp8!MV9kRQng=6Qo+sEui>#w+ zKUHBz5H}6y#2E$pfuM~8Xr6U~5iwml;Um*L{nDC#P*IN_&_{SzbhN*sudea9EQ<(S z?uSOkWb|-3(+aP9*T1aCX7fsW>OCixXP?tqz*EPUGiQ!!Va@rJ6mJ7^@yu8KbxQ}2 z9Ur+m_R8Bew?l%6m*%12oi?x*3G%iio$aw_T=~am7W_#)M~_zC(Ww?)cCz4O{y2<Y zz)<niN`#r6a?x6E#D?v}sZa>VO68NSquHXxKm36wP>5;vpH9QbHBbBgIy2j%mt*Nb z-(g2J`@)vxon2H9)Xu+s<1N+Y%y71!<`mEuf9ZlJ>Fgi_18v6V{x*G=zTH!J1ywGE zLu4c;y6vj(MO~f3HwG<1&z>;F;9F@+XSe*Q%`h{|HJ%s|Hr~SaBRncVb*^f`pVu$O zT)e1{^0eqXHR~QgeU+Qt%d*|c7^lzg|G;C*GkPn)^?-adhWEK~@sHbKIr2M52ZDqH z^}_JtZ>3r&NhWgAwR$ZoQyDL{$X8EaqdlcxitnS$=RbdG+>yA>4PmlX>g6Z>z!96A zj#*-zTIi|2wTWx1)%X`<KYUANqdOMAs53r<Fr9>j68CXEE%>7!JGIgyQ(+`zL+n)M z%bSlsvDZIi3U&W>pd&o$3g*iKpPbfHS=|mu{S=T>NRCRh3{(=Y0f>njkeL=8pmh{L zO{naF8MOFnUKeksTi*+}WZ>0af3icR@gjXNlAQ+_o+o6IUp;Ti!FL-GbYB1;@O|3f zc<h*mDEf|oj=yEbcgv3*J+Alp$F<n5>q47eI`Uu57tM(ob12N}O&?@(Dk*s=oTKh# zy*>ES7ccme0Mt*NKDK=OM@}!#zV}4`3^f+Gb5buYLfl@uD5wi;*r|BCcKGNT)mSC% zt{)K)7yd!OqJxv-7;?=h3;r2BCHJ+Ld7D!iB!1UYwAipo%uwT|3s@^M@Pfxq^{vQe z<x^O-N~vN-cIbEP^h+*z;<K_bV+&WBC4dmE`}pB)SSuGV{O)zx98(0O_J!cLg9l#l z8ecDXf^(b%Nu$#N|Nrg1d8}pGb>4TYYo2;!lk6rbaganyvLq{t;|PKwI0Ili2oMK> zh6HwiKu#h+MiRh@6+wo9*a@U0fMH93oLDyE43;Sbc7ni?{*Y}+7EMuuWs0&WlH%MP z#AbK1y1J&W>aKjhZ>?{ieb0TbUsZQ?cQyO%s(bd@Ykh0jYwvx}dFP%X&D6&UzAyM= zsFL6sHdJkC)RCi4i5xyX9P-dxwG?^)tXu|cBLk9wLS!EL)1cM3RZP*@TKEQ(ygJ=> z+z*n#Cna<z$QAvN+$NgFrsA-!>C)#ylp7pI>wnnC^!x15o7(O5BeYoj9BN;`u0#Ku zJS?(JV}gbXrX98nE%Ks~7q8mTS9LV;;SXKhd{~NS&K}!*-Mi2E96%Ny3;)d7y5p|i z28$K^R;;$z{i(PKpfC`8)htC?ZsrR>2q}?sP9Sp9j&kn&J=V(jmjx^lcXXjT4ioUv z7N!#z+ZZc4bz^J`or;ut9{)wB^o;9xk?~1csBosbI<PRNw%<!w9VvO}7+lwd`ov=F zg{fO!@VZv_ca@KSyR%A0_AXO^C%F+~>sI=uXBoU0ncMS1hfbTp8>RgWSrYF-Yn(7X z(Oe@xLawP%*5+<xa9!i52eJ5OcoHoGi=QnEZWEJzI7V*@=dfBQZ1dFUHZSnt?V;lY zlr-@Vz)-ftrGH0tYMuqiefP{YjsUoVFGm75Ig=-TtILK;sfR>7N_mODjUU*-gE)`s zi{|UU;r!-JZ$4>1DEKi|Quh7y>2F79P@LLd7Nr!WgU|96@3r9Qr}^qky~w2GC;B2T zWDOWL%qz1JmDS1@X~OU1+k1YDEPm@~0ABUGbjl8>t+v%lR(RxvncFpA;$yK}n0&d= z+AV%Iz%F~?#62|y|2H=us>Z?FTVL?^Y(CKTouhPz972y=2Bg}DCu3;9F=;E()Zx(% zT_y?}UiqsnSSJ<R`tOON$&$e_%7fj>ZP^HoiKQ^WFC_z!dKsvK6*fBapq8y+-Tkm# z1O(_$S4mW4h(+^INS}e%&tk@lAG=IiOaNsIA5?hGhZ-CK=++DG8Nt?~d0r>~^hKaU zv>SeY6=)T0M!)PR_LsVHEIZQw?cchv`P8S5Z~o99x=)t~*B{s18UW_{vlUrsF2Ny# z=UUD{6+QV-ARc2wg^&&;!8t6H^x;)lyy>RW*BEQME7EB@)?R$o{K15bU`qzDPaF%+ zZhQVLSYw<dA<<RYd;Dw8tTCmr=xpg@s{x6fCVO~?*|K$okE9Bjb9K+=x7N7*+T-N= zet?PpFQ7e!U7ME2)a4x^$T|Vx8$PNjb_>*YvQ^K#(8xkBbzx~E_u>ZC0a+)(DJ$U% zQpIsLUUKUPZ$#;YwG4Lm4~&Gd>wyorJOG%%YA-`-i18heX9xJY0`BTn_RPri@rXma zRa-St=Y#5`E;+p~*{sghcsw@y<O%&QfBJ+DjZf<;y;GYrqEBeGa`BHnbeMSLL!oZ= z;5~ie`sSDa>6bQ_p2<ZS^`(A~evPTTP+NW4EDU25ICXFpHyC~3TeCa|1MB1|5QVA! zTC?#WmN~OX$_`R{uzeP%(8OVTNNMBVC`_5ItRk(bD(<3p=MOruvDDU{I#SwpfygC( z__K*ud<7HvT*p-TlEgkYADl5SMBnm)KXYdDLFK{s#jCD}A=aPBs8yCj6l)bz+Zx(! z9zr_54ErKaQotZ5`QdHpc;rCIRtD9Lgjy4h(aE4u%cP6xBE2m?%@D5S11v$=87kP? zDqzE-Zdz<V`iaLu>u0YF<Pr<M1h=4R?1~4APsyvs3uSgcsh3b&UX*cEzqxTE&si=0 zv**sJKB<XzTwefs>P+IZ24j&@5`HOo|NEZUx}BGw&MGfeqf?8A2--#h9{uTYz#lMg z@ZqT4K|>L#oYF#Y=}s^62X8h|V$q4YELE%W0;l{|IAAjmtls@78na73(Cq%TgN#-6 zU2@?U8egR!9_`{n?Uqxg^!Hf+)H5CU%o8o2&z?J{KeunO-134~(9h~m!jIw4$x?0K z>U`>v>113OuK21lF`FYxrfQIbT5vU}*zbWUPi}*byfiBg1}OY2Rub-kqt-vS(HGXS zw=EA0`tTXO)X{GpJlUS)-TP{{HOq$O#e={q6&;7!yrS8M-6Mf^X^fFhg@hFfgIj>c z+dV)EjmFtOG<?;-TRdun$g#;$01N({7Cx8zp46kcj&X9Jb**34yyesYf7{1D(Cp=% zJ1LHw6GH-tf(%xU5UN;Syd`(!xWsII0+@izq=s7n;tSu|ZA@jF*;C_XoOEcTyZEw! z#$l>3n6WnQs!_6T+YPEJWowuAJhFUSHsg`dX023=HD;zA1AzV*%%+OiACeDUxo~SG ztMQi5Iz0bv{1Vmh-?SqY?I62`KCN3l*QF8;*Jx5dAd9S6v?EU)um?kpg&z)<%eH59 zo1c?xB(r^5%(m-QRUVe8DpK?_fUX;Beu7fHnAox42T@$p;rsRZ`6TnqsUX@K{zQ)W zVW(%Rk?B+F#V_BDksc72;F<vrl8^P9&*PPNV8<C2|5+A4?>8tRVVDx;nTvXd+?SVm zc5Gb0Q;RFPN<BJ(g``DxkdGn}d9Cr(b`tse02p9Ua1GUXGig~cn;h=OJTaF1Rh~u+ zsJ0!<IMB55GbPHp$n+WdTVA*MvFmw*S{UWUZ7zgLpQ+<6_2`;>wrft<`P|xYeE@M= z3to=jzqW^CN&;YvpsD$s)=3EJxs1e5lAfaCENFOh8yhnZS<vCbDgXJFE);Uds6;bB z{zt7(_+;4a#xN+xiqA~QAPpW?(3cE3g*H*9CJV+}R@bca(1(&+!}#b|))v0a$;K#h z6Q1j8SRK-K8$(FpIXZ2qi>JD2nC5pom|Fb0vrh}32Xb~7?E~G%`bHj+ePv#(P<Je* z%4fWczB_hdWf-2vaMaN$1HL=ZW;?g^l^)mzmdkh9F531QQ*yQ$n~V!R5!W$bZi=Jh zGLJBgD-1kbm#p-OG}!RVJwBEaJvhd-==5nV#|ph_`;@TjS%gJ@j%<FV)o-b~^|SQ& zJ+k>lyr_0(@!+$j;?>#H@<|{m_vlKllluFMpPF8%c<>tFL6M_d;h|E<9w-7FS!;t- zi8%u*9DV{_2B@MZxs)t=XfTv4{G!c5h`_lC=Dz!n>E;ivUaM=^$r}YIhsBpKJ->P4 zi5r{8zo^?}$Idb_C4$naIHfoeT{!0owmyY%ubc%~;n@l}SW*q#a`Doh&7<QebiL;Z z?e&l8OgXpoKHnGpeCdlkH?iJQTz$jJRN7JURubYaZ;YGmx7rp?x@EJpeDjuZ+E`FF zmiW^yo3!}22lfY@8D+0gHO}KNj&%@|h^2vYX^ZFaM`kl+tA{_#!k}bZ5YiV2-Rf8A zWiD~9i<_NnRwBDb$C^uRJI$k#tzVv+2a>n7;C15eH}urgi;DaFR<@>2(V3<573i6> z>jgh4>=9xO%QhxM1>BR|=}$Tq9`c$<ByW)~11esdw!9%EzA(gJ14B(Qe8n4BBZP^* z>Qnk#?49pA)qA}LOcgoe@80wG-lNZn%~yT(3Eu|#=}&U&tAzH8R|+w}gN8T7rb$_S ztVvYy(huTkGp)bPvzao9S(SlrJ+~1|?%CsWw*Oh(;Pb3*UiZ&^?F)RBiqZZi56KUh z(9}4n*ovl)$vbokKglgYT1{;^Cq9Y;CKy{()wx05JQGqVPxI_RvgFcZD|>Ss)t_L> z$JpE1ec|W6p#!q|DIaZfuX-HL6KltecEwt?t<l9`Hk%7)&u#uC{97%zbx5y3c}L_w zCkQ7xfu$x8WL<~N6<ZX`R@BlJ3#Lub423-oStdD*$mZc&2Cw90VHzxDw0M$}Jxbgm z$-szhx!Qx**`E0^ftiQjcwASf?Y-F9tMgeGevWzcEyp(B_OEeO8@VE;1SEQNA0876 z3&nICpeG)pOjhK?S78feS+$`ZyXq9k!WPH6@btk4i~gi;@ZqH1q-HSk@>@Pj%N7T} ztnR&a7u@f%?ARL0H3|;EBGC~hz+rOsC#K~5b1Y6$&4ci&mg%7nvKFr?^remk=2*$p zw%w{rx)?xKUmB5@taw;j_!+-*=XoxF;ZoB|T-2t8nd<jRe~%eecJ|ud)*ikMyiX3l z(-{!MiO{BF-SI(iXE^(4AmTwZc6<3QU2L(s=MHvr8KC0MQV)jli^eR+gcB_B;MYoV zI|GHo*kY1}9)0WqjXg{-jqZQo_~wzf^!1<}FF(2Q(g&`2@y##IKdQsG6UX%&H~pl= z-k!ppYdoL*OOM~!{N6{VmsPksY!r!t=#bS`FMJHXk~tuNjU6ecCpI%|lsegH+sLJ7 zKSf%{9$J~cp3<KF+BMy_YF_yn`9se(h_7rByA4|=Z&MBtbp*mA6G$bO^Fnh2kxkU5 z93aSEp4y74^?%T9*-Y6j)ARrk+;(QF_*NGm4Dpv%ae<>zUu+O=sZIW*V|@CDZiO;0 zc6c+6=MJ3Vgq;P$B&{vi>!$wFdap6JvAL~1eB1rUNfB)a*)=pB8MO$oB!ZhVswCCj zL_SC~tMt@NbJQM$;T2tbupC+Rs6!(RP2%^(Nk*(LszMvjU`k;9#wBhKv}AA7p?L~> z@~5A^xq0SkUA({pZRYP`DRPm`6|LzDdUW08OO?0izTx3xe!VhJwdriCE^jr_k|%;h zE3%N?TeF4L|8&da!=_YRE&Uv9j1~m-hO6rS*Pi~~`?mXAu@xpZQ`_L!urRPjTs2N? z;T0a4I2|Lni~3cd2cUF^P#|cVD?I`))#lq*aXk7{8*XRICPJ5u(!qG~r7c~@X%7|S zh*|C{tkNsG@oM09=nInEoYfI9*FZTI_{3y6JukYb?xq^_R99jDV)(aO-}Zt(dv@~+ z3g)7NYA?{Eq|pEZqX%d8cYI19Y424tFjZ#sOqaB9!IFrgsri=yoM|*^r$h!PgGtQ@ zWl(@a69HQfTpNIX^P{|0su-v~{bgNps&iWQgQ@7LM(&=#RD*Y{fB7kHf+>Br{3iR& z->NI)^v&Ps7FGn+Xv0%qY+OQoBxbNQnM5uSFjNfF#xm884KcLj=C__JZKD3jBf9*p z3r8;$=STqO$M%$gt|c|5a^JLsi`-(Y=s&ryHYe<SNZ$-sD7n};j+f5#_?Q8|?F^y} zL|gsD79gmiGrpNWfNL(mQU8p?xBe>@WY#uaJY>-+eZ1%&*w%y3Bg#_5cA@4|QB|F8 zbbaCc`OW(Zee2t072cwbg1xT2*Dv2fKwB2i>Ti^kbgH@$E_J#z^A>caV3Co$xul&M zSq)5~pl2EMBA*s_8+9KUz_!D{YP75auLiE3tA1`8v@4f&x7XOC3t0w+p3QsGRgY@$ zwHLdfz5ep73LoPC!EZjJzw7l-3_zn4U6TnW4jfw1)fBDXMNNch#}a)*w5d28Qgj;^ zeQLC#PF?34(AI+NUm3hmh9hE9mb<U95EqF%eC$M@hoGih%P&0Gm91eOyVVsmE$dX2 zY0Da5C1`YtS977|mXy!MGqa;~Oi2$}cpiVXT?DIsM!V(-uB;=%Zq=ZVgr2<IckeNK zNhGPU8=9JrZK>A6{dwt+-*&{hrE=Q~9;-V1__<i$FCYTrL!svIG#BB7pVS$oO(im| zb-ifCa`0KiX{Qb!OQDg4jtAOKs7EAR>1b3U%isWn2G6fkSA4Y`w~Q{S%-d8dH`+oF zK4dXH=w{nT$(RLvSv+%5Q8tdFhOd41$<5o}sfQxikg7ITm<A$^l3CiwUP{F;p4kf@ zhGB@%um1xVS!#)mzgLsAZjLGsH+*!mZ5N46YU7;62l;lawpIqbjs9|81NMb4U)Rx7 zU(eCDk_s$1AP75yp<>h6(?_)XZ*ax3)z6Vn17cd-))%Mfn1wIwWRH0@`ZV#$o9A2- zNJOGDR-E7RBdyTU2g|8VgNN6}rbX*;eyqXBZlzvk>8+p}qw^F0FF_=b)QA&M$8QWY zn0xiR^t#NtGrh(wmXX9up-AXlmca?11v(E}@xzOL^oA`>pkQ#}ffDh&_*^N+^W*6m zSAlsv)#C7zy*qtbCRtkqG<Q-q8F-@chY2e=Z+GSI|D4fB?U6_MyZ+p?$YO{ZZA$Z8 zX=8TDFr6Ta#&_{tn=Jjd(lM3VP!|FIU0=r)cvbjQWVx7#$s7rmszE=UkBE?9YmyhX zbe3oAZ8l%j@BNJ6&_~g*=B1rM7(Q?S7Ht*eu-F*eGB6lyeN0o}vSnTr!cJu}ysle+ z%pV)jMoOdKqCJ0v1zz-KbN_=ndXpDicX*?QPe~1J)2ekl#Lv>c-SVOqydFRFDS7^| z1JNe+h-{8<is-z5de}!e;TA-?w+I*6!dp|BL}LItu<*f079Q=;shxRYY5Si>ibcpZ z@P0T2hQwQL5Yog~fMu&>j+btTVDPD3sHg5F9^_00_LxuW5TEC}rJU9aEWYJi&+7T~ zS!^*DL*2%YO3mMP4#Vu0(WbPDCmXcjj<d+6Em=WtZocN-r~M^obusxBVk}0XGMOW{ z5jA4B!!ypgl8r@2Z(h;i{Cqy-@h@I2`%<hJu-tu)u?l>N7YICeUs}d3+7`R1O`9r= z=0V#WY<sOaV7;M`oU|MIu79N;yVb7wv#rhj2lw7vf9ev7!<lN(iGi9A7}Xy<bLPk` zJ*8Igt@5H4JQg(Z|33$z&8Q(%YN8cAcY>$0o;zPZTZIp5rfIjCJtVUXBxONw)7b6w z6i~<oo2e5X?a*bl25RBe(_DGKIrD)*wM?C5k*9d!bdTb_=g;JQiKC&auvfV#+jX?S zqM!e+=W*0u_0_sB{oBs!qMbTo$^>2dU-sAh8kcQZ@v2Xz-4|i>pSTNMZRoH4x-*+M zykR*XLq7bXO7bBC=s13i4W5lkdsuV2zzr<@Z~Z*!2GG+N^k^%Nn6Rf$StuVif(`_Z z!E{wLlGZ>5?Rvr6Vi{-|1|-*}#-#PtF7LFFl<i2}N-T}7zF`Y%`X{jas5Y{D^*HgP zS`*mFr?70RI5d&e<<a%uZ}DwcFM7et)Bl+u=9<70m^-1(s6}Lw!UMN1d-2woYGZ*i zYs_>>XL$`0e5c=lg$J_qk%k7uT)KMg`FawKB-ZxeN+98U*>HYSZ{uyztwA%`Rn&(h z0#h&}atzhJtiS7hh|gaTT~6uu`)~ZF(;H6e@eaBwD8~Txc8<zz$-RI;2R>*P-eM+V z&cS@cH=fbME|<m5Q3!v2#K_Vw@nCxs3w#+A&7VY+B?e_%EX(CPPpk#=nNRELLj~l8 zB9Jw^jXb_9j~O)?X&=TEuz2Q|JuuO5u8@_3q3@<Qh#-3Wd*E7B9&8(Vu*6|eY2r^G z@=Jd*ZnKR-FI#L;sQEPZJ@8&k$xOLz-E-%Td_WWN!;0YD1d+f(k4}W>no9_|=;S$G zc+g%`)0}DAu?M|fVtAXom6j<*+l#n{ZktzjW*#($?q1Noz@jkLmiu6@>z;N_;AOG% zo2oB2S7(f2piOW0CxdUR?l_?8S0;b@b3^A0w6oI_QMdz98phZ2-dOltm(RP{zVL<l zHaskM2Ep5ODSB?Mc%W-xK|xDJz_)$%DV;;m{qoc9fxp`?J;US`sAC>M$&uq<jPyh= zhp2!pOz~*L_X~C{ug6t?{_|J#2tR$vNXSDsT~ebU+V)!Hc&ek;!AF^6E0C)i4=Z9T z1|+L7b$CTv-fDKf><kKveRx&&${&&n6=da<rxWo467)JUV1tH7S<A)x;Q8}MZuyZ` zHZpBq^n$mI=QsaA6Zr>kD`0`y%Rlv`W{k?xu}U1gbaIwMQ)XFlYP;~EQWn~abfmN7 z#UqAg5W@q(&q00@OUr{xJ+SJ;<;&0MG0(O4JE1?ZuUrvRljY`E3~rWVL`9i!>}E7q z@|rl?`kWZMte@?4ft#*g<YZxt)UO0@f0wSyf5!<O20yoX^2r;UCmz49FCH1AFmXr! z;i=tUVl^2{ZkS~E>bdogYLVZoH@VIW9aB8Vj-!q$icw^(ndYfc9*`9;{Xr+Rb=nH? z0G%I`&1<!=!~OcTaBG}8`sE8?jE}%Ff=4jWuB(F%(st4YX#m(#vi3}OkkNC0%mWFq z3&D$Txmlq*7sS$XIQQSrmssgeR$^`6THm=py|3^V1nKTAKzAV*FW&qHO@^(D_sK=~ zy%aimb8a|1{`4ofQDlKYt%Heb!(LRC#W^d7mwJ)G;U^F6)fFQr=yov+uh5*8e_p>y z>p_p-c+RHSyFc{=6F|*$f?EGtwhRCOI{ZmQK~%+q-pPnKd?(H*fFZVP^ub|{yQ@xV zZ|_^yw_$EE4m?Z#s*XUeUgr7sx({F1q+i!j%X6xId9Q46FFQXC@e>gj{IKZOiyVXW z>#DwRuKQ73-4~pwB(Cka)5Fw_Q}n2f1>nB&rW~CB?H5n*V31$<$B+A2f83kTlX~z} zrJdzh8{gN(p;##tN@=m8#fk=ZDPFv|yB2qc;PS<ayE_zj3Bldn1BF76;4aC-?{9eK z<-C|RYtCI~*131@&;DsY-ZLVV^)1xmYjvO`s@pq${XbE51rL!6r$WnK1#Lv-{3`XI zT3a%3W0dw4Z=9FXBu^nXHS>D$$QWY1s)d|p&9mxHuV_@k%h}IQsab$uxU~||kXbB7 zufQ$d;oXs{hjaA(b@P!Z-z8Y^h@kBABeTZN0IHftRVyBUMDb5-hTq<Ts(O+g#( zm322KDDxvPZ{E|1#U_{QQgo!m)9%`MBfb9P%thR2bMat8bRN07@9g9rO5KUr(dCA` z4>lkD4PWK9=f$(mjtC}DTu9{ZXsU})PaC@I^@$KztM=pYQ?#r2bLQw(?(dfi0c6+b zutCb`KqEn@cgnDc>mvQJ)1c#d@(;5RiDw)sag057WSPt;jK1_zLeksc<tuf~(vY=4 zSTi#6coxfl*T|~Gtx*;t#_fc{K`T<;{a6P^N=I9FiP}OuWP?!KFQ-Jj;9FG!K_$ay zX6Og^<OeXGmxyc6%#%O)+|$%I1p+w%qw4GFul6nMPM*&7Z1M$ag$W)<e_g*GX5NM~ zdkoXu67`gi4Q|Vl>HoEgziz`RP;-lu@J!XH8tw{?W07o#2>7ZzS<DslKF<WHgt0%S z?)i4Q@ZeIP^>C48LQnKS>cGP$&Vi50Q)L<DTppPaALc=Mp|z(IDJ^jO-<)5Pr3Vr1 zG0xG5ILz|m-(>>sGS+*gbL@aCCS|X})t`%sv0-6#?VQSXGMLJYOMm49aR*mQG4dtN z^w1Y1aar9D7rMT6w7_Zu?cXdYg3i@&@flz_9o0Mm#6ErV>Ccl?UI~(>z6nQPUaGyW zy64My^5*+QL+~EYvRnSmOdX7k(~AyOm2>GW+HcdK{MV(~`DrcrOF~L<BFTd;!Gwdb zUTq%u-gk33q}6(>Q!st>$6#bDBYgdFPD9ia79ViXZwBzyz=}r;fzS2725&L3&*!Yf z{W;M~9_X|At%If`p3L07k2MVVZvig?xV}FveQ}9=5Kq8sa;_Tv<uNM{zml4;9s=*> zuINC)bd8~iDIrSDk7d!hgbHuD=)m`R17PC<!&iUI1Y94B;clxU?Vmxn4rT>1!oyiV zX^O$CR}lv2TxT>2Y0~s~rVAEC))J<1a7hB1D=$G@qQBPBc|4BI+ayeDoT9D|NBgr8 z6e4rA9_<=f>uqUW5y10a>PL89O_y5#1Jzhmcq?3}*mi(eXqQY&#m@FZxt-M&f9ahb zY*2}|;)9(PrhRZ#->T&l0~?++o{%pS-Ed?b`-AD5_!Dx?RQMJBScK^H1lumS*7vL9 z`+(j9HaVGFEfeD<i&gnKW)w~Wa(iW{c-v&>_KMoEGQg-|oN#6kOTR{La?|e-YA5J< z@-XsUhJQ+m)Dv|SW2<U;VI6NK`{{6E(dNvrbk~BE8QgyL_9%H82-jutSQV(0@Fwtm zE^+jd^_qtcgjz%`NBa(JPq-(piH}=mG2bRQj`tpMzWM1iSqlK|bU5{;BL0iB_kNw> zcYV%S6G|31H%YXmYeXrwu2cyHTMRFQher|)cz|ZtqqF^g;>$v<k8TdK14Kop#_^MX zn3w<juZDojB9*Yrix!J9aZuCQ(tG`LbhLEM70u@XKcUD1z4>}sYkrU8JZhd!a|H#O zc>n!Goa(gk-6=>FzNve4!U2|M25(J4H0doy(L@JkczMJYB>~EnI7%!I+d<2FY;L>( zn;550PPaP*_ynbB`vY|El`a3hi*X)2q5Nfj@+k$$Set065hvW+S^8xy5dB|bfbL!x zVc5a5#HPA9<x<`1O3nFKi98XNu?|`8lbKl6<y2=n))2KSlWv>QR~vgiZej;E(w`Q4 zN__b&-RIsnLmul|lg-%rhB!hLZ=r1BGP$}3Du$OkLJp^|{1Jcn2;lH68bV8d9N)<z za`V_9cM-&E4Nxw;V@au_WJI<TMAT4ock&K0?M>81sGR?e@ZpSp&t)uTb*YWiX|RCm zknOb{na5l7fdK}*$WlIRed?QOemXTG5WZ!T%KgT({(?Il;wZKX*lWnSS^ti?ZaJvZ z5;a+>Yne}E=(p)aA&J7UuCV3cW|tb5?tAe@>{R&<6J&zvYpDR2JIE~(x)z)J_;fIB zgjl4<vQHW#hP=I$H4NOz?Jn7@l+b+pT-Ef-?;%+Ce*Od4NC)zeRz<%T9LkOTg|AQi z&8Q9wYCJA?%#~M0b<>+~Mt+1UEs>XD)v-b`Gum;7;#=`Aj@c{trwH6<ID5q)iTZwO zk(}Gp;I;6^$rCfd(NoC<8f{K>va%pWLP9-P6RO8$WY~%fGD(?YJX-JfR=lUMQ=Ug| z<aT(L%%?^19{3aF&U~}~y}lQ~hczh8e_!72dmquC`;dZo+8ULF7gN1WH=c#Ks^Bjt zE%xj!QrL+1E)>OvXn^Y?XXqDthDSzf-hwY`sx9}uNQf$Fzy*b{hikv=B+mh0%g7hp zP4lG8#GEap6CpK}Z1ShWjkoSZJT}H>1}F3P#ahYXY&dMCWKrR6a%iAG9EGjU+FSz1 zh?XyV{p<)!#V*&<4P0Z-hN=yu%<q3o*4{7B@KE*rRI=R+%MiEidHo$U8b^D8l+Eo@ z60V?j+z(*IJz5+{{@cj11{rb~&BfPwuUt6P!w{P|>1Ha{cXKLa9gOuWoZ(Hnn3>bK zrh6fB<zHR_Gsfn5eA3S7aG+GY`D3$@_Z)psUHIWsbD8Mofo(Xe$4xJ4?uJ!ld(#KR zZ;kM<cUJ6dH?^iwQ|S{tlr`}6#XVe#?Lvud`Aj6~#16X;@8|wATKEtR?J~{E+u9$T z0$_tLs*IMrL%*QmXKNXg&98c21}n;0PtY03qhlwGhnZ#9TlDIE+VQSZ97bDGc(l>6 zZ*{ZQHsY|RT#DyjY?5!b*?ExJ@%!v5{9!q^nYgHBQSX^w*loW|_c^gjux0y3(MJ&( z4!{&4b8(=_uc-lW&-QmJ1IMp6F1#Cj=SJV=uzuk#m<j4!dDm>)ioc&%?e!3XyMv}2 zW34Ty<RiY_j}|cQx3XL#-Weqva!+u;QZqnT3ctc>F<K@*@Uha*5b}ga)^OPs5m{4E zi8VKw_N#{$ggMucM91}ht84>80}m+N2(8y60u2+XB(anEtKPOkBSm?!zmYxD;97|C z%%mwO;Otz5MKF3l+!*=G-C;r}7<P>HG?nt^aC++9B^CRNjK_bnOY{Aur#Bu`J<Li~ zt?HaEEjj6M{Ry`(zuuWAPN7wvb3z)d{f+s<o`mPEJuMGBY)|avP#~JdDZ{&TJ^AJD zQ)yiG%~Dx7c(1baDiO94=y2}!!xcXNEa}-9p6B<sgM3R{bWmNCX_<UxRQn@WJ!9*W zTd!8?4P;))6dRSos!784I~pl0P(ucohm<?Nn!=QJC==Kfv%9iy^s-Db!+kz}#{_nD z9rDwk_(+&E?YC(%db;1%gWD^ve59&6&e?#yKxhF2r9m|}v$0X*nf{~qN0+>D5<UFX z(9%n>81#A)eA^u`BtBK}TnRt9n73Q|VCgmbPgZGZu+63;;UoNngDQxj`Ef3XUBYO< zeCnj_oh6eaThj2t{x35r9ap4D(q2k+m77u-B?t0=w&@K5Pl&zgFPN2^@AW$QYiG;# z^RgP_KsgHp`JM@1h6>yR?K9*&Dn{S;)pV-uUkqrmen#Wz{uLCg@iBz9-S}IG*5oX@ z;#T|baCvzQr2*^e*p+(hW-!MpCH7uOk6mC`rG4{*UUz`p8_h?pnvIpNyEJl$&;>1Q zw1e&8p)(ivpp?X>g1*gOOSI1PPR~2tjb?)Utv-vog1cjmWb+TX7ti;sEweJqS=(Q- z17e(^d?&8#KRY<^gn03C{zXdXdDn{;0PcXLtuu!6kduE+9YBSs^9=95O*w0PCj(?? zNIM#tEY5t@c|Ynj2~^}VZtpEwVC)E?4|B1?i_tlJ?`7<rYSgnP&ra{|owlN+!)t34 zt1>#I)PoRvIr=B{wiuo^MOlnnbb1sjfahbz92JCgnYdb~0ya)idoc&c$DAl4asj2E z;~TlwYbV0|$jS86J+4sz?vH6nwc#-3OFTC(&K~5ENG}Bk<;a<ai8iBI>R;ZqK7&T@ zj4Axb4<|dpJK5k3#_}~6+AqAt3q^%vx4(-Vp2&FXY0$qnY0>^}=b38hV=2)QwiI4o zKTG_xtwTDVAYO}wX(5|)V49~^*6MgFmIPiFeeRsLj&av9{&M3P?RGjF3-6j6ld>c& zcuwm&`slH&haT@&WSsNWB))Dw7Oj57Y1{i1FD+K3?YJ-2UyTSibCsbWn*CN+8!-F} zq8`RFCNRVu(%m%9^qA<E78fI8l<8^dD|2@qTHM(tQdG1C-i<5woKjgnjby9H2~{GO z-CJ69Jy_XHs5<rL&eZZtOa;8b@(B@!vtH}zG(&q;w$<2(*PJtcsn~y&Veg6jH5~X< z6nlX+!(J5yn=ey(+)KK{d>;fqF-zkGUt=jlr_+vk2(R@0E_*@*lAe$NKGvtEKDLH5 zF>(%J|GavY1++7=pc$=1P>-G9k|*<N7@NuDW0PP%4?VH66Ms?T+1*xa=Sn-z4hZ~A zYg87P8IQxpAPnWSdcX{sGoooUa>cUkTFaHd4^K~EVW;beqBl{0mSUL|dK+W3w=mT| zfMQGGa(WM|jpvg=BGQof9^fiEjc>m|V>lf1sov&kj_&3}|47uijL=s`+TCirM4p#I zqbxv~Y<mg@E1c>uTlnPjCHDux3BB7WOInoxd&*8i@%QDUl^ucmFB|uT4e1u&gIl0s zyv|5+T&LA!0Y|C0GEKdV*2-D$%&sO%m>N{rnddR<axco36<&fIg=!n`ZfLbO)?AYN z3?aZ&r}_V$yMNo-BIh@q%~c5hyvEA6k5P1I3I0YHI-vCoMHTvUAG&aXFYr_&c(Q*m zgVqZ<0$;5Y@+ylsPqt%2nkROWwA8I)g!bR&$X_Z8CB75<%9PqRC?8A0RYLVoZQ|T& zHMfau@rO6Fu!lGgMUG0Jd@Pdgmbb+8a`#gVFz$=jIE&#Mk|3VSHz1nUvxPiyqm4Nb zN-RIFXU~SIc(kDvAullQVr?vf@^Cje_nXvpGJNT!P-z?44L(D4VKZ^_)!caj_DB1j zWrKhpM>2PPS~lmGKlAgZ^db!l6W7Bqz8@GNA-L7So8xJ<hfXcHXZA=zBC+1vK}ngU z2iJE(Ru8jltngPF+_u=4o1Pyh@2Hv2(+f+J-^<>fTeS1c_6C};F9eR^wd>Z^Pxmby z@A(_FO^fC?G73%J?NW4hJ!HpOR}So|x)M!S*Xm~~<VuUDLQ_TL^nXnWSvS6$VV47U z+YRO^P!$!&7qPau*Z*`aO;7SES}T52?%nXOFDNYj&11Zfk1FpdbmBR(Q(^LfdCEo) z84(%K@$9ITC^)AeavE8`8hM%m)K1zl>%c5B6p|(QhH;&asv>f8!6Z;|m5q_`s^pv& zyYi$`j<2@Bb|J=Am|SsR-^M)i_#5rC8E(Ys$S!jC-kY?-@}mp*d9kXCO0#!tt0A2$ z2#FWIFk18)aXIOovnX8OY1QLE=%8h><C=s8ygK04Oo$h#SM%)U#eu07)6+DxE0f`l zPz!urlnTxAZ5r{`6vE0rID%=k?d%^$Puq(;)XV$KF}E&{{)Raenbfa%8+`WdOF&9+ zXhyKOdl{I1WTtiPFYT~b=;RGoOxvS`rIX*%IFT-}(+HTbw11SFYQ67suI^oiq7x6} z@0`#GnJi1~vs!m_sc9_<x2JAKc|3a2+rd_54B-(2&u2+ONCmrm{zeBO7(C}fjU*HQ z*4H0}`-Emwl3dA?Zw(;}4?~t>6>q^|>@D428-|sQU4@9*uJ(Hd-_~q$aGngqKJ#={ z`KV9cSbgRiOU6tC1Q7-F_m}ZF`X3)oqI4LCR5jr+lA@je8;%;QT;znpe>2@-NT{9H z`$<~4#vLCxvcBx~5^Xg0<@!NS@R1~(FAfiQ@(>#u2>&TE&yprSVt$7&T|Sx9c-bCo z@{fhfW=i-m$Od6)sB)?5IM70lg>{f{%et&40UMcK#cYUDy)XCI)<1%QCk94Eyy!U< zj)SY@k9q<&XCv`;aCz7!f@CGFXyT=9_qO>y&Y->NA-`q?fs$g;u6=ip))lbRxXeSC zYZ+vIo|OBze(Md}QT-}QCgSepN6hAobK+*29UEzN^VyLr0^ix^Jc4?B3<*4M1oAMz zQena?hcAlsO$vLHUW#5q8Nh!MqnaS(9`$vK7uf!%aqDq1L}f(+<0xE5!TI)^Al=Xa zS>MZPe|RPC>TzEojWHlLF(GxnV>2a>)u^1tpR?2pRwV9`+1pdyeNB{LQ%}-D8$vAN zJ{FVLEyB#Y%AwS_MRd&LhCms9{oGko9q3$N&>PdPmNagV4K(iT*Z2>sp8hdl9{;On zt<qxMdN$*YcYr5I8!H<Dl5uNdp7!Wj<MS$s+-@oLagBJpnODa{jg_~KJLtuMI^R;! zK$#yTq<_1s|2D=q`nczT9VW}NwM5t_lK$>LJ<aCUWrsVu<!4<W2W9FL@(K=CB241( zjo0^99T$o|Vo>qbUWIQ{<k!t=(6+lgGxD0gX;Eq9C;-Km#p`ikP+seLLSzf6U!KaZ zwgWZA)pYQ!cdb4^Y&`G!aNOCzW5N&=5TA$QBuVsB2fC8$y+<_iwK0)*Fxn~XFhE!M zciWMVJY6)b(hlGt0Nm8NP$`>{fp6>>dgclnqr9~gZPK#%nACkX)69IPCw6L`6aYGr z0s{~R{101tE?gE!Li9lLr(_(M)D<t2RW-l5MW9;vSh~~Ll_c&4dDlp}Ss7IKIrR=Y zeaTL(%d_?^l3t5@KhD)6|2^Jr<FW&EIjoz~%45bYPNqwp-a8Ik_393>dNq7Kzke~Q zLxz^UjM<a@V$}=Oc@=y9z28R~{JrVj`|401zVW;d!&-L71;y2MF!SfUiTb^k!wTqa z;1w;Aw{SI$3#$iaz<5~rNSf?KLDrTmZliDBb(;m{ys_1!c(zpDnKp=Wvkj2sg@_DX z;0TvZMO;Rq{e<%WF6feaW+58`LI@5ltVv0S$eb!Mn7d?+d?+-#&jd&GhXzoX*^phr z)(Ee8zn<w%`<aNhD^NRpEH!VAuzSnwzdxlb*j9rDihd7qj~V%HS4tj&^&vaw&mn;V z$JDlHRV(|}%qs7lEz!V?$Liyo$np!F=Q_xB>3o*sgVrhc*NE$4W_}m)#W*JNP;WzU zo*Jv)!_dB#`!23`lm0fdG0Muxh;gcjc1`B)M;&sJcABC+O3$Rm)JfQ!@p0QMT^*kP zO`ks}g>iIHm?l|O379EbXI)&&LY1PrIg1zkFtESFyQpZaIaVsJ*YO4J@luR)y;Ti* zN^4p5>~<dpbgeQ;`n$e#Pu;>x5@6<JPS!U5Liq+(PVE=l3%|$!6>J%)qgwOeTM}XZ zSjM6)^!S>aarA~0`+WdLmGYP4iPsoS<$8bnj|b_%1d@F8b1^ex8~L71lQN~;nXR4V z+oop)_xQGIq*UeBLyKoZtq~lg)foxC&sjeOYdCk9Aa15_B4P}JU`4LK5Q)tz948l+ zR1TDX|7(A3=kaMnzq(BME_T_JDte2VTcW`uY|YnTXKE!KyQCKKt2uT^N6A^s9?CN7 zpSdquuPkEgqcPwyzdkFONQd`*tb4!m4I#yin6n!MeLdYhdg)3Kc%Gtt(&i^cAQabx z@o;=}kCERxv=sA8LX#oI0J1bj^}N0nFcz=uQOT{#4MhvD!IX6=s2YlSAm1dRK}Ryo z!cY}hU?L$lOLo>>!<UgG{~5{{65g|oO2#E*&WaV6SFQxT<|8b0ayc&UM)eCG=P4Z& zTD?Y1u4i^zOEwW{BFaG@PX9n!%fq?#_=>zIInDHd&-9iO@ZaIojZ6zzf<&V5-@Vn? zf6fvs#zFP&QjL$q{t~^m5@(EAtYFe>+4L*pMzX(k@%#b_hI@r!;a{#!-H5~0MW8!e zuKC&i5BmmH`)b!smCG;l(ZG;;hxL~BTSD-*`WA5id7nPo@1eDJC3u+<qV59cywL#{ zRhT{e4jnru3ZyAkHW$R&(7w{>U|ps67t-Z+Ru~JMT%r5&*&?eKz#pO`VXam8p2a*~ zA!1ME=yxHrJ{=MxG4B_ZPT@?pP3&C~J2e~^j=)+Ig`+0DyK?fEL43VcwT5)<=iz%X zzqyRm!d-bm?t0a=y%DLl{c&)A=3gg0%13=HKznkrnNR_fBWYHgTw6|xR6r;-{@$m> z620(fRuwze1X}T<j8un;XmH`U6tSetUl-w<T=w{Y`4${y)cZ(wbAOaE=8pHxImSCE zyI7CaK1}ft3*3<DpW3qtj?2@d19y!)p#f53OfB1ov4&1PW<KyK&D7=BeYzjz%7(Qc zN1AUP{jOx+E1-T`CMV0v{qZg9y-^6{=W!FZ0RC@*rTirCtLkj_WF;MPuP5^m<dinn z0OkCT8l>pI=I<Le>@%DdoW)~-FJKw4R^Gy!-8#f``+NsvyNQrEIxZ|c+tlCYW&CNK znF!t!S>*A!W{_>#fZXx(VV*qO>htz-GJJ!t3M~M>LF|w0>}mFr@sDrR0C<jRuxoBr ziVqPY*pZ5WR=MSGHpGvRx)Hd>gPrO%2-bjiAXA3p>MaMkysq;otRp3`&g(juiGUR2 z7iT8vmArVz6d;#|m%6bC>H`p2gMJn*!ltz`oL$hga?x7v%i)`x+ei={an3g!NKQPB z73~2dS8m*84D0PV5`!)G46_6Dx`eQti1;KC#hO|D_zQ~g0#m*?p({>F|6rp391d<D zWR*RfGQ8Z%Cv|5?z@&0k8~B-fJz`e>v9$1|o4p76G?_L1V4CH;oO4fV()j~uYc#hX z)f&LcL?qh(;OG=^E+gYj>^y?`z-qYo3rj<+Ts-U}v71|VIXZI}3BHrYT8|SpA-R~E zj1v41v_@uCV53h{H}V!UyXbsi0P&L$;x=AkT?VixZ-HRlNh{4Y+b*a6;rSe6R%hM| z)CEt#&)>Z?@EXku3xReL#T080KlY|xgR-$dUW!Y(IPmYaT3K~okYmT=grKS=&3f2~ z+pV&Sz1K7=7vk^a(fbmgmMo*)Vla2Uv_A%9sv==jja1V4&R1SIUXbCJp^n6)FCS53 zOltVZ$Y1SX*gX<_FrL(Bx=;0K3OQyV2=G*O{1;D!)ySs?ca|*ZN*sL-p#BZV|38S! z;7-RwbH0;y%$kFaHi3j^`Eyecsy6H0>R9g6tjHRnYhQ_=;fXM{_Cg8gaD2~^YGWx6 zz0$@mq1`F5f}iU2HP3yx1M@RE`9=3+z=K}xeQ=q4nKH!#{n)xu27Vvpn41#AO;>jB z?7{QsQi}X3OH8WumH@iE6&%FGZ@-6SnGjTq1{;lP35;2cEL*39p=7{=3X)sQ7YBSV zge3?>PK*iGq3b^|^)QSTpH0*U#sagDn8F9pWBgnUeE*wz^W?Jnnh+v<n7hbQo$bdV z51aN+P2;=mqy5F$ltb8zS$|2d6J?_Apc-<lMDkIv=IWoy7&W2&pT{6<<z&8$!&Dub zOv|UYTOKR`|L8BfD+8>U7fn9v$d-nk#qU!+&q2EeWquFrg?J?5B1>l^J}<~u&-)Qm zT@!>WBjdSfceQdVeeuvi3{k)V0)06keqa)lx`98ciT+`c>2(-*#M_h`N83aBCRe1E z6e~N{Bg;Vj_M<LZKwZU7#8z+0Tp_Mz9s|t>J)yM(+>VVjBs$vg+aj$?3o$t=oN|UE z3f`k^zK7?1+~Zl@O#@|P@`^$Co9i5=J}(rQpWg9qhx~<6z&0_N*dlM>%SdnP6F)HV zeq6b0jQ=!RIt}5P%Ov#2{wl|IaIeN#)J*e-{VXvVSL@nNne?a3%tTafmM7@}(v$MH zdKZL_+mcS72Hh{quKOnO-gUS}k=dkZ;5P%y$E^?s59uJn`_C)gjJ{s<;$%w}{~$$! z@Rbp{AzF9($xGox=OZr9H0OPh@3bpP)jiyfW7<?r_TH29be6~3QuxRvr4>R^SrG%b zEQGP7TMIaw%%isTsOJ1~wo+@!t4MD5cwE!+)R(elf724Z^xr+r#zRv$44~**3ptDO z4&Lc>xXIasf@kTkR-bF`wx#lJ4<u>~P+dyf2rNmmG8vc@*BdRYxVkc!N+$6MGIJ)f znD&D=J7mT>H1{ZOo})1e3w--iz;v;V7k3p}`pDbZX{0ZD?EzZ_tt1pRE|+bj2tNnH zbQjG8E+69;Wg787bN@8-&fU~`??r1GFCaysP~t(;^Mc}VC7bdGN_m#YllCHbdyR`W z7GEGq70Jokc>Xx58VfadM!rU7eB49d%KM}RQ(anlL6cC&p3nk=H6BN2SpQfHS^B!Y z^^+0%_>Kp}*^nygM_nD;&&Qd(=X=g9ApDrnjl~Zk1}Q!GuOBgeC|v@)9gjcOnDc>Y z*2e8ch^WK0vHgM;3nNMO+OVgzn;FY0Edbifn>f>t<jkH;^)uy~IZ7-7UMF#xBsqrn zmMH-bDb~<(`s%qzIqEI~^&dAv-L24YB32!bUm%t>pTEY~nLef5!zl-6vw51zKQ1d% zj*hGLyGf7d*1?oVG8=LY`^8fild{?O`T<WXW!FMNqqeG_eq$yDP5$^Ky*mq<rWxCN zLm?OvQ5#pi7=D*Y#NCb0e2zvx*6~@F+q`k|)GWU@P?hw4;4Sg)yGY=){sQE0pSao- z#MiGs$5Ti;=8975njSAcGT=E{$mH!EPrfKx+>_P!>kZ&~wCfHd{|6kATbY{BR!B|t zDi7npC4Thrvu}?j1#$<)dI#;AT+erW{XzD6$*I!MORDB1$NL%IPKq3b+r59bGIRzy zW~>)fXlPF2)h`76)XIii7Z`%<BOZ>>0<GTP<ADyIDH!#DraAS5KaA0Y-}jsWcXW(g z^O~&Yh*y#t(FKdTw5Msb=|+WgeEy~l!`~#tYK}?DOzbrq&DO{D5e#_@%!5NIoYrdA zdlYJ&d1&=lKe}+7Pgrca4ZIwFvTKhVVRn2P3GZeryD@$jV3y}ypw?Qiog8EVAfLzv z-hs`&VCk<}CJeJu1F5YZ1@Nq=D0FwwC|;^jW0YEV(P$XEDi1Dd%@@-_D$66_gmKHX z53PSrxGWp-wl<zmR<E*vGXCEmD^s;@ev1gx<E`~R6Vu)_?kkQezWxgZI?O1G`yH>J zd33MneQ!B5_2}MdTL03myp&Wmz{jWxBX!SA46KMkkNLIvICAaGaImhKNe>IQ)?4cz zXEXL(YjY?nE!m~Z8o_I-dn{>@M~=HagVvn@(CI+T#duk!!fW7*2rV94EXOFa${2oN zDU5kLC@jd~Upve1{=K1Hjt<UyESbk4^*4oSJ~V9XF&*N>$g!664)^ha>G|<?<a5-O zTMN7cG0CH#!O$H)CHIPYmY)p%sqNK#A*>HoO&}%$A^(GYWHQZw)<mzxs0~1Y?7GOM z%$Ca-78so+|HNNNT{xfnK`4qVA)SxAz{a$Ig7{tY%<LJ`K%Kh{UTS3e2ML_`J}yUC zwa;W@UCP%GLO}~fz0e0c{_u)2$fY`Q(>28ZSB&>}^|^!d=UdXrcdb~eeEe(CG6eS# zF`2=N2lLrevRBwh^L%wEfDRYo;4d6pHZf~&v`;)02c)PSUoH!I$jV|+BMtqqRj$}Z zr$P?qh^63#vrUT4?dfQ!OwG`$%z4O-+$Pa(0BN1GABM4^N<De>%^8CUqSYnsxWDBk z>4Ans7OJ6~J2Nex_o5)o&`bc>{s!=ouoR@Fh&C81P!lR(PF~NTaLr^;kdvdU{{mW# zZl%?twrq@~8zOyKaQww4p_1J}n5pMbYxpl=Wo>%m(nAQaU?yMzJmPZXtJd-|b2Z%4 znJJeB7$q0kcQ)T|rZK3Tg1I(Zql_+=gZ&vEvIN^aYdrD46TzpPHn;;EGvJo-=<X7! z`u*Ampr3Z_N^sGcl19XIpU(|L{Ad-$kO!*nHBj^F3drr5GNa)-u>(Ti>kIpmrF<O4 zKP?Kjw#a3ts<a#bPxwqRbo|fH_n99zbS<ks`cQpxo90*93Y!Pt>`4-c+M8B<{qU`A zp&t+GCQpMq+f_PjbKzC3n{HZ)XelcSI+WaGi^<WDMc*H!_(32FyOc1;GN`FU8AW`u z^w(izy(@Di_sOCD3#7^z9^8ukn6fsp6{^LZM(N6vTZR$OAz@6neKgd;zd&=Z+`NL= z?`sn*$9Xz^YZ)gV4UgcK24;9?+*m#!V;aTdIyOGspZ%JBD&#C?Sai2jvw35aSt+bs z!_H1y&aKL0<Dz9ZOW{k;zGk<MHk?}>jg2O#ofPLy^jqA`6qmSb7Ka_>{R(%_Zj<)) zM!F=pEg;DEtOE4ABrz;g09pMRCaJNu<f=d$o%<A#$7AeA`<pcxo{VvjZFq<Xc(6he z2+nz?!k=$x<uLCS#v$hU=*gK9GPw^s69Tz%DojQUx*eus9G|jngn`So>|Rn<y0ebV zp$`B38gM!MJ4rF`|50>XWeYIp-q#zpoZ9yFY?lJWv9)Yy_dQw1BrOdY!_wUDOD1*D z!ipYr0$fh1xOz)N_QJF!zUx>;v%q5M*=ljiy(_M!_Hn`kNuH!fK|rTxkZ$N(pUxOh z%?9P9_}!bNP3Du8C_^Vb0qUSjVu4rLrc69ZZS!dv4l3BnV3e=aXk+VPiu2J`8aVcr z5C&5JT2%Vy;@$3WvlYUX7WC8-^>i8?W4_hpJzXLnz<cXS+k=81mn(9pZEdK3UmtI{ z#JRWqDUH7mRj0M}VT7=jR__>aRZnA4Z)NmrS%K6|@u%Ie@9b5o)J~Y`gn}pHqh|8l zsX)EtiBF$-J*`O*S=ee%zD18eRX7X~*rYZ*p)Vs%#BAD)q>xBFT<U@QUDlewVJYJ( zO=x|lse(GJ!LW&C#p>F7m3n!|RbnES_JzFn?dRMd5nT&svlfkr0=whT(M=sj6i+L= zI5`w|@%RonvBf!rIlN_*BWC95h9;yabC>~ml%&Oz*4FG1lSwegb#YP@OF%IdGn<@J ztB_y9O1i#M#G}=1mGhT{CpItem{rlh$=uFoQUjn%#V2T?cIq3fB<D*E4YS&8Imo}* zzV~&O+m~W`-k3hA-)%9iKzbfr<maKku55Aa%e_BOB~6<Kd0&cWztwuoLBiHSN8REV zzP_^e2^@4b2$k&e{d}TfBu9e3z>2kf(7f_cZJ=z}<r{tFdKa#hAPm2syed<6gh%N@ zTJ~DaiDNdO&sZX!p7Ow@@mo8mD(lVZm|FAn_?`Q5ha-`68Z4;$30+n8@=|f_DWkfx zfl|weVaZH(792n0KJD?Hv-7!<ZzVY-0nqkvfY~XyTC-CVPdsQ-3%c2G0@C>|sbK+t zXUY&=iMuQ90gMRjD_!iZO)CIjO+60le`xM+@bpOB4e{nI^sBpxO;#FPOj$v%Eh!!x zp(JIZx99ybtThvx?vqn<287E>-+kN{zi^i#)Prl~=RCi&;Sd!-T{zCvD8MILJoZdz zv-l8dkl=>lnGVQBpqoZ6|2tQSMdgCvx2)Vd)!beAk>K~8Ujx<Bo8HR#&!sUF?RIQ# z$(0XTn=uLSglncXsYMGDh{z?sB5He6jTas0Z58S)Y;H>O;giuD>2lKle#Q)fP`>$H zFS!#Nuk&%bq3u{+X*eEKh}~2<UM8@{p@!A-HCN|MPEN|8z7zq5qbWT7Hj&Xcy(d2r zfRw%=Vm_V-3kPws5+{KKKI#Yv?D7vFuIX!zG-O}&?@fM*#fMFNB3!rKh#OPAu<Ur# zU=$>8#6DC!kk20a=ZCYTo=nX5@k%PeES-JO_KdEW*{54OuckqTvfvzNd`DYkPo(_< zzy>oeXraI0G2SJjDBuYz)?@x(clQVMTGz0x80B>VV^VpYbHd0Q#uZnej+4g^<mR4e zY9TdmnTB*6W*Q#ezx=(RH)4v;sbk`l$2j%*um7pBj2B@H1|Mpfb#n$D<0J+G?8yFx z@EMbgMBy2Ji;XpFl3FhQq+~kJ#Baz!mA1A~APS^fp)>s~q{@MJKoTI+6e6*2ih{$O zErmH1fL^Mx9kzkxj~ZOVR)n}kaqZ;u^`Ho8`-6t#0uZ3mQDW6d?fMtpd@=~Ul=<oC zBN=h-E5s|}G5*&>31*I%6rH;wg)i9dumzloKk{Fl`skwgi(8!`aM2zOj*;~+ra!$! z*MYhT35Hr-yQM4ptJXHpF{EYktY>mZnS#vMqltBtg_5pl7Cxo+-;dpenk6uduU$vV z!x9N8{jwZ%s_e8hZBn6sMrSXG$?R;*4HYgASzCfKaP5f)99DJyko8Q&YkF1e_$I8q zWP|_u231kJOPZZ)Uk`90KzRTZoE>kPwTyrj^A0)1RT6w6rk={f8myan0kONX&&LM- z!EvX5<R3x%kQ@8=P~kLyBRmuR1fwN*(!99zkIbJoUx(K<v0<jgt1R401=n@vAtkot zGFUVZPQmHQAZYz}Q^Kxnj(LPgZ^O}RiOFFJ=UpU9?U1MAiv{6akR|Mve{21keP05; zHg~4PoGOHVk3K+?N64_nYDkR+zfPQ9Bc1IUDWi)!Ag)U*<Zb@iKgJQ|8$$Vnf+Cj$ zhK3AGQTF4H4k+3kkz8EdQkKByiHu1gRh}nI+41w?*1DcT|JIl%3a{24ZE6m3BBc;p z!7!4N&|b2Yn|QqMvG<vuqGsFd2<p|gFD|<|2nlpAMLp5Sa&zgBz)bgWW1->!ldV$) zysY}VZI{qAjEu?OeYQkWc?-Cih}atMu?yL@{ApgdS!G#X3C9Q|mAsv+3R@>0B}_e5 zTJi}JykH{k{??WE!s5e^8vi?&T1uMcfAEP+r<U=Uv<Gl`w9V-l6SSph3*2lZdbiw$ zMcaJ~-`}VIL<0F|_7l^coNWzbw5SI}XY}~D5so?SSP2^qjcw3Xx91MCNYB|gnN0$v zb6?N`vV;0OxJ>D4E@<m-Eb5t`y|n3`bxT{|C6F)xx!YJnIpcq*Y{&K&AB~TQ3JCzw zUVJSD=*rS}LyC_{n?cRdATZ4u2zx=zw|yK&1=4w~X0+|mE3<%1;F7y&_=bN|`TE6k zxB239!QPH2p*FIRcLC0@Nqy<LSfEBMDL^2AvZglU>J4}hAm+VpS^J+17@*B;OL;4| z{<nhQ(XN8P;&!k3NLc2*i{0Aw-hW<RqdPuvq=qX`4sP=1og9MBNNQcOa|3IAQX&`} z{Hv=~osPP7eh-rZ4G8gQ9{@2`n6kYuhws^o3~zk5hSmg`5peZ=q;DgR|GV|e8)+|L z>0ehM-4oA61IJ@N$7YZmqPlW3r|s45_(0Zy`B(VAQ<kW64-7t^<Hx`Aygc##kD08K zlhhs=jIJ5N)dXIa7R%$gq5_%8x-gxYaYOPa=0p;@qQ--}zj$TGEE!E39$xFdcizDm z5{gk1u>_}sSutM(b|aV}hHrO~);490s$w^yy&lC!3Z-djyAqf%+qhEKb*3Rzjjq1Q zr$cY4UJAOyE_HHe^Pg5BzE~^EsOIGVElc)bpjAjA(1lHsIq!j_Ct@gFxgP-LMj^Ea zVBt>h=eybmm1$#n;B8_uLG5zo^1qL<*Qk2cI?W~2LH0^+rs9Ua)0cwxQAG=rI6Op0 zq(jqPr-3?$QYPy%9U&sOMMC&^C%=%$ZaY3sUAAzV*H^7|wdkyV+j;hUW`C9Y?C_F9 zy2K*Rx_;S)a;^r&$RB~;`SkwT6D>RNh97nw?tHGgH&k~83xU3wc}6GFJ>k9B4TrEJ z8U(!T$Y*9+1(0w1u6SRPe5~oP5FNFv47Te@ylPQ@LJ0~vPX}S4_T%XX3cYL5#KvU1 zPvbq2EZHOvh1OJ$bj><K>D>1pXpf5m(M>y=-fwQL)O@%#Dh!g*B#Yd6(_|2%88=|K z8*azZ1zWAg_jwrkavjqiK`yZGxixqfpqpfS40&S+UUYJCxGEak39!72JZbLzN?}wV zCgCT%uzmK+0n~f|R9u*NbMs6<*VFU6oOqfXA^Q?~cYh`-NOnYMqf?i`nWRPuD*mYD zkGw$m=32WB#I|(1L9VGSnl$wv_{Jje7*jqeF+f9p9mi8rG|bU%Z6O(%x#KUs>mYBc z;<3Fya-<dv{-*EK3nAh)TUr-7%!@1H_1tm`eZY~+m5~u-WfVW8n~ptDnKNRKI6qp` zJZy5j=q@DDy~aKKfB-HB!es?gdpB!$t1zA9y)4I*lno(%Dq1JbUrtX-o>LYTy5-n} z<Yc(8FmeD$1p>y?z}W?b`Nyw6gs%w5+KwA8Prvo37t80I|A7?L*uzGGT_?!wpv=`F zZ>;Idnr0{hIOCU>#D|mb)Bp47m;Riw$;~SCy?~nHzpTk==m+?;bv?LpIyUdGx6X+0 z<4eQ6w??cD;I)hAX;~pK6hCcZKm4Vi=%B7okrcT}Cg>(l^Z$2wY1nF!Spa$!9c|9@ z!4}4^rE5!lMkS*oX(i7I)$eM-WHEZPek!Eu`{0Lh-(T21s57aIZ6(ppY`fxBQ(Ik~ zElpB6OUKz>Lf0=HUY7U$Tg8r7c&ZTbRg@Y+NBe*u5$A|T7nf*Fhu&F(cU1nhS%rD0 z-3H)VuuRco%ZosF?gulR*1lVE4^Oy-xZ$jnpt7)Yf5bCyji~7DK<<9SKZ!+jD*y-^ zb*fatjh~DN&HQkHiFoa^|De$Exa|{aCnZx{*z#k4VnK6#z)c#`zR*gsdQ#_J&y5oS zEfKywZF9-wI6L_a)ziyk_RfpF6K(CvrjV0bs({k<4JPitnFYqYSxoSm$g6Gcsq!HO z3=07M^W+ltyBy8B%Mq&JUEbR81AEEfrhiabZ#6tVLqgl82bJY^?RbKOr&0dt?^h5q zy=`d2;%;rU&S=p=4WaKNuox4n(DK!}Z`rEZ56S}#+a5LBZMG=d+fCziBC}b*umjAz z-QXY}>ZuR`%wZIU?coVjrl)$|#VGpI{VpUuph=T`?zboW<r@2d`$aE{t50wyr8&g1 z#q6Rtq%sZ>y0lD@VYTA(raCEUG<~4@!uFw#1vxqg1Xp|X&C7Ccxf5-)RD{5Uc@vn^ zGgqST`u4{Q?{)5TZ=kyV4{x-atj3u?$`)g1H~3ovUZ@z_TQMi^8+f}@=(PQE7pp5z zCfm1OA}I=W@<%Ed*gFRr&K;y2sOP0OeZZOe+;>^{Pwo<Q@wHhv(`5e~eK!NI{4*h< zg;D2}4gR-p$3KZ$qfg<yy7$ySgmuGDuCf54?h?L;gIDxV`>D(?-yq9U6ms9EhTsi- zvnIU$+qb$4GlZPae!nD_AMMRa$K*x13X>Z$mpp{cUG}o6TIPqsPBVocG8%`QcttC# z{>B~B62D-#6VopW<{;w2k7Nh$LOL$~(JyNC0SyS|UOE8&^*DgWXJsz@E*=hQ%`0as z<l3axp?Im8j4fMn(K2BKE$kw1$@Lmw*j3-|@%hY;<Mq_7l6mT73WYN7Zf+(dV$tx0 z6XGI(f0BB)R+V-aCs)u8D7Q0V3K~($>5$RtAzRc7c5kc695&G_b1!L8ny%x1TC0bM z%o-7LwDcYK4rhU@InPD!Q#|k{JI-3?=tLt*ID2G;UvS11`5=kKbvzuhljC+{G1zs- z&0fODxY;`mSJ|vD+i{@rw3R2wivChnZd8zR$|c7zfljT3$$`|CFOWen7srK6uMzh3 z`gw46>h6#BMA=Qc70zP2eC*S7{1MH@+Axk8-`e~Mj_dn8SL1V@ZIq4cOCJG-)}*@6 zAs2kiYUo_O@kb2pqWPPBK>FL)oiqhT<ASuX=5yv{I@dT4>m$&Z7}i&Kt%1K`t!F?$ zKj*FYvfzz-&dWiyz-Kj~9gD($JtC!mKnq8sK9=^yr*UyE4moBQF2BD6vy-wc4g~iV z^D+jmih_eJ9i?Q#ua6@E^ZbUZsd=0alCR%-irpB^Dcg(IbWeffaW;-qM!dQWpTLe_ zfPUM<_C-+^LKi<G?Opc!I{IXKj;<WcHcQgty2RQ{XThTz!l|2MUiTEp)t4zup(?>A zuSg70lp{3GLXBgs{evc5hswz<0W1vB4RPIwJKb%Y^ZZD&$BiOkPSJ$feY`1prSc`n z2J?|w#}^g@n(~<iR@<Qt35=2D^K?bfDe&IFNEH#F;E%{V?K~SvVp=s(BPQB)OOFL{ z&M1$+U}aiA$9Vc!r8Q^vFqbQgftYKyF*4Q-RH{DL+nh4@khQ=05fkT#rp+y51f4g$ zZ|+|y-Ky$RcmaR)6s5`&ebn{I-!pvhZucf0N?V_{-Fj64`%2;;foBI0BJTV8bk{y_ zC{|`eJ}ogh*gI8&hbU-_*7ce_Cq~KrCxdbQ)2<Q3g9VMJSqK%;M*o1tl>U8kVq+iD z)b)`bHRa7N#Qnu2ux7xy908R#ptOB)|Hj<J%wb+|r{`qrYx{CTHHbq_@wjm`@HREG z;nuVBA~nULGafWOdK!PIGzh-vL6Pb1%Ezq;;c%+j_4%E%oTCrKz1rJ2(|ChmQoMCs z<xP0oz&N4`trw@;Vj02J{f<FqTHCL)i@6gv76e}&vyA{$BPf0&z?DhPq9v}lwe9pn zHELHjO?NqEwbE?08hMZWXYtlw;W~o|;l~(!ztd@(;Mz>VV;JRw*gL^Tk{#wb&|mhm z61ycVAA8%+GnL*yJ%1Y|PC@3AV&J6#hP#aI{g)p(oZ7aH#4ndByJ#e|T>NVZh=2zO zlf7e!Ih4C|rXrm`3&}Y*&6cE1g<}&dH85N;e2hV`hiB(wCm9JrfZ~Gkk=#h0Pc`Vo z<a(Qx28Zx@oa>ub?bfsIi_rBeqZv{n`=V$F^?lG~S-E-BwTb5WA%H$K1+o4jF7N@k z%^Upnon_-gzOHC>L~7T;LIpL&X4K8^i$u=<y_U;6T%P>A+it_O62DdgI9QYb$dNay z1k%z0jBs1N*0gr7Y9H&wczibKJ_VcuV9tnoucyC5`<x$q|J2cUA^a!{lQ=#lYdNz+ z|A8>^M&rx}-$4qcF6j3XBkbUMg=gc28EmiGW%K5rRev%&u{@gdJqYsmc2WixZ;>!& zOG^K2z&Jc$2EW&)kFQ*hcBhZdE~iZo>bf|HN1A*ry2yN1#{#FWDLQ@J`){5g485Mv zE12%=U91MGkCjw!_Sk8#ZujRvkov=(jk1e9@V!7odp^-tSwNQ-{Z_!mLdE2hTX;^{ z1Zk=k6-opaT;s=M0P78!Q+&YDQ%HyYi(u2;lSg%l<C6G;4-1iSbbIp>mfcO*x$0dH zMmhx3KMbh3<M*Qrw;UH-_=LArl{XWhhn0=-LYCS|iIO4hDfTUb>Ldl(ACnZgpE22a zgLXbCfP1c}D+$R51hyWt)_Gbx=ZQMucJp-T76{6Gz!<equGddKQ;vKthC*iX#xNG) z)vgCT6ia_^LpqloaMwL$&|ho3$&LeHeCKdJd;A+x@N%$jvCX1eg=>Gk;=$4#;dXzb z`+W77eXSB~xM130laOWAyj?3RJsHM4LwI9x8A@=G)|BoYe4J*Eh?<<8w6eHM;&reS zarQEMS0Tt8LCiPNV}g#u>69MSk4<X1&4%*-yLC*5f)FLW7^m=<iKan%y=0}7Bx}Tt GgZ~fQj4B=g literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/menu/shop.png b/Telegram/Resources/icons/menu/shop.png new file mode 100644 index 0000000000000000000000000000000000000000..80dfbc6f1cfa20f8e205ccdbac3ac965206afe51 GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfl1QS#WBP} z@NS6jYR5p4+V49(I?lMvTrh?4r^b_HFE*n<qtqkw6Eap@nf;sF$A?G5^&#&Rqkt0) zQ#kbxMa#%u-@EtsvvUu&e7pC0UG=x)mh$rXyCve%?B<`(+G@me|NPRqe#`xq8_zzw z<J@%qRadjjX3zCe6Y6wXfBpRP&#%9(y8QCP!Lr?<D?_rjR&9;aoa(jsq6A<2?z?*7 zuN%*%eSTW>vBGA<<daW|cB(Y4|IJjkGsc4ZNkjM1M-?_|ESu_sl;!&S-4@5KXYW7! z5UA0_g~6u%@WJ=r53dQ)66-$twk-Huu!dOo)z_s$1y*yN7JmMDCvUs*L=KDBUw7Ti zNi&k%CveH_T(4U*7cU#L*P;bivji0@vbIKPO<kloO(7&uYigU5;_kbAIR+9JcE{+o zuQ;1_*l$DF>aSIMf7bk~u$kqf_Qg?cGGp_HH&F`%G^Tp3FyvwkyfXC<vn-24oVnTI z4LaQuoE9E9f9a)(Z_@<%-*x->*_!p&+4Ed*c*Z8bvM@HLXXb?rlMipocE2n+#oL*9 zuk+MXBeqF;GrlB0?96z<R^c@JEZbyH2?hhrXpJv(l)V-?=uLl|CLqk9#>?3_W5NX8 zN0ow3mPg*(iZb!0WHIgGSg7CW@+e+cz(F`W$wbO^@5ic|`{Hc^!M}^+84Nn*S%5ar zdtrI=NO!xM07pVW;-jL;8`Ts!zW%OF=&_UAb~MTHPyYV@OfR?pU*RsyxEGXIJYD@< J);T3K0RTJ270Lhr literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/menu/shop@2x.png b/Telegram/Resources/icons/menu/shop@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..38625e754df56e214a38f600e25a66b63f7b8930 GIT binary patch literal 1239 zcmV;|1StE7P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGK}keGR9Fe^SXnD>VH7^!F-3+l zXPzZ98A_tKFb_!*<-$L3L$2Jo^atdE3tUJp3~@n*BtxbQQOHz6gfer^Q>*iy)&BP0 z-#(jjI%V&RujhT%de{4WYrlK#wcl^wjLXP?k%2Fk0h9XwEh;K1DJcmJ4YjtmR`;}M zJwHF6pPz4UZ@<00wdgG4U}tAHG&J<%voFX>N=lMh?-!+*p^lD@`ucjf?d|O?EiJvh zy#a@WgrujZ1N8Ovy}Z0o5LZ{%yu3W%(b3VXt1BXNa&o-9yiQL~r>3Te@$vD=%E~gC zOqZ9JVPRo6H#fv<`V9;W#Cph1XJ@CCm6e#&*475{@bFMD0LjV8lt%!;x3;!|ggz(` zWK~radA78)2vQShXlTI9*Vk7M4-Y<Je0&`I;NT!9Wd-Hu=hND8adF&vU|<0J!NCC+ z${oJ*fxra?1;BH2a~BsEf&@rNNWkyv>MAE?@$Bp@-DTV+?$FiMg<()oP)0@u=LMD# z<ml*_nVAWXk&zMM2jK7TkL?1WMGzh(a+3%`%<JoG1gWX1;lTx#5tN&oYinx@kKy5A z;RleIm`Fn{g5XKpnKrVB^Zfjb2Li*&%1Rp>8^H@8BdD;j5N13e>+9>n4}b*WukY{g zb6yEgPENw)?(QBP9nF2ZySp)Tb919-lbe8L1YtkIAh};)ND$J4T`7qoJDi@Let&<5 zDLXrxJIu_?9335jsjaQ$JYlII{2O0iU)Yoct*oq=N0DVvX~<b32>++2rw3Dti;H<p zp&(L0etv#r*xA|P%OJx60Rdo^mzO!Oj_IyJkmMn!&0ueD@9F6&iSWB>k-^*B`}p`6 zZ!XkRUI3k(oFXD3;Ip;0_3-e(qc}J?L`6k`+1}p1zrW`u7Z(>4WiXqYn~#r=!W14J z?(FP*c6NsHZFvJUHa5cPZ(n%a%F4=wL;<83A_MBs*w`3Sy12Ngebb1bDgaMLN=gcH zjQWm@s(4CGxMfIEb91xQ@UbxhA}oR{g^_M*CBJ{h|965u)Cb~S8y_E!R!-#a>FH^0 zZB665Ma<$rry#tBdV6~-Dk^v^f&2UWtE;O;2Pb$fK<6!MYHCtagH>UIgO=sZeRK*c zEiFZ?iHQl+H&kJ9VF(f?axh=dBBN6fDl9_dHF<Y;$Jf9RBp7nwCXHC9AdS&=xEX>h zCC$i??hMgKjCn_ADWC;A1*G7zu8OyedV|>5SlMa1NeZaacqj_OmmXF{&Bf`5?&T#D z5sK<N1(#T9DBqyLhVFA?V}n|39HZdKfzK~5FGu@^GKh=wz-V@Hp#{TNGdJN(CoL@v z%*n|~Oiaw59X3!PG<CbXyPw<3A|OAL;ED+=W{Ava;Opw@Qd3idgM;zH67wS?Bh`<v za8SX&Tkx-Nz_75efNy)rhk%G_03!oN2EJ$pegcb$u#M3xz@q>F002ovPDHLkV1n!K BF#P}k literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/menu/shop@3x.png b/Telegram/Resources/icons/menu/shop@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0da0b229c58d68318ac6e55a9db8291a10671d22 GIT binary patch literal 1770 zcmV<G1{L{<P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS>R7pfZRA>e5TH8w%YZT8cGbuGM zsihAgD&~cXQc+AplTt)f1VstO2cOgzAzz|@z?bw%h(JOj346(sG$^$)FK9-lQQ<8y z6D%#wN<F{BlEpCJ%<S3Yo~@iS`(d+Y-7nwzF7wTvJ=V|4fRzC&16BsC3|JYkGT^KX zxEM1O6clvu;6Y=u`lZ&^)@EmCH#ax+ix}k0%ggKT?*9J$JB&H!_w(n^SFc_b7Z>l{ zyVs!8+G0LFKF^;&cTO;hR)&U#0s{lJowS?k<>fUnFraA9naJ_+@qmB;o!;x}u^TsT zR8>`x#*K}Q2M-<~f2*sjYy(3yE-ns_i1YgO>%V{hW)cCFm6gYj9|!#T^XG<!1}W9U z!{geuYc4MTdeYa|*V@|3)B^_&#Ky+v<mBwzw~vswZrv&`FK32r0sj8}D=RBh8FW`- zVxp+7x3`xVK7INmQh^y993%!*USxRp?j2Fb#>PbIsZ*!s=jVyx@87?Xk&z;gh_T&~ zSFT*)39z=d_Qi`AGA(H9n3xzsj*N`RsPqW86!C=Y>FFULA|gT@Y{SFD)z#I+;OgqC zvtqUb%goFqrN4gtYHDgCSo$9p7KY(OnAfjgOG!Fm%(TpT{``4{8E9!~nVXxV$D*Pl zJu{?@zz!cioSd9Y8lOIWiZRK?7{E|r7};f5KhV|H#mp%wDGW2PzP^6v&K-I@cI?=N z3m2HA2G|Jf;>C;m_wOg+hYuerYB_!SG~<m`Y+_<!dU~3aC<X>nb8|CW!Lr7%LEz@i zo7A=V4-E}f)Y9JGPCVbfeWS@uWaHIBaq<BF?c29W31(eCKR;0eFqm412XmOaySpL} zBW+ad^5x6K`u_d<j~_p>m=xfOVI0D+l%yZVI45SLMzPI6<)V3ddS+*5Yb)0Z49$Z9 zP4aDRZHkJ(aP;U=A{lEil705<8S76?O=VaLa9}{%9F1CmWo2ctUOf$lRh#+k1wMTE zFf%hlQfh%=k$m~`CGlV=9XWDD!-`g5JkkI7@k2UsN!9T<1adopL7Ycpbab>@n{m<H z+}sKZ3Z$OMVAa5|>y3|(C+;Uto`}07B5`0iVvLTC%Cy_{$bq5F)aJ&!ckg2JL264% zN;LGV28IsCGEb6f4F)O)_U6qSHmPAxgT#S>M{P7NEG#^J{Fsy^B_&}qsisghuoEXv zune|>3`+rTZ*OMX<~W2}%(}dqX)M9q6xe^Tb*WbbZ|#~zf^<83_3G97`g*Y-XV0Ex zLFDqxnKS$-19;B`K!XkIdq_wKWKW(vnU<EexVR|OgI#N3VId*0w0!;gRmNcZ5tBGh zue2cU&VQV&HNx2HB9Q}8VKU?eT4dN+4Cbob++5xn+wDw6MaA;+@@}gb&Tne73rFa} zkQ#94&>^%nZ7<%weT!yO<1`hIBYad;6ij^i@&$eP=g%Jpn46lKqM|)|^vD6~45@%= z@&V51QJ^~A>k;0~JXT=4xjIU|{bwtty7N%Vf_*r=s7pvl2oDbzkJXq4aB{^?O?>BN zpAB_xQv$=Q$i~J-ETHOkw<!1T-^WsEWM^bcwc`UreeT`6r*^!T`GeqneSNXoqJ}ck zHXj||V`XJ!I-?@j!3>dYh9QbOJ}~Uc$qde>85tQkT%d-sACMsN;gv;1{|sXs9~iC? z$Q!(`@95}I9AiL&1P0<{5yN4Kf{qUiZzISI7eurwV_*?AP6K$y2WG$tGeoSw%*d*l zI<1PCnO!q<TNN`St7hu_e^-oOm71}V>MR?ZW@oeobyW;sBajyUIzU^g8B{jS&S+cF z6&P>oaL~%n&)3#y1{FqOnl!UB4lsn11-7m9!=X^^j)yGa!yw|6-kvBr0S4lPh#~_a zVGc&O%x`Y;R}2~=AlZIlO7eKEcj?k4f?=bxv$Maye`#q+)Lv6lL)Uh=0>|Bw$iNtk zE_|hhM_g#s)zvYc0o*m8J9jQUJsqbWLgFId!EfF1!OZ03B&Tgh05~9ix5HU54g?1W zqe?px5{$w$95C*^wj@rzrKP2~;m4fooCskE=3o@2wSALO_4b%d4By$|nnLj_5?r5h zOUR_u9+4wl3yD;Gy@knL=fWk5TNo<?RtBsLSQ)S~U}eC{fRi%t57FX%+DE3ndH?_b M07*qoM6N<$f^!%;>;M1& literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_away.png b/Telegram/Resources/icons/settings/premium/business/business_away.png new file mode 100644 index 0000000000000000000000000000000000000000..b6fe3bedefc44399c2ce6273a838cf37d10fb26e GIT binary patch literal 662 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfr-J>#WBP} z@NJ0giH(UO-#-8HRr`6?-zR0_rogbJI&7y>%x28gknVirl@!9peM(`84_C8OP>`A8 z$9>c1ZGXOZ|MS`v`G0@CdpYNI%yX;fme2Re^F5z$d*^%nvWpo$YLk7`tY!GRk3Oog z+yC5nu~4UqT)+SF%l?lXmfp<SbvIA#_tf_#R?|<bE<9jz`st<+t=g&odUwaHyZrKq z&)=uzyJHsy{0Q>aoBqDcI_2*Ba-S5l*?Ti}r=Q*{;<fy8$Xlm{4wJo>URvKa@4(wK z<Jo6T-uACu8>YSZVu!@OJOha%NgKab*-G{H9e<o+#CYy$(cilLN0SzQ67F<So4j!A zKHjNbu3>&lgE+2anKm>!G@O6_xxz+aVL-<+Qz_nMTs6Dz`Waiz<?D)GdkyIRyLp;6 zV848<@VWf!uU+?qu+>W?zgEdwzHm|y2+<OKa5G2DGQ#F;<H37^6|>K_J-Jx6`)-?t zo15~Ki9TwPR!l7n&0ahC+L;6Q#`QlJ>UGl;h+e$BPh<WR8NT*|2>}ZOCMe|0^;;gc zI@e5k*|U!o2EGkPlLS{M7}VeKP&sr<g!#?aDy4}p)O!zC=1s9#;kj!~+19A9KWnbP zo@)AwWvZ9zJGs!B{rAs5FWxlq^wUdOTld82Xa8gGl;UB_`SP(M!S_e<gSuemW&hkO isvJE2)&G94_JMWFwKw_GzMgpkN)w*0elF{r5}E);92(mI literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_away@2x.png b/Telegram/Resources/icons/settings/premium/business/business_away@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..be8035562fb145483be761bd94dda19ccac2fb6f GIT binary patch literal 1190 zcmV;X1X=ruP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NG5J^NqR9Fe^SW75vQ5bd(dFD}v zJZ}g&3?vju3K<v~7<iQ^GdU(?U}T__GC&MaC`txOL>VZ`!26l!z$10ZJ3{2%&s|y8 zU3;(nGPr}g*TDJLzrO#m|NTGK=Hz59GXiD={<8>JzVp06K|!ghsrZZK@9%$iclYzp zT3=s3I5_x@W__|TF)>X|O)D!a@9*!w1ncndu)V!KJw08YYQ1Q<GB7ak_Vy-lLxw#) zJ)Mz}q1TW)-eR#dG&JaPh4rq#zu(i-Q{9Y41oC5Ya*{P*fZpETj*E-aXi*8~=jXSz zwPnBqsmS&9b$oohk}=gYcX#*M*;%P-!<>_olhDvmRl7n!Pfw3w+akfGr6p%)XOT&z zK9rW0Ha|a)bf&`3&(AY6GcPYMl;h>)m6@68=H^B@T8OHus?N?%Eyyp7)oR6~a8H?( zl=Ox8q~H(-Q{7%&UD<55PX*e3v$C={y{P$+kPxY~i;D{)$AMM1wY9ZURZ5($t}dz4 zjg1X-qq@4fii!#<Tv%8rMXGWZ78a-mbplCuad9D1`uh5sot;gz#Ky+n-rfSaxVVV9 z_T%pr9v)s+RwjyHWMrheD+B>WB5%O2uC699Utizd-Ccq#FE1Bq@9yrF)mBhYAQIvE zm_D*9!0heqC1_;*#KZ)Jq52{sB2aE*v4uT8K2BjQQBzaH8y4-0ii%iCN}~(7y1Ejn z=H_O~=ZO3JdpafXazzucrKLsW0M9QeDdB8j3%z)i6dD#5hJ=ZZj&5sfW5KwS<mcxT z7-DvLdC8u#v<`=)H4jA(4-f2%pq}~o_^=nF#>Pf+gz>(*y2^UM(K=q4-EQZE$+oq% zH8?o<GfzH#;SD^X;IXkW+S!MOhA3Z_(2>+g*z@yqSuG08ot+){N5X@?r+_3!QBhGe z%<4p6U!ST*7Jv&idJ@r_o0}_2WGb?}ye#X4RhA3p$jFH3NS0q;U%$7vM~*P6;1lW# ztGKwB@>#**;bC^GiIxsm`g~GC;o@9cTFT*XZf*hs0yH_ng7Wh6I7JHE(a}-a0gfx) z;NT!9=x{h#LoEGu*I~JoM%S;btfX6I0y7SuWgG@KU~g}4mWi*nl#~=C8Ozj4dw6(2 zo_oIUY|qZl*zPLzLzvVA`t<aK@hKHE#z7?_4UUhGjfsCqq01R)NU|Z^-``J2NHFdS zkxWcXl+~`6i6ZAR$)IObQ&W0vbNIQrxtyFFgXTFg^fXR8wwR+RV+?A1iAql?=k)Xx z6^bFv=~Ue+l|33`a&j_N@%Z?N_TACZLAyTc1=m)L+rYp;^6wZV94a4$xw*MX1$7Zh z4&f6MIfRyqi^tK?(c0RY!RDdMBNJ>!z>I(yfqxu<-?g5BnfE563;+NC07*qoM6N<$ Eg1NaTLjV8( literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_away@3x.png b/Telegram/Resources/icons/settings/premium/business/business_away@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..59afa2748fc356339d4b1ae9a59cfd4f33ff00f7 GIT binary patch literal 1806 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa60e6a4eed7197178cb12965176fcb66249618 GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfEY{P-F~maf z?Zl0JO#uR~v0E0JO{;6rRTZsQsAs58NP09qC(+x9Q<Qbfgb6b;bhc~_5SD(;%iFmr zz49b;_?6P_H;!q_mA~6-oPX~^*4{_@(i?KC&1Tn5VN|Hy6}|m-@1hkNT~CXC#<4Y+ z@Hj6~zg0PJdg{#7$gfgC9!sn$`DZUX`E>Ec7q?hCO=S4iFJ(LS{qNz27xK3M{=0A8 z>qCqRtuBWy1gNd_lU(xps}%=R!()r%j{_wbmLx2>n#DSyF7ZVN|MMEV=T}U8L$Cg= z<4=>_eV5O{XZhu$NsJCsJZuav|4({-@G<9%vuVZY6)HS8mc*N^R9j`9djE}S-{a3e zbt+v0c@8_=%G*9Ehv5^Op2&}DH!UV!y6`DH<m(m_U&%%_n}_Oam;B%Tb8S!D`f{te zdA433JSPR{{MqDfI#D9sXr|4a6Q!}QTI0=jG4xD+{&Q~OkNC$Qe=K@7_0M58JH|tk V1y6Cwzk3V{a8Fl1mvv4FO#sWG!?XYZ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e48940aaa12414726b4211902254063cc7f6285a GIT binary patch literal 928 zcmV;R17G}!P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NF3Q0skR9Fe^SFvg$Q54lEaRo_b zVXPt~tq1`TK}<r*fQ4A3wG&bZisUPlRO#{ocA{9>qzxp6NQwk8U`!y23IU@oVfWzO zTprIEoj1G7ZZWSI&%5`WbMIWonbBsmmNfxu0@ein?F8(m%WrRQClZO1lasx@J-^=% zpO=@H$H&K7tyZa2USD5LJ1YylUhl=l#cVeFp|g+z0W@W66M|SQ_V)HBb@F|&q#$S@ zn<$|mNhXu?`J4;m+c`Zw#do){SOC}}$OMcE5`_Xv)cyVa$z(#sI-O2D9@mo}92`77 zJrNTU2=$oe<C~kC?RJ}LG@DJI&$rqO1ki{NA>^z1%Gqo-OYWo5C=du3(SrsusUSBZ zG0L&q?a$B8WSz-mjLK{_kVyqOOpFWHFvH<6`SyA}rQw4N329-nrpzL}VT5#id`vXl z@$f4nTq>3B?(U=?Hns>NMI4YJA<QPU{jKBW<t3eEDwQImqoX5=v|26E8e58kKygSY z0uzy`hqWiPvskRb4Tf;H+a-X!1f(q!Si&Y?F6@6fkdTiW4V|B#<2Hi@Ks>%~hQlF6 zuCK3ga|UiS8tlbeq(GoJOW0)DUv$vG<MEh=3_$66y-tzc-CY($qtRS0x3#s!5ddrv zM2drukQOEsT=r+OHXuD6iNOs;A`xwkeiRa^p=Ezjt?`wKX$c=59!in5TN4u5bNLK- zLhtYI!C+7p=<h*>L}@}!eK(Da$K$!Vxw*Q!y1l*q`1sIZe1+U@H=e2KblT-|eZ>rY zsx<^l6i|A480tbGsa->Y@NYq@p>>nhn9yJ_AQgjkCTe-MWgzM`8p`MMco)V3Aga@; zeC>-hB)se|5j3{n7K;-P4-ewa$s8`1%WNq5a~weX)Th%alR}{o{tEKeG`PRNXRYYZ zK|=uVop_#`8;Phw2Y=+OY-eX@qOWOOC=?70U^NM^QT#{DbSs~qpT%O);c%?3Whje> z5kDGDG}Q0+tJSLX(ru`)MY1MfO~9IfHG%(m0zU!X#bt_=W%$ql0000<MNUMnLSTYL C&x-;8 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2db7e80ea5827de72e34dbb9cbb80e22dc7caf94 GIT binary patch literal 1405 zcmZuxdo&Yz9RCf=Jj+{UB8G^wk#mVDv#srpv6+x!Ix&}yJn~qLrg8N^QbdK7xE^`l zJVV2sggma?5S@%nZlf0CO6$%)_nvdlJ>T>Be!j2s{p0)np6^vJ@-a2#J<0$8H6j5^ zl`&x(Fgcl?x?#FYMvz$QF$|~~GWZ~KupxfLP!A7aDYIb!lKvA=*e;P_j|>3Fb0I)p z#*pn8SMHxIfh+&7y<M1%!mj|JAS7a4X<<;&36e$JDSc_O>AaM(MYx%<Xm#5Q^P`zr zMn2Nc4u!HqqL4@<EpH+|X78eBj1@)I{k>R7#>9O1S~OJI+}ybC&k+6kda|=?iZgbR z`Bs?p`sw=AMN#AMjeU|j_<tp8g?D5*RO^q`)m4c^($v%>5C|^0Df)fi2{$)4ud1pd zkw~7No@P`ie{_1fy`e!Yl}ZDc0O<2v$KRWHhJ?HqiyLAbY{;obBgdxn)74c~S;@(5 z+BLx#qi!|bu+S`onHiZ(maMHgT0MRCEGQ`Gh?CRPmKJt1m&+|LFR!mhIXS&iDk>^k zURimLSUR7?LD<+t1_yUBkB2fC0RaJ8nwt0RyI^`?$igdSWVcu?Z*-JW(fRG$x6RE9 zg;XH#r>yvZeD*AAW@d)!9~&D>Boa6MA^0m;xf4k{z7U)%A|oRcJ-ofWDJKaqS0f?B zy6>g$;};ILw%t{e)6?e*DTmL(unr9QRx+3A9vJv`U_d2T{OJ?hsi^o6Oi@YWvcZ9e zW*JiH*!XyEP7dbk^WNUy)Cvy<LQ^@Z507SPKiXmbxVoB>@VU;`7I9S0@*(cpyIvF9 zkdTlh7Rw8oc$lO!KR<uS-oCW7lo(H)l>^8I7=(tsu1zq7oUUqm^2F-`3=W4o2f4Y) z=ZWfyi;M5%=0@Mi4o+18Jg@+~q8GS#`p{_fZNq`~SFdP+)3dY2OrcQNO6o&u0=hcT z35wdUvA#4wdF%Y?0?x6fwpP}J$me@gD6W=yX{NV$nc&NxYBVELQ%@`&-W6i}yr=G9 zd3AMK2!kOMi84=a;aN;3F&^fiqwLf2Dqptytu0o{{8hS=@6KV1Gvm9htgZQR;-Ez~ z$QAs>*^o%`KJGHyw{NGqdf(-+9O64XTLWVQ^NY{!*E+Cd+@8nwO}MMHi%Xr@*l=iV zIR&C}_$9gl0D(Yg<K!};QXt4^T(*kb@OA9GAilz|J4}fc4B20<U1q3HFDNL8i;LS* zBG+TsvJsd%(OiHF5{D*hjvL(S&!Gtf-o+U$ZL)(U#3|R-*3N>O6)rtnIh%GMEj)`j z5@p$BC77G*VRlTp^3PQU_9&gB(-#ezCa;YC0xZ`mM&^G&BG25-&+qH&8*#5-rzp^r z*_{J}JY^M?goK2M@bFbFr<=0fm*20kI9Pcv-MmdK7K=opl9G~+4)li?x;GU`vr9|i z?$(^x@d=(6x_?vW@ct}Qz%C%6*qDy8I{7bC^((z^>?AV`UPSWv?w;T8jS(#qpe7_J zp_0Z4r*F9MMsxa4b@fppIZF?AJZ{j_)8k*%Q|MgGi+=<)(|xwph@;#{r)aD)7L9yh zEN_(VoH1F5Z;n7ynO-Kf&dbZokC3bdm5&3KoqW<{;Ti!vs9ea802v(}9YdKzQS@R- zW~N%PYTN8YnhP3TN<x%%fBsw_=`W4ma5ot1iV_bEk-tMDP=v3;?XWrV;9$ma52<Tw zYwJY+As66_*{Vl4I!<1Cc|w)L;nd*r?N6_VCniq3+sl}*_wn(WUD*?wZamT1*;(hA t<=N=w=H_zbI^OVS+=Tl7$rc>GA%CT8aP-<4zm4tJN5qk_H5mHkzX7{|bCdu8 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours.png b/Telegram/Resources/icons/settings/premium/business/business_hours.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f0d03e3f6ad18cfe6f5563bcc97722fa6c70c9 GIT binary patch literal 426 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfOvKa0F~maf zZHOVCi-U+L>)g1-6Eg}pdMEE_;NHR1x_d>5RiIUa-@@Aqgnn<DbaMX8)J@V$<M&qn z|7-s2y!@<hNAAmhsI@=;dEfi^`3whyp76J|B~D4*nR{E0)nkc5z-`Bn>sy~35ENLk zcY|X1miM{0%VO0hOo*)&iBgz<;`1H@9#-WOEp;!iSa=G>K9@+Do;vgS%u@}D%`+y& zM9LgDDeq-9On+i=uGH`>SJJtTR}$NFvL)I~d=343LKuAJUI@{dy{s@+eCoE`@X(_y z0`qT)x*jYMS@U{U>deepfg*--&2?EKER8jP*A&Sd&z#k_wC3-kl{OR3bTA!tIP`U! vdQnB<;q~b(ol|}-yjXnigY?2OemC}eyzi&<1@t6!fx^kt)z4*}Q$iB}wRxH$ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours@2x.png b/Telegram/Resources/icons/settings/premium/business/business_hours@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ce098492028837a3ab6cd4d9b7fb8cd214094184 GIT binary patch literal 765 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2Neyz?AOk;usRa z`8HyA*C7WH->?9e4I3x2);ol35SHF3!0l-7uwm0zaUBtzw^uINZ}`Bjvvp&Lz!A5I zkQ^3i)|<8#N`{|(9&L*BkV=0yr~2JoW8<B|re~{dSV8b#7~|cziv9QJpMQS(X_1ZG z|2_5(e(tgdv;Ea3zy6xle`i*>iPYQgzn5Qr8U2HSLE^x*8@dab8y!|(wVHqakWU;N zV;i@OE^81|^190}zo^bCV(-hJrZv?ocYE|x772$P>XSW}uJm@CG3RiON0UNjvtLBr zZEaU>R;}f~Iin{%GmRAMb~T%Q_G!`UueVHG${jUgh4mP?R?k`KwbV&t)m5#zjVGC$ zJ|3zqxO{O-+Ga_vRu%@ME%)DhJ}$8EIH<s~R^%ANgIJx~-xF`98%aKj*AVfkGT6O< z)l&J%-6NqKQx#?SKHr&h!;!m4j@exCisroj;@u5%W(%xZ_W6Jf8>6MU;*<ipxj?Z$ z|5cXgHJ^X3{NQHJwbx(sw$HvIzTCgdkU4C%=;sTUj=3#PEb&`@`BD~h(c<nP>2NzM zv&+B`6Ygx87;^n<)m}bpVSeQky%kH%jy%73@!RRAm-4nZA6zieN~rV9^I|C;?i0Np zOUpJ!q!e-X%s#s;L`(8!_Esrj{vu_cm22ayd=%Xm2kM9^8(Pin%M^(%d?}#X9&P2j zJju~ho`>!5Ljm=^W6fbAp$5w;{w{Fl(&<|#zx(dG+i$sywi&WGUAR?Hvt!3QuI@b> z3iNh85ng!btE5X&li#JL1Gi5Xh}>T+|NHPO{%@1J?Uv5JnY?cQuk^$J|64!PoO=C( e2nbg6GudBYR_!`I^?L&->3O>PxvX<aXaWFn#Wscj literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours@3x.png b/Telegram/Resources/icons/settings/premium/business/business_hours@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0aee12c236cf0f7d0e75b1f8c227a1b31a96be6c GIT binary patch literal 1130 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbAj}e#O(pF(iZa zZIofQlB3AExe6+ZtxG!=BqZifG*griP+?=aykvtweM4*SwxnGmV*j`{b}X3T(AD6o zqbktRD^@1d(Q(A(o!tke$Gfa6H}BqDUNKp1bN;=W_rLGGJ9p+>ak~58l{_oZ;EVJJ z?fY(SDlIQJm+F<1mv2A(FhJwU#|l0^z6PtAK5_B!zyJRG{Q2|WzklDpeVb#pS;p6n zfn~|%msNZ1=K9qO{(Hcd&U4V==Nli_8@F$3i*@Jc=d)N^=}B}vWH1zsIj1oF^whIy z?u#e##XAZ+Ft9pU`nU$v+Qo0bE&AktuQdas_Mt<0_YWU-j?TNXEZ_jsoxFV}drqJB z_Q{w%=glSt8O4m4H=mt6cC&mal)2j0?(%AH(o&@f&gWI)vOV96u5Qm&p7UiVi}1#s zmLDEJe*Ey^!pkpp#JH!KZ0uc7be%!R$VF?7wAg(Ql}$AV`}_Lt-M@b`E=hXf+u2Ij zCT`F*_%1NbOI2)T)LNmV8}c}Gx9I;o+4f|6@y<EdUqAi*;@h_>+iUA~rtG%JZDC{V zYhKo^#WVX#joo@lpFKyak5*;r-PEg-W_eMax@v=q(#AjU9vq)`i<@bIBafm&Plv|U zvuTMxvTST^r=FU@!6&8QT&Q-}u!fuatjClC&uSc)Q#6u24}EC$KUj3E-J?x~=egLq z3Cmm*Cn<6s&g4;DVr=JPq>}jM>S4xQ5Tm}9HTkovinGwMM^X9av(JXDPL*x%cMxg| z_Os*KfBpUU&3uO+8i;kPPWCj3u-=$E@l4c|>C?r_7VF;1Gm*+Jh!>BTa_Z@)8ar_& zmYMntr~iNaz;X0m$+;9GHywl8`*-i=@)>Sw=n<T<;qR|sw={Sz{`>i}vb1#T{{8tP zzaKqn`mvGS$W7tUx6PZ47cuO+onT;LZ*R^e&%tu+v7tQUUWMcqg`DI2CP$?Fm?kEZ zmzURcaDiT@*V04o47}Fn2Q8Shx8L3s;v@0+<B!XigBJ!A?7lne;e)jsGxQvpQ%u~v zr?2?(p*VAqxay|KlP8Pn?rB^x`(vR)^3!~OW@*XGs!Dd(qt>>aKOUm>^~aAB-&q(R z)p_|CzY70zU{kfDLyzG_>DQWtZqpV9eEHF<@v49JPUhg~5-%HrR*TE-cf_`O+COUB z_EyJBXa0$|I?B3RbJHK@FZIp($^W2ln(_2iVds}DVP)9BCu$UFn<HJ^xid&1eICn# zFG6ao53cAnW$5nF-Oi`bFe!SE$)v=T<NH!>*A*S{I;tir{h|J$d(x})+Bda<xl<WY eVB>!!=KYMDRNrbZ(s;-U%IBW0elF{r5}E+<kmp$d literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_location.png b/Telegram/Resources/icons/settings/premium/business/business_location.png new file mode 100644 index 0000000000000000000000000000000000000000..ae7020aad4fbc6b1b8c5f2e2952f4d8453132fff GIT binary patch literal 454 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%)rydF~maf z?i9to76$=W_J~c5*$N!j9UdE0=&+p?JfO(InXY)SkoAZci<o2B#^28QT6bM9zg~Xj z|Neig4Sdq#)^C?R{o!QI{r<-mJkBN4QYDXnwBU1&-|uy~<n}iC%ml$#xBWA3Z@a|m zn)KD>qm8E!>t0C_i{kI=UaxztwK@L!2~MsRcOz8~-kv&h)v6;>PUixHH|*BzYHFyx zpL<(OmLno+;jtV0<(AHSKJniUw}?pPiP?(|@IP$5pt$9uY;7E~;^Cs+UtJtc4fEnV z-e*U2%xcO!YvRk3EXJyP;;_zXiIX~9b1qgLFML*+W)Qh1;6l?ZgD#~#l1-ipN4%IN z7H!h$RByb{V6*Pku4TdkzL#yn{>PTYs^@*J&DWd#?}o#p=@XytOpQEjFz0#ZzOU<U Z$9v80Y<+%9XDTQRJzf1=);T3K0RRcws$>8F literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_location@2x.png b/Telegram/Resources/icons/settings/premium/business/business_location@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..49b7ee3c819868aa70dcb36b4827bb6650a46f42 GIT binary patch literal 845 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2Neyz_i`d#W5s; z^KJOvEG<WoZ_7JA>i2dnN)q+R+HK=tb&);b124;b$15gb`x+D!Z%O7}IrM-(tf_aw zU0+p>sF)T9miK=zC`cclX=}XXbe?JPx$ni#)>N07&g?n;qTym={58hSpDN$KdzZBF z#^cAyDngtrOcx^croVsxey-p0n>pv67pJCXCYea(ZomD%MVz6*)BCj0ny~DZA-CRt zKa{j@QUfDT*BWlo_eDE*-PKzvqRS>Cuu`sM&e5dItx-|xzoc9)yo^&xSs1V&{D9Ku zpEf7kICkybd-rYG8eUr_E0>P4UmK#<x_y79sJQ5TiqXsSiu2F6Gb;ZOOnB1cro8#) zpAR1z9v&$^>fBzqHEL}ILzc~4zt4QV(i2an1e^-w=FVX?7Y_;vXY?!BU$j%k+hsyI z!=u<_)BUb$lTW%fpFe;8_3PL97Y?&IF3}58IQr<k(yzw{-+!N9*mwJ8&bN;r4=xjM z>FQNJH2G4~SME<$d!r6z>dx4|XUg%%mKU#z&p!Js>Ho(oEX|H#;s?7cSQL*wTB_WA zv?={UhDn4aOJ#+}#)_97d*hx<D*XHPONy_(e^Eq&HiNU*t$C^`n<E>K8-9IW@bZwt z(O2H?@7}*Jo!i0S;dwh=#b<f&qNNOM6T2U!JN`W`*W$Ib$SwHBzm5~r*I$15#P(0q zw2a^d&MieeLG9aa=kAPIcH*M}kUu@NbNAi6nwAWH-}q*ZU0thZO!iTG>6$TdWr)|B zHS^wnt%_1VB_&d7ADgBj61D#N6stao2LIxi@EAdX{^KuMRfRZRr8T2AMl8AV<;xcp z)zy5*wtqf-x_gJ(9(Vuyw_f>`JiB@|bpQS5S50L2a?PY;-=<HH(EaiK@HG>;{`|bW zx?|2^tohB`x0~nsHXm&G{#Q$TUB~v@Z?9&3egCkkuCB~Nrpt-#EI3u&Zv5}P>DU3z SuX4{oN!ioY&t;ucLK6V5zj>+v literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_location@3x.png b/Telegram/Resources/icons/settings/premium/business/business_location@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a442c069d4bf06da0b2e4019908ac8c19309962f GIT binary patch literal 1226 zcmV;*1U37KP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS<G)Y83RA>e5TFWbKQ5bi)%4?z& z*CVd-8X%^|%4=XMCWbOK6aES%k32f^2gp@sG7yR6Ro+T2MP^9i{yO`1TCpGB_pP<h zack|-I&1x2>)VgB_u4xp#cs9&wgR>SwgR>SwgR>SiK;*(RHH~FQdwD9SXlTow!!O9 zY`eL+IX*uA`1lCb-Mn~KR#s<c=hW2H?d>hnyKWc)05E{fCory<k&!VlFmQi=?>ZUR zPJlrH(RdhRg1x=H*Voss=>t1K1l?E^Q$ncs+}vDX`al3gzR|8LDk|FC+zbp&1V9Rc zrYbJi$;->TxVRvOCc+>HS$QUXW_o)1>gp;nE(-=(i0da5DojjFu;Q}75EsVMq_wrR zv4RS~u+R+2rl+Q+?(Xgiva%?!fDt>K4xy)~hZRu_?(Xi^$;sxPo}P-cvWPHZhl>#; zH^B`J4cpt>bicj5y@iE^qoX5GYHDh_y1Hs>Yw7gH#>SnU9Xei)92y!TR{iz$b#QPn zEiFwo!%uW#h$sR(l^$YyV`GDuskOD$D+h*%BCzukG1;G+oBRI$PRTSmIq7c<07VCO zF!h%*FpATIk_JawU>X332PEs9bSBCEmnbMGpp<8Qef{zA(H{u_VE!0qlxV6-g*CCO zt1H#Ut4)M`qlsl^W_nFV?I*$#F)^HAYBH~OU0t2mklJ5gU$6f3Y7<RWI{Q$>5{1h5 zYGZM#sj10p)aeJHqN2hd<BSqbRVntw9v&W4zE2xhbD!sY3<drl#Kq~RaSFSEdm_r^ z-oeby&bppO=)@3Rr@FeD9h6JJ8YnK)^h~%#{B%s>Cpzg0u!AW#rv$&UvO<sLAkWXw zf5$d7xLnu?QvMKd*4EaVo10n0U$KoviGyJmZ-?8cs;c5vT?Y@laOb0{;{i!0E1fHT z5@TQPo$2AhQJrUa?SvWqn?inm{`2!Qz3?(5EMO$7nsATZ-Q7)3Ps=RnteKe^7}4=& zkhsphy}ikH1%(Ci^73*sS-C3FXp|CBhMAwA=jJhnFDom1dU}%O_GjVj!VzICs6c3V zc-TLpY;<H~L=eU#1vlwuXJ@kXUMvXuJQkQt@2}O~-tHAq*5A?5;qREZ(ed$d*&M#I zlJ@q<wOV+-IygA^nv&adczF2jzK+1spXR;D%gYOKrNp(kA$Iol^>KHRitq354>6pk z^yuiQnw8!j8ynNK4wsHMDT|AX^!Yf*rKP1$Z)CzvDO1nR&fePEqR$Pl+H!JoWL}A8 zVd0ke@duW;@oik9^Xd5EHZpMC=JN6~Y4`6sb1Nw+IXOA;Iwo=7R9af9qn}JJKEHHb zLc~{?#l^+ROs4ORXZufIgzWF{W5W8%{tAg$Va27qrKJU{Xjst2zFJ;h#?!HXEIVo| oU@KrNU@KrNU@KrN@IO`HFH9dX^3*z>CIA2c07*qoM6N<$f?j<<wEzGB literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick.png b/Telegram/Resources/icons/settings/premium/business/business_quick.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e7e1d1f63ab711a6a5f4588aab8c8761605271 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf?4qZOV~B;| z+bP!lj)4NlzZY@0xHNJ&DRnq456Ji-pfz{>f}oBS0#jY|g*x4p1p_rywY1i=bb4e2 zREf&%p1jwn@8PYr=GCkI|4Dyl{(N3>k-go&H;?Ys&pep0AwtJ%Y0>7JJ;xtE{`h0x zyfjfRR|^?AzV`WNn7sDJeg9drB}(U8h0W)ma})z2HAE8nlir%&fBjXCkNws!k?m2s zGkw%f1+BjN`s=KcQY+cXCqs0^I_IBFb9G!9(iP&oBSNRdYHo;@>YC3#bGF}(-(b-f ztvCI=LIGQI;{mOyUOA3BV$K5nUhA(*_qr*zWcIoxZ@-;rCc>pU`DB8D#zYUM#)b#G z@8;?3;GTZE_42H<&ul-g$T5?iw)J+d&%JV%<2~!{zh8ef>-S%|xpC{Kd#F^*Yp-3n zGiq&seaFd^rppl*-+$j4r5m6%mFxB4#kDVvD+qONC~azsX6CvRwKFDd^M?x`eI^zN z7wxREn!D-hg57u5-ON!F`LOt+#@qDd<(DgW#&Ep0WYLR}zwNR0@WYC{<hhq$iu50^ zU%+5G*U$O(*Q&G6KVQuf)#KCLd{bxg$%wUKR)4IQ{a+lT$G)6fzrsYyk<q5S*KKjo m$_MpZ?&ifS{X2Vq-U0Cy=?r^UuU0V!MY5->pUXO@geCyA@A|<2 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick@2x.png b/Telegram/Resources/icons/settings/premium/business/business_quick@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4c1852e08c328ed31c591c3e5e0e49d2d886d0 GIT binary patch literal 1083 zcmV-B1jPG^P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFrAb6VR9Fe^SW759Q5<%;ByYv_ z2n+Jwpj&KgT-{QV^3FzBTHY*#4P_$>3n?pEcx-Gf=qknaDEAf$kvu{qkCM0lS5v3c zoHOUl%>8fv{byFc`Of$Ie&3v#^O}$l>#};l>Vf~P2W-JSZ(3Sfd3kwiYHCVKN>WnN z)6>(<&CTuYZ6FXhIywqQ*;sBtLBYt#h~Mx3)xJ(oPbVfODl03ENgHJs6&20S&VGM? zYdcZ5tgfzBRaF@+B<F{Pg$)f28L~oKo1dSLi;I(&(aFHK;`Msz0w&_)<KvQ&5}g%A zwB+Pue4!>Buqn^a&!wfMqGHmvn3$OD?QK@tG=gE6lanK@t4Wxfo6}Q!YilbzJ3Bu= ze`8}q%HQAL4-XI5EGcMkxm;3ph0xK_q3FcM#;&igOS5`=dj)m*O?JEe;NU<itq{h? z$B7C?leC{+US87E(@Bc1pr)oqryJ0|zP{Sp+K4VZ`FuWcdVhaE;p+;fr>Dh=s#bVv zZEYpeQcrt(dxR_x@X<X!KB`JeZ&jY+<Ks6sHw9URg@tk}3IiikAS7slr<Rr$QU;zd z0JZqd&CP^K0y~N5ATu+Qz9sS9-QD2e;Q9GELD&GEY&IKApb6{0+wERiSt%<kW0@Hl z87#<0+S}W?kJa7X%})@baKlv_8yjQuLRkpN$Hxa3$27ym#b9VCzsmdjJDbRVWmi{M zEEUMWzyQ2nTwHKl74Y-(b7^Twd1XKn6BDPVrnvme%S$fK0kg8QxScEo;ijRXfhfYw z&d!cjR$W~kVafw+`Lu+zaJXq~Y$VbN2?>IQ8Q;Fc;UHvrFgiM_RhWyzO;b}7(G|Oa z!t(MmA?pg9PA9i87sTVzlZtryQ54J!*u}*~Zh01@7gs%QPEJlDBO`Tn3{A?-&Bd5v z%d<#lXQz@VT`rW|+S*#Wkb&6a@v!AtWOsKrFE0;g62U@2vKAH=3^-t^m_>+I@WI;J znwghH+lq*Yczt~h-U<|Ow#4Y8#m&UU#l?Tc&CJY9bab>C2dbQ&o}M5LJv=;gb#<v^ z&EMiItTOcW_NGF#Ztw5!`}+FKEy;Yx)z#H>F-*t9!^8M0D=I4RzX7L!pPwI^pu}fq zXOokY_4V~8-jV2cOjJ}9W-$D1f!XWpYwWTy;;_ci-m%#Q0Y?ZdOxTX$Ukr|d2xyL> zSY!zM7%WdX{~jJ5s(cu#VUevKuzJAifq$w8{s03|0&}BmY7GDY002ovPDHLkV1mSA B?neLs literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick@3x.png b/Telegram/Resources/icons/settings/premium/business/business_quick@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d927c704fc527ccf91ba70feaa026bb247ed6bac GIT binary patch literal 1520 zcmV<M1rPd(P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS=T1iAfRA>e5T3IMIZ5aOWTl;?$ zMfP3xED5EQP$Fdsr5reRfCHq&ffFeX<U~##kYr!8l#(@(CHuY|*k$+sSKrh$&Ajh3 zGtUgZ`ezQ_p5<Py>wagR=YH;)U%#|Vi+~mZEdp8uv<PSs&?2BkKw$(76zUVyZEbB6 z5)y)ggT1`GTwGlK{(XIZem*=r+}_^q@9%GKZ_m%qkB^T(K0Xrc{1H=zhKAA6(bd(} zv$L}wA0PipxSpS%M@B|UN=p3w{r?CKJP@Fio1443x+;N-UXih}vADQ6p5-6w8W|bo z<>hT`Z0N-+b7FFGGCn^3$M`UgJv}`a78Yb8mn7cN(P3p}^)vJ`Gc&OaNP<Zvw7a_- z5fSmN+)PYN8XFs_(8^PKd3h-;Ec_;JSSQ-s+vNdNCS6fc@g3Znnwpg1q|q)YDEL}# zm6eq=xD@NWzrUxYrF|8*tgI|*gj-u%NJMOGY*JHG4-XG1>^wa^eaR+{l<4H-gaQzw z5)u-^#@p4^b#HHv!W4FJ)`)72+S*zQD2&SW^)(L%xFKDnFqoa4%`>H_>*we9`ua+N zgH=Hu%tPSj=0?Ho`1lz24$l;Qx3ja8g-F*x7H?)|#zO#Z2L}go4XU>KJ32aIQIhLk zC_gkbB;@Aq?k<-b<gu6XVnov|FE1COq#GF=95gjG<$<8!h6R>qgr+++HAP25r`Ev0 zfRLMqhli{^prN6Gh6z{4($Z3u`;berzrSB0x3#r3E=ZboOiT<HlUi+^+;9jwIy#c# z0>^W6b8`ZpzX)^8R6sz0s0<^3(#6}`n?zw$5L#MV^7HcnkyY8**}(~VVq(I{$%!4* z*}&p~9cXcJQRl3r!K$h%(GV^zEum;5$(#fs6n`!+FGZcEq@)mxi7%RX#t8b%R8CIL z^768%m_z|ouTa|&6(*mr#fb@@a-PeZn;R57c!LVNb8~Z|7RbQFiJ=Tg_F-*pjoY0p zBX>3`DoRx4{{EgIBTfu|2tl8jYHMo?3kx$aP^bLr>+4%rS10OJ3NVrpRBS|_V@3e= z2`r4#uh!O9l<s)GeSCaQPfw+IM4rjhmDk1DPKq13GF@F=#>U1xC}1V=$cG0+g&Ra} zk}68FySp28E*>jyZ*PgLU>{X+o;pnziJ2%?=>k1HJqlUDS8;JM4HKSDY;3G_++@k# z-d-Uq?8rDik`=kLcx8j6nFo@li~Qy6?2K!guHDzyr<@gB4-XI1(9o%aRDg~Qt2H`0 zD%?_~XT7W)G?J2%bb_QAM5K}FaIMR2J3Bk`9MiGFa>}!#w40HUA(sp*FWgM!o-7$t zC@tc+t(1vS9|ai}tK3F-csOg2jukxOsKFXhvtegvN4=-voltUevc0{1dV0F_{fZmD znVA`3El4$bK|w)xcXwQW-_ync0_AblXy`PAz^h&bAx8(h94f~;!KgWiZSCsniVQ`? zS)8T=0|V70pwCulXsC*~$)@n4-NC^@9}<<vi;9ZKXjGX+-c(alBRtorOonR?xw$IP z2}ah}*Ao*HxlX^ljS>pZ<OEC#r|=03N<-?D=4Az=tgK8QbW~ta<zHD@(I-F{M?n&g z6W*AraD0@-hY`k_t8+a>ri1UN*b^~cUY3@Y;@yhqe~Ws3b8|DcY;rA)0;-Ji3vQ3; z>FLPGNUj@f)+p%K*Vm&o%^hdj2tQsyp*Vxze|}%Quiq{%E`(aDjL0B{YzPUyg@wiW z`T6GNCcenS&4Z&6YB;Ju`AH*4)xI!W`$=Y+Z!H2^1hfcf5zr!_ML>&y7J>g!1pWib W|Bg(kR?{;80000<MNUMnLSTZm!>Ky} literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/status.png b/Telegram/Resources/icons/settings/premium/status.png index fe154114576f3216e631ab8122122ec0f0bcce3f..712c91ba07f48f5492bc55bf502687f4cb8ae3d6 100644 GIT binary patch delta 449 zcmV;y0Y3im1Gof`fq$Y&L_t(I5$%&N%fe6;#udhtfiN6m8Y~Dlqt)Sp#VCRXtwW2& zZ(y{F#bOjh#{2@tB{5hAT0~(eDozwMI3M1<8#?CS`o1^Zd(OS*`R&|u&pe*LIw*<? z1OngRJ|2$~1Oa`&-=9n-BuRe5Vi?9SjApY5F=p81a`_t&G=IHb5Bh928;wRUm&;<Y z@caFrv1pp+I4&Fxuh;9{ZWoC}WLXBZQmNP=Q537ys_nWPtyb%JJSLOLd_E8Ta5zk* zQZO`4L*bz@-PvsBuD5puK{%h!hr=O}NJx?dM5ECF4u+Y9X}8;LTk_Dz<#MRYe!tIT zGMmi?2yij4R)4EPp#TG;SS&u&<DgcnO{Y`bt5hl>TG#c_X!Ij=PN?HEP&%DHolZ!n zUate{R9pUXxqL=3&yWrt3lj99C3LQ;s=fvxA{L9y=X0m}#(x(C7R$2Z@%To2XDW*F zRz_wJI&&}>+yN{H75o5%P@m7&>2xgl?}~<a@no(a@j80FXoc-|Yhuv!=veni*ImbE rC=}v(J{SykyIuT)j?;hl-3nX*5h!a5mub8}00000NkvXXu0mjf#j?^M delta 377 zcmV-<0fzp#1n~oqfqz>`L_t(I5o2H&1w$_c7>RVUxVX5Fj}J35GerG^2M>1b+C`*Z z6lHyVeg9E`v9U3-3Un9Q+S($k`0?Y%h7B8Dym$e3QE+guo15E{Cr`AswLg9Oglraq z&BVm?`SWK~i~nD{b`7q=#>VF7&!1nud@(REfD1wxEXe$S|9}3arKOphn?nTS;^G7a z1%akRxJpV&>({SmVPOdk4ZU#T0<uPY?23vCpwqs6`(|Nb0aVA&&kuAOP^6@!1fMoE zxrm4epzHqr{Tmb%1d*CLbt=%5-@kt!27`6$*75Q2q3J^B0>c{^#z1Wa1qE=qs;VlW zxj;b(`0(L_f^vcaTowyMU0wa{+qXc~6DLkYlV)RM<Aectf`Y3-kN!i44gp;RjJpXF zCIEd01VCS0xpD=rfmp`Hix*+ehtk`&Z6iiAx{DMQ6~n^9fIfg|Cf`NGSU##|phExv Xyp6?Miua~900000NkvXXu0mjfbTP0F diff --git a/Telegram/Resources/icons/settings/premium/status@2x.png b/Telegram/Resources/icons/settings/premium/status@2x.png index d05da77b42d4912a4511193c40126b639acb3db2..f0e395b4ea57c6490940d6a765533e2853447305 100644 GIT binary patch delta 979 zcmV;^11$WL2E_=FfPVw8Nkl<ZSPAV{ODMEq6y`_7!i;F#ZwtgwSjcLG6on0v8pM>c zku+sD8!0j?vQcP?g$0o`laz%RrQA!C5VDZF1(VzV{N6f!nS7Ubn%Vr{ET;24=klEI zyytz-`vnB3KeYmC1=I>Csz703p}}BK<l;l)va+(bx3{mauYca&ULVr@7P_IKp|7tm zDk_Q$Y&ILR!SD6;H6bB^aDNDynVBCSAK-R6ogpD1jIyJn<2#>!f1{(L{>XzkGBPrV z+iJBkJUl%7=H`asZnrxnC57z$AuKE`P);>9HH;FANZ{Dmm_PC$j<~qEySqC^4<R`@ z8LHUWSk#7rXn*kZ^mMZKhtS;IO!e5<*kH{xK0Z$1<mBXUc_bwzWo2dkrd5EKl$3OJ zb!oL)=CioCNc2NPLjd)9Jt6P!@8~wq=mP@-pP!#FTV7uF)G9!Vii#MS%jJrXkB2Az z@9*y!2#)INYJd)hgL0ahn)*S2h9m9O)zuHJvPYxQNPn@8j*fIX9sFBcTM4|qy+t!s zR8$ax(;+)Mn`lBqLr+dlNIN|}O)4HCGBWb)?2N)49v;TT#30bp(h`B|>+5Kyy}dm` zW@cuHth2L|a0n)oiBy~ddgA)}nu3BjIyyQrG4bKyfxyAR!S?ocdc;!Z<>i4dC@4ty zJ}^j2OMm0cL+DmeQ1JNpNPOGd+ZZ5iZEb`ggWcU-dW0}HH-`wdwY5U<^z>91mPZZ_ z4rXo`=`SxY6n$%JOQX@)?RG*ug~rB4tfmm3n3&*6g84*5M69f=;LyX~Mq>iRW?{3P zacpjG78e%_`6T6*mKOemDl03A0DY62n~PY8$bXI#QYjKrQ&ag9!ivzK04pvSJv}{v zfq~uK-HMVR<BN+6{%n|MettgAF^UKQi-`Rt9H>IAxQtOFC!s$gM%Yd05}b!*Q(9WO zzrT-aftx^?0*+9bmJ(*z;cIJa;%_fUJ3l`!!St;bs}S=?CzY3%&(6-u2`6HMLsCRb zDu3yOV2U-Wudg2-9ws*q0W*%nOTxu$Hgj)#U)?90nwsc}D5rJE276QDb-C5pYr@G& zl;T2NT^%>0v?C7g%gamg420U}=V#nxq~m&HfjjcS!GVxfMDFkJ_l8TZFUHI2>Z(W{ zOohC|!ouVR_0kR(fsv6Bk(}@)nUj;_B}|KA_o}L@<Ktt`w9e1Z+0{{TMDHDNmBSYS z<^%4#Pft%fJ3AJO1(z!CBdZbA3aAxOEAXFJ;4hVi`X<?;k1qfK002ovPDHLkV1lZv B<n;gm delta 671 zcmV;Q0$}~c2$KeofPVsgNkl<ZSPAV`P0MLf6n?qBu0-!;AX4`Sn2|CtGBYz^qzvQ- zFfbF!fHGqs`3H(fm>5b#3{1%2PNY!2@>Sxl`<~jJ&f)ktUW40SqqWyw>sf0*d#`oQ z#|IY70A>I)fEmCHU<O{w0OzG>|G+#RkIiNi2n2NG{eE987JsAB==pr6?|zF#r_(_) zW^uV(47s@sd*8IB*XtROe=zJlu{9o#SF4p&DxFLwpcC@*OAuNp6dDW$X!)38u~=HI z)}QOr?RLZAut+4jh6|Ut*=*1y#$vH+p118np>VxkBP<3bolakbQ>)dZ(Fg?=3I#5g z#Uqi)WT(>!!GAF*yWNh*<56l7i^c7B8$|(^5{ZQ3^YAhd2;5T>^fRR(4u_M?W>Gcp zm^2#A!|5*uzzh9NlS-w6l7W&)Bq*_wNCY`-w_Ah3Kyi5GG8hb^WT4b)HOixQI-SS? z2)5hpuS`$DA}52a(7+0Xg6v~5nGS~oD#-8mlXq|GkAHnS%w}`BT;}un1nz7$Ga8M< z;Si-!DwVxnPcD}emc?R`OeUc?VhKv4(STk<{;U+ORtpKx(V?@T8VaJ%=R*$H6sy%r zi6SrWoeC<Is#Ge$ARvO_WjGv;dc97@0)4q$QY`c(9E@&!%mVYH)9H{;?DcvtLcm8p zm&-9sihq8;&$j<zz`(m9R}+uNFIIz$FcN=VlF!di<nz3eN~Orv<nwt}<Nx_Wz9QXj zH@TW-vk9kXiODJ)TAExCT|XX=)oK;>Ijdy%1W%_^dJ%LB{DItqc8|}Ew$EfT_n^Gb z2mbZoQP1adIxB?b-B<R$qvK4N0n7kq05gCY_!wUp_zQW<pU`aP7vul{002ovPDHLk FV1lCuL9hS- diff --git a/Telegram/Resources/icons/settings/premium/status@3x.png b/Telegram/Resources/icons/settings/premium/status@3x.png index 01052baef0a0a36e8cfae110a990e23b1165aadf..9127319e96a61143c6a317b2d2e2702bcf2b0f31 100644 GIT binary patch delta 1506 zcmV<81s(d13D69XfPV$LNkl<ZXa((AZ77~!7$2KqVG3*B`JyHiHA^dH-d6a+q)AL( z+rF%3^MOR(it>TNB(#krg%4WV%-b|+nU^dSA}^!lb*(Y>|NZ}`PUnBl<8eRFbKmP( z-9G5L&-FXk^}DX~oa;W<xi4K(|I`eq8BjBzW<bq=ngKNfYJUa{%)n&>=WCyCWo4C? zmKG5avA({(ySu9${MTsB&CS1j`SSa}zoVlgA0MB;hPrm5$;rtiD)RsR`}f-M@@h>D zH23V;v(3%T>FMd)w{LT%Zr;4fj)aDW1_uYTBgQM=y?d9g$@ccPlamw6H83!cYKbs3 zG{gdoSH^O&)PKSI^5y?kqp`6u762LRX1wuGX7}#h<9L^plu#{%`1p7ZFgiNQ0*qJg z?d@f$pPrtgxKp}Vz~$v-76AGF{rePPyn?f{^WNSbOMPo=%hA!1GJg8>DXWD%K0Z#h zMlGbKrgGE=2M6h9v$nS8A_q7oCdQ~kqU7GbeappGTz_0lwG|Z=oOWVj!o<Xc0*qI1 zadFw--)GrlvxSF;6UNY<oSd*)$g#1p+T+2R=jP@P3=E8njI^}0czSwj#~Z4>apOjF zbMxoVp9>2M&CJZGx%BjOjyk*9T3cH=E#B$c7p=ComYwqT>sRVa5uvZIkNWHE?7VX2 z3U$M(2!96oHZ}eCa1#&^Kxa`QAt8az<pgX(3L^r}HylRJM;jX(&R=_bJFOFLZf*w$ z2OOZNsE9B?kemvOK*HuIT3TAJtgLWgB$0#3`3RTdZh7<O4ds`anaL5tW+SJK=;&xp zg~z0%B#~c1L4k;T^5lt#lyl_e%a@{8Y;GFcyMK4@IKa1W-*6ztj+&dB<5UX^3v|H= zOIur8t`3nYw{G2%GYrud7HxQVnB#}X#>Pfc4BXw_#UEsUe?KD6%E}Ta;x)*6$<NOh zF&i5jEJi8$`t|FygJen6)zuM~+1c6bIFV6BkreS|y|lDMt1Eg_bP3|z*RNkI6%1z= zMSmZ^B9TE&O%0G(IxG>V#b&FnuKw}k2RDw#jEoFoA6rh;V&u`IM;rkKA3Z!g*4EZU z9#vIU@CxnW;i0J2ID#5@_3Bm3`uOpqMgZ!qf{|byu3fvvI`H=P-r3m^so)F+*yG2K zMb&vD_4V~HUcAt##c!2$Dzkj&&K)>_V}CSMetv$`I>vEPBN8etEkz;#7FD{A;Gjd@ z$uBG}E>c22fBwwL$ssimp2EVyFhaFdXlQ7dnwp}IdJ76}FjO;<)`t%tTwPsB3LiXp zfZ8BpWEoNR_^a_pg*(uP4<C})P}yV&gENA<BLB~iwTQZ;vA%;xTU#5B3jm|b;D2Zt z{@Byg!_rVhwzs$c^y!mEuB)r7At52~D)y8b@zMv$i&2qySfki%-QC?9X`P*&RaRD_ zr~LKnmqvhogXGP@a!^XfnU!k+^<yk?R(W~3ev#;W#f|ERH*a2Eo__vxoy9h?I?dHN zFk-!|tSluZ1-+S0&h!9tO^tL5dVdB62d`eeii(QDfl#_LJ&=ET#Rx_8l=y(6=xv1F zKMMU<jKnrMIT;ibgw|M()sQmf`}glsU?RrQz+e%F1AI?H$01rbd<54Dh8r=+f`x?z zE|Me`ap|zg$Vh`|$ub$Y8t9RwlfDp%SBssUoh;mBF!KEQbDTmhBrOGCbbsGvaOBt6 zp-Bmb#o$s%hD^(A0%v0JLW(7Qp^PgxTw2M?N*-&zzP=i7doB!#0OI1}<l&)&6&&_w zW@aQt!or%Gnv~!pr%m*r=XE-4B&<9;JB#n2a*|Na7Cu(s48gKLPsR%4HdZ+%@>}-u z^6Kj9IxikFj{9Q=2M76m|62iz{{H?Q9UbB&F|PL#6BA`5U?QU}>89`%0!L?DB%|Fr zK0e0hoB8>9>C;uDngKNfY6jE{s2NZ*pk_eLfSLg{0~#{$556y(Rm*yKKmY&$07*qo IM6N<$f|eEhF8}}l delta 1053 zcmV+&1mgS942}tqfPVw{Nkl<ZXa((9ODJto7<P_V-iab&;4vWvQX(d#WTY^Zl#+5v znPFl;C<6mAL5vKPGLsa^D`DbQgox;PB*~lmbx%3FwbtHy?Q_qu_r3Q&qqV>FKfd*? zv;Ot(?dL~7Qh*d71xNu>fD|AFNC8rS6d(mi0aAbz_>UE^nSZ!;o6VM%mKGTqsm<Zz z<Ky7q;PLTM+srDn>FMcvdwXAUzxVg|{{H@efB>to*WfWbJ1e&+w^dtPn?^vZOx@nz zx+%+zu*34~?eeDHwcOa)Fd8sCJlv>@3HFB0m}^IehKA&c8m@|AVPUzsxzW+lGC4r# z`1p8tcNeFhw|`fwiWwOhqobq#{{BjrlamwVy}7wjf?gXJ6%}1wUCEsYx7Fq4Wkp4W zB#GGA*rTH(xAX7O{r!DrW~O8W!)A87{p{?FNh$gG`T1E~T&$ZiC@5%gagm9M-7YID z)AjY-0H=aUY3OllYfCrx`1m*<)8TOFI{uLXiW0=*6Msu0@WuJmV9?sy%7+>l7|?L> z+Eiy}C!bpq!NNj9Le!{KR#xKg%7mJknen*WFkBU@tE<b)%QdQEaB#5jNUW}|=I7@N z7lv}Tu&@vs8mgSdbqsry4JByW7!`bCVuBGKk7#5I3JQd@5)%{8&(9ebr_-5|k|MM> z2kh?dW`8KU9-Eq)goJSFHa9mJmzS3p+$%zRbHIj%28N>Rv9GUBNC?|IH8sV%)YjGt z?acvk@PsvUa&i(Eppa8fPY=JUuC6Yju~E=)@en4+$;rX#=bO}(l@;8}&(F`iS4~X~ zZY>5sJUnb}ZpLaE{QCMD*WlaR8-x4yF*!N;;(y|T*B-CE=#PZCBO)SD9(WfGWM^k5 zAt6B+%_m?~RMhtNHcto|qf=2!Sy))eyXYdr!^3I;y^IG22F}gR@r0h9p45uFi$QjF zHt(g2EH5v6c>!wS#>U2YHfSKKs;bmnzVW4{CEiyX>Fw?PcK6|+qoYH}qot)qCTo0r z{D1WHw6Ku!VR65sx@xzYOi~Z4VqIO`$jAs23y<ho)$U^67znz@n3x#hloGtVyZedH zZ>6QBudlDXuGj(QwO2{b>f@}eEHtM4s@B)nnJ(7qD^c>pSKHImQ+`$Wun`v*r?fPE zZ0y@=dwY9QQW8%ZuOy9)jXVwnZ3kxO=YQvgvsMB3_xJhjl0gOs2d%OOArE~0l39k( zCND2f7}H0fxbId=wzjsWHl>TB+t9T!%>b>s<dw>A+7IuTVU6nR>mME-c;4E`&z~_* zIuCS1B_$=Psi|sjkV>`wj;g3+Ok+}j6d(mi0aAbzAO%PPQh*d71xNu>fE4%#6c+de XLRu#M4xOR800000NkvXXu0mjfJ3JUC diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ea6c5f2e8..8bded7e02 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2056,6 +2056,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_animated_userpics" = "Video avatars animated in chat lists and chats to allow for additional self-expression."; "lng_premium_summary_subtitle_translation" = "Real-Time Translation"; "lng_premium_summary_about_translation" = "Real-time translation of channels and chats into other languages."; +"lng_premium_summary_subtitle_business" = "Telegram Business"; +"lng_premium_summary_about_business" = "Upgrade your account with business features such as location, opening hours and quick replies."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -2154,6 +2156,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_gifts_terms" = "By gifting Telegram Premium, you agree to the Telegram {link} and {policy}."; "lng_premium_gifts_terms_policy" = "Privacy Policy"; +"lng_business_title" = "Telegram Business"; +"lng_business_about" = "Turn your account to a business page with these additional features."; +"lng_business_unlocked" = "You have now unlocked these additional business features."; +"lng_business_subtitle_location" = "Location"; +"lng_business_about_location" = "Display the location of your business on your account."; +"lng_business_subtitle_opening_hours" = "Opening Hours"; +"lng_business_about_opening_hours" = "Show to your customers when you are open for business."; +"lng_business_subtitle_quick_replies" = "Quick Replies"; +"lng_business_about_quick_replies" = "Set up shortcuts up to 20 messages each to respond to customers faster."; +"lng_business_subtitle_greeting_messages" = "Greeting Messages"; +"lng_business_about_greeting_messages" = "Create greetings that will be automatically sent to new customers."; +"lng_business_subtitle_away_messages" = "Away Messages"; +"lng_business_about_away_messages" = "Define messages that are automatically sent when you are off."; +"lng_business_subtitle_chatbots" = "Chatbots"; +"lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions."; + "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; "lng_boost_again_button" = "Boost Again"; diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index ccf1a1d7a..ae6edc957 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -3,6 +3,7 @@ <file alias="art/background.tgv">../../art/background.tgv</file> <file alias="art/bg_thumbnail.png">../../art/bg_thumbnail.png</file> <file alias="art/bg_initial.jpg">../../art/bg_initial.jpg</file> + <file alias="art/business_logo.png">../../art/business_logo.png</file> <file alias="art/logo_256.png">../../art/logo_256.png</file> <file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file> <file alias="art/themeimage.jpg">../../art/themeimage.jpg</file> diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 93df20cfa..c3ce95189 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/painter.h" #include "ui/vertical_list.h" +#include "settings/settings_business.h" #include "settings/settings_premium.h" #include "lottie/lottie_single_player.h" #include "history/view/media/history_view_sticker.h" @@ -128,6 +129,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { return tr::lng_premium_summary_subtitle_animated_userpics(); case PremiumPreview::RealTimeTranslation: return tr::lng_premium_summary_subtitle_translation(); + case PremiumPreview::Business: + return tr::lng_premium_summary_subtitle_business(); } Unexpected("PremiumPreview in SectionTitle."); } @@ -170,6 +173,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { return tr::lng_premium_summary_about_animated_userpics(); case PremiumPreview::RealTimeTranslation: return tr::lng_premium_summary_about_translation(); + case PremiumPreview::Business: + return tr::lng_premium_summary_about_business(); } Unexpected("PremiumPreview in SectionTitle."); } @@ -1219,6 +1224,13 @@ void Show( DecorateListPromoBox(box, show, descriptor); })); return; + } else if (descriptor.section == PremiumPreview::Business) { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (window) { + Settings::ShowBusiness(window); + } + return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index b7fb7a40e..9fc4da279 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -64,6 +64,7 @@ enum class PremiumPreview { TagsForMessages, LastSeen, MessagePrivacy, + Business, kCount, }; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 1f17c1a7d..4b453c9b8 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -402,6 +402,7 @@ updateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update; updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update; updateSavedReactionTags#39c67432 = Update; +updateSmsJob#f16269d4 job_id:string = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1653,6 +1654,12 @@ messages.savedReactionTags#3259950a tags:Vector<SavedReactionTag> hash:long = me outboxReadDate#3bb842ac date:int = OutboxReadDate; +smsjobs.eligibleToJoin#dc8b44cf terms_url:string monthly_sent_sms:int = smsjobs.EligibilityToJoin; + +smsjobs.status#2aee9191 flags:# allow_international:flags.0?true recent_sent:int recent_since:int recent_remains:int total_sent:int total_since:int last_gift_slug:flags.1?string terms_url:string = smsjobs.Status; + +smsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2252,4 +2259,12 @@ premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = p premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList; -// LAYER 174 +smsjobs.isEligibleToJoin#edc39d0 = smsjobs.EligibilityToJoin; +smsjobs.join#a74ece2d = Bool; +smsjobs.leave#9898ad73 = Bool; +smsjobs.updateSettings#93fa0bf flags:# allow_international:flags.0?true = Bool; +smsjobs.getStatus#10a698e8 = smsjobs.Status; +smsjobs.getSmsJob#778d902f job_id:string = SmsJob; +smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; + +// LAYER 175 diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 9716a383e..eb6bcb360 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -94,6 +94,7 @@ settingsPremiumIconTranslations: icon {{ "settings/premium/translations", settin settingsPremiumIconTags: icon {{ "settings/premium/tags", settingsIconFg }}; settingsPremiumIconLastSeen: icon {{ "settings/premium/lastseen", settingsIconFg }}; settingsPremiumIconPrivacy: icon {{ "settings/premium/privacy", settingsIconFg }}; +settingsPremiumIconBusiness: icon {{ "settings/premium/privacy", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; @@ -103,6 +104,13 @@ settingsStoriesIconDownload: icon {{ "menu/download", premiumButtonBg1 }}; settingsStoriesIconCaption: icon {{ "settings/premium/stories_caption", premiumButtonBg1 }}; settingsStoriesIconLinks: icon {{ "menu/links_profile", premiumButtonBg1 }}; +settingsBusinessIconLocation: icon {{ "settings/premium/business/business_location", settingsIconFg }}; +settingsBusinessIconHours: icon {{ "settings/premium/business/business_hours", settingsIconFg }}; +settingsBusinessIconReplies: icon {{ "settings/premium/business/business_quick", settingsIconFg }}; +settingsBusinessIconGreeting: icon {{ "settings/premium/status", settingsIconFg }}; +settingsBusinessIconAway: icon {{ "settings/premium/business/business_away", settingsIconFg }}; +settingsBusinessIconChatbots: icon {{ "settings/premium/business/business_chatbots", settingsIconFg }}; + settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) { style: TextStyle(semiboldTextStyle) { font: font(10px semibold); diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp new file mode 100644 index 000000000..23a4d8914 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -0,0 +1,568 @@ +/* +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 "settings/settings_business.h" + +#include "boxes/premium_preview_box.h" +#include "core/click_handler_types.h" +#include "data/data_peer_values.h" // AmPremiumValue. +#include "info/info_wrap_widget.h" // Info::Wrap. +#include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common_session.h" +#include "settings/settings_premium.h" +#include "ui/effects/gradient.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. +#include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "apiwrap.h" +#include "api/api_premium.h" +#include "styles/style_premium.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +struct Entry { + const style::icon *icon; + rpl::producer<QString> title; + rpl::producer<QString> description; + BusinessFeature feature = BusinessFeature::Location; +}; + +using Order = std::vector<QString>; + +[[nodiscard]] Order FallbackOrder() { + return Order{ + u"location"_q, + u"opening_hours"_q, + u"quick_replies"_q, + u"greeting_messages"_q, + u"away_messages"_q, + u"chatbots"_q, + }; +} + +[[nodiscard]] base::flat_map<QString, Entry> EntryMap() { + return base::flat_map<QString, Entry>{ + { + u"location"_q, + Entry{ + &st::settingsBusinessIconLocation, + tr::lng_business_subtitle_location(), + tr::lng_business_about_location(), + BusinessFeature::Location, + }, + }, + { + u"opening_hours"_q, + Entry{ + &st::settingsBusinessIconHours, + tr::lng_business_subtitle_opening_hours(), + tr::lng_business_about_opening_hours(), + BusinessFeature::OpeningHours, + }, + }, + { + u"quick_replies"_q, + Entry{ + &st::settingsBusinessIconReplies, + tr::lng_business_subtitle_quick_replies(), + tr::lng_business_about_quick_replies(), + BusinessFeature::QuickReplies, + }, + }, + { + u"greeting_messages"_q, + Entry{ + &st::settingsBusinessIconGreeting, + tr::lng_business_subtitle_greeting_messages(), + tr::lng_business_about_greeting_messages(), + BusinessFeature::GreetingMessages, + }, + }, + { + u"away_messages"_q, + Entry{ + &st::settingsBusinessIconAway, + tr::lng_business_subtitle_away_messages(), + tr::lng_business_about_away_messages(), + BusinessFeature::AwayMessages, + }, + }, + { + u"chatbots"_q, + Entry{ + &st::settingsBusinessIconChatbots, + tr::lng_business_subtitle_chatbots(), + tr::lng_business_about_chatbots(), + BusinessFeature::Chatbots, + }, + }, + }; +} + +void AddBusinessSummary( + not_null<Ui::VerticalLayout*> content, + not_null<Window::SessionController*> controller, + Fn<void(BusinessFeature)> buttonCallback) { + const auto &stDefault = st::settingsButton; + const auto &stLabel = st::defaultFlatLabel; + const auto iconSize = st::settingsPremiumIconDouble.size(); + const auto &titlePadding = st::settingsPremiumRowTitlePadding; + const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; + + auto entryMap = EntryMap(); + auto iconContainers = std::vector<Ui::AbstractButton*>(); + iconContainers.reserve(int(entryMap.size())); + + const auto addRow = [&](Entry &entry) { + const auto labelAscent = stLabel.style.font->ascent; + const auto button = Ui::CreateChild<Ui::SettingsButton>( + content.get(), + rpl::single(QString())); + + const auto label = content->add( + object_ptr<Ui::FlatLabel>( + content, + std::move(entry.title) | rpl::map(Ui::Text::Bold), + stLabel), + titlePadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto description = content->add( + object_ptr<Ui::FlatLabel>( + content, + std::move(entry.description), + st::boxDividerLabel), + descriptionPadding); + description->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get()); + dummy->setAttribute(Qt::WA_TransparentForMouseEvents); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + dummy->resize(s.width(), iconSize.height()); + }, dummy->lifetime()); + + label->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + dummy->moveToLeft(0, r.y() + (r.height() - labelAscent)); + }, dummy->lifetime()); + + rpl::combine( + content->widthValue(), + label->heightValue(), + description->heightValue() + ) | rpl::start_with_next([=, + topPadding = titlePadding, + bottomPadding = descriptionPadding]( + int width, + int topHeight, + int bottomHeight) { + button->resize( + width, + topPadding.top() + + topHeight + + topPadding.bottom() + + bottomPadding.top() + + bottomHeight + + bottomPadding.bottom()); + }, button->lifetime()); + label->topValue( + ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { + button->moveToLeft(0, top - padding); + }, button->lifetime()); + const auto arrow = Ui::CreateChild<Ui::IconButton>( + button, + st::backButton); + arrow->setIconOverride( + &st::settingsPremiumArrow, + &st::settingsPremiumArrowOver); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto &point = st::settingsPremiumArrowShift; + arrow->moveToRight( + -point.x(), + point.y() + (s.height() - arrow->height()) / 2); + }, arrow->lifetime()); + + const auto feature = entry.feature; + button->setClickedCallback([=] { buttonCallback(feature); }); + + iconContainers.push_back(dummy); + }; + + auto icons = std::vector<const style::icon *>(); + icons.reserve(int(entryMap.size())); + { + const auto &account = controller->session().account(); + const auto mtpOrder = FallbackOrder();/* session->account().appConfig().get<Order>( + "premium_promo_order", + FallbackOrder());*/ AssertIsDebug() + const auto processEntry = [&](Entry &entry) { + icons.push_back(entry.icon); + addRow(entry); + }; + + for (const auto &key : mtpOrder) { + auto it = entryMap.find(key); + if (it == end(entryMap)) { + continue; + } + processEntry(it->second); + } + } + + content->resizeToWidth(content->height()); + + // Icons. + Assert(iconContainers.size() > 2); + const auto from = iconContainers.front()->y(); + const auto to = iconContainers.back()->y() + iconSize.height(); + auto gradient = QLinearGradient(0, 0, 0, to - from); + gradient.setStops(Ui::Premium::FullHeightGradientStops()); + for (auto i = 0; i < int(icons.size()); i++) { + const auto &iconContainer = iconContainers[i]; + + const auto pointTop = iconContainer->y() - from; + const auto pointBottom = pointTop + iconContainer->height(); + const auto ratioTop = pointTop / float64(to - from); + const auto ratioBottom = pointBottom / float64(to - from); + + auto resultGradient = QLinearGradient( + QPointF(), + QPointF(0, pointBottom - pointTop)); + + resultGradient.setColorAt( + .0, + anim::gradient_color_at(gradient, ratioTop)); + resultGradient.setColorAt( + .1, + anim::gradient_color_at(gradient, ratioBottom)); + + const auto brush = QBrush(resultGradient); + AddButtonIcon( + iconContainer, + stDefault, + { .icon = icons[i], .backgroundBrush = brush }); + } + + Ui::AddSkip(content, descriptionPadding.bottom()); +} + +class Business : public Section<Business> { +public: + Business( + QWidget *parent, + not_null<Window::SessionController*> controller); + + [[nodiscard]] rpl::producer<QString> title() override; + + [[nodiscard]] QPointer<Ui::RpWidget> createPinnedToTop( + not_null<QWidget*> parent) override; + [[nodiscard]] QPointer<Ui::RpWidget> createPinnedToBottom( + not_null<Ui::RpWidget*> parent) override; + + void showFinished() override; + + [[nodiscard]] bool hasFlexibleTopBar() const override; + + void setStepDataReference(std::any &data) override; + + [[nodiscard]] rpl::producer<> sectionShowBack() override final; + +private: + void setupContent(); + + const not_null<Window::SessionController*> _controller; + + QPointer<Ui::GradientButton> _subscribe; + base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back; + base::unique_qptr<Ui::IconButton> _close; + rpl::variable<bool> _backToggles; + rpl::variable<Info::Wrap> _wrap; + Fn<void(bool)> _setPaused; + + std::shared_ptr<Ui::RadiobuttonGroup> _radioGroup; + + rpl::event_stream<> _showBack; + rpl::event_stream<> _showFinished; + rpl::variable<QString> _buttonText; + +}; + +Business::Business( + QWidget *parent, + not_null<Window::SessionController*> controller) +: Section(parent) +, _controller(controller) +, _radioGroup(std::make_shared<Ui::RadiobuttonGroup>()) { + setupContent(); + _controller->session().api().premium().reload(); +} + +rpl::producer<QString> Business::title() { + return tr::lng_premium_summary_title(); +} + +bool Business::hasFlexibleTopBar() const { + return true; +} + +rpl::producer<> Business::sectionShowBack() { + return _showBack.events(); +} + +void Business::setStepDataReference(std::any &data) { + using namespace Info::Settings; + const auto my = std::any_cast<SectionCustomTopBarData>(&data); + if (my) { + _backToggles = std::move( + my->backButtonEnables + ) | rpl::map_to(true); + _wrap = std::move(my->wrapValue); + } +} + +void Business::setupContent() { + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + Ui::AddSkip(content, st::settingsFromFileTop); + + AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { + }); + + Ui::ResizeFitChild(this, content); +} + +QPointer<Ui::RpWidget> Business::createPinnedToTop( + not_null<QWidget*> parent) { + auto title = tr::lng_business_title(); + auto about = [&]() -> rpl::producer<TextWithEntities> { + return rpl::conditional( + Data::AmPremiumValue(&_controller->session()), + tr::lng_business_unlocked(), + tr::lng_business_about() + ) | Ui::Text::ToWithEntities(); + }(); + + const auto content = [&]() -> Ui::Premium::TopBarAbstract* { + const auto weak = base::make_weak(_controller); + const auto clickContextOther = [=] { + return QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + .botStartAutoSubmit = true, + }); + }; + return Ui::CreateChild<Ui::Premium::TopBar>( + parent.get(), + st::defaultPremiumCover, + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContextOther, + .logo = u"dollar"_q, + .title = std::move(title), + .about = std::move(about), + }); + }(); + _setPaused = [=](bool paused) { + content->setPaused(paused); + if (_subscribe) { + _subscribe->setGlarePaused(paused); + } + }; + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + content->setRoundEdges(wrap == Info::Wrap::Layer); + }, content->lifetime()); + + const auto calculateMaximumHeight = [=] { + return st::settingsPremiumTopHeight; + }; + + content->setMaximumHeight(calculateMaximumHeight()); + content->setMinimumHeight(st::settingsPremiumTopHeight);// st::infoLayerTopBarHeight); + + content->resize(content->width(), content->maximumHeight()); + //content->additionalHeight( + //) | rpl::start_with_next([=](int additionalHeight) { + // const auto wasMax = (content->height() == content->maximumHeight()); + // content->setMaximumHeight(calculateMaximumHeight() + // + additionalHeight); + // if (wasMax) { + // content->resize(content->width(), content->maximumHeight()); + // } + //}, content->lifetime()); + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + const auto isLayer = (wrap == Info::Wrap::Layer); + _back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>( + content, + object_ptr<Ui::IconButton>( + content, + (isLayer + ? st::settingsPremiumLayerTopBarBack + : st::settingsPremiumTopBarBack)), + st::infoTopBarScale); + _back->setDuration(0); + _back->toggleOn(isLayer + ? _backToggles.value() | rpl::type_erased() + : rpl::single(true)); + _back->entity()->addClickHandler([=] { + _showBack.fire({}); + }); + _back->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + const auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar; + content->setTextPosition( + toggled ? st.back.width : st.titlePosition.x(), + st.titlePosition.y()); + }, _back->lifetime()); + + if (!isLayer) { + _close = nullptr; + } else { + _close = base::make_unique_q<Ui::IconButton>( + content, + st::settingsPremiumTopBarClose); + _close->addClickHandler([=] { + _controller->parentController()->hideLayer(); + _controller->parentController()->hideSpecialLayer(); + }); + content->widthValue( + ) | rpl::start_with_next([=] { + _close->moveToRight(0, 0); + }, _close->lifetime()); + } + }, content->lifetime()); + + return Ui::MakeWeak(not_null<Ui::RpWidget*>{ content }); +} + +void Business::showFinished() { + _showFinished.fire({}); +} + +QPointer<Ui::RpWidget> Business::createPinnedToBottom( + not_null<Ui::RpWidget*> parent) { + const auto content = Ui::CreateChild<Ui::RpWidget>(parent.get()); + + const auto session = &_controller->session(); + + auto buttonText = _buttonText.value(); + + _subscribe = CreateSubscribeButton({ + _controller, + content, + [] { return u"business"_q; }, + std::move(buttonText), + std::nullopt, + [=, options = session->api().premium().subscriptionOptions()] { + const auto value = _radioGroup->value(); + return (value < options.size() && value >= 0) + ? options[value].botUrl + : QString(); + }, + }); + { + const auto callback = [=](int value) { + const auto options = + _controller->session().api().premium().subscriptionOptions(); + if (options.empty()) { + return; + } + Assert(value < options.size() && value >= 0); + auto text = tr::lng_premium_subscribe_button( + tr::now, + lt_cost, + options[value].costPerMonth); + _buttonText = std::move(text); + }; + _radioGroup->setChangedCallback(callback); + callback(0); + } + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + _subscribe->startGlareAnimation(); + }, _subscribe->lifetime()); + + content->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto padding = st::settingsPremiumButtonPadding; + _subscribe->resizeToWidth(width - padding.left() - padding.right()); + }, _subscribe->lifetime()); + + rpl::combine( + _subscribe->heightValue(), + Data::AmPremiumValue(session), + session->premiumPossibleValue() + ) | rpl::start_with_next([=]( + int buttonHeight, + bool premium, + bool premiumPossible) { + const auto padding = st::settingsPremiumButtonPadding; + const auto finalHeight = !premiumPossible + ? 0 + : !premium + ? (padding.top() + buttonHeight + padding.bottom()) + : 0; + content->resize(content->width(), finalHeight); + _subscribe->moveToLeft(padding.left(), padding.top()); + _subscribe->setVisible(!premium && premiumPossible); + }, _subscribe->lifetime()); + + return Ui::MakeWeak(not_null<Ui::RpWidget*>{ content }); +} + +} // namespace + +template <> +struct SectionFactory<Business> : AbstractSectionFactory { + object_ptr<AbstractSection> create( + not_null<QWidget*> parent, + not_null<Window::SessionController*> controller + ) const final override { + return object_ptr<Business>(parent, controller); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() { + static const auto result = std::make_shared<SectionFactory>(); + return result; + } +}; + +Type BusinessId() { + return Business::Id(); +} + +void ShowBusiness(not_null<Window::SessionController*> controller) { + if (!controller->session().premiumPossible()) { + controller->show(Box(PremiumUnavailableBox)); + return; + } + controller->showSettings(Settings::BusinessId()); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.h b/Telegram/SourceFiles/settings/settings_business.h new file mode 100644 index 000000000..e255fd715 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_business.h @@ -0,0 +1,37 @@ +/* +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 "settings/settings_type.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +enum class BusinessFeature { + Location, + OpeningHours, + QuickReplies, + GreetingMessages, + AwayMessages, + Chatbots, + + kCount, +}; + +[[nodiscard]] Type BusinessId(); + +void ShowBusiness(not_null<Window::SessionController*> controller); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 1b5f8304b..854c94b05 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "settings/settings_business.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" #include "settings/settings_information.h" @@ -419,6 +420,19 @@ void SetupPremium( controller->setPremiumRef("settings"); showOther(PremiumId()); }); + const auto button = AddButtonWithIcon( + container, + tr::lng_business_title(), + st::settingsButton, + { .icon = &st::menuIconShop }); + button->addClickHandler([=] { + showOther(BusinessId()); + }); + constexpr auto kNewExpiresAt = int(1711958400); + if (base::unixtime::now() < kNewExpiresAt) { + Ui::NewBadge::AddToRight(button); + } + if (controller->session().premiumCanBuy()) { const auto button = AddButtonWithIcon( container, diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index cde74eb5a..f9d627d3b 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -180,6 +180,7 @@ using Order = std::vector<QString>; u"stories"_q, u"more_upload"_q, u"double_limits"_q, + u"business"_q, u"last_seen"_q, u"voice_to_text"_q, u"faster_download"_q, @@ -367,6 +368,16 @@ using Order = std::vector<QString>; PremiumPreview::RealTimeTranslation, }, }, + { + u"business"_q, + Entry{ + &st::settingsPremiumIconPlay, AssertIsDebug() + tr::lng_premium_summary_subtitle_business(), + tr::lng_premium_summary_about_business(), + PremiumPreview::Business, + true, + }, + }, }; } @@ -1671,9 +1682,9 @@ void AddSummaryPremium( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = account.appConfig().get<Order>( + const auto mtpOrder = FallbackOrder();/* session->account().appConfig().get<Order>( "premium_promo_order", - FallbackOrder()); + FallbackOrder());*/ AssertIsDebug() const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 4094b584d..5ef11ba50 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -25,6 +25,21 @@ constexpr auto kBodyAnimationPart = 0.90; constexpr auto kTitleAdditionalScale = 0.15; constexpr auto kMinAcceptableContrast = 4.5; // 1.14; +[[nodiscard]] QImage ScaleTo(QImage image) { + using namespace style; + const auto size = image.size(); + const auto scale = DevicePixelRatio() * Scale() / 300.; + const auto scaled = QSize( + int(base::SafeRound(size.width() * scale)), + int(base::SafeRound(size.height() * scale))); + image = image.scaled( + scaled, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + image.setDevicePixelRatio(DevicePixelRatio()); + return image; +} + } // namespace QString Svg() { @@ -157,27 +172,41 @@ TopBar::TopBar( rpl::producer<TextWithEntities> about, bool light, bool optimizeMinistars) +: TopBar(parent, st, { + .clickContextOther = std::move(clickContextOther), + .title = std::move(title), + .about = std::move(about), + .light = light, + .optimizeMinistars = optimizeMinistars, +}) { +} + +TopBar::TopBar( + not_null<QWidget*> parent, + const style::PremiumCover &st, + TopBarDescriptor &&descriptor) : TopBarAbstract(parent, st) -, _light(light) +, _light(descriptor.light) +, _logo(descriptor.logo) , _titleFont(st.titleFont) , _titlePadding(st.titlePadding) -, _about(this, std::move(about), st.about) -, _ministars(this, optimizeMinistars) { +, _about(this, std::move(descriptor.about), st.about) +, _ministars(this, descriptor.optimizeMinistars) { std::move( - title + descriptor.title ) | rpl::start_with_next([=](QString text) { _titlePath = QPainterPath(); _titlePath.addText(0, _titleFont->ascent, _titleFont, text); update(); }, lifetime()); - if (clickContextOther) { + if (const auto other = descriptor.clickContextOther) { _about->setClickHandlerFilter([=]( const ClickHandlerPtr &handler, Qt::MouseButton button) { ActivateClickHandler(_about, handler, { button, - clickContextOther() + other() }); return false; }); @@ -188,7 +217,10 @@ TopBar::TopBar( ) | rpl::start_with_next([=] { TopBarAbstract::computeIsDark(); - if (!_light && !TopBarAbstract::isDark()) { + if (_logo == u"dollar"_q) { + _dollar = ScaleTo(QImage(u":/gui/art/business_logo.png"_q)); + _ministars.setColorOverride(st::premiumButtonFg->c); + } else if (!_light && !TopBarAbstract::isDark()) { _star.load(Svg()); _ministars.setColorOverride(st::premiumButtonFg->c); } else { @@ -232,8 +264,11 @@ rpl::producer<int> TopBar::additionalHeight() const { } void TopBar::resizeEvent(QResizeEvent *e) { - const auto progress = (e->size().height() - minimumHeight()) - / float64(maximumHeight() - minimumHeight()); + const auto max = maximumHeight(); + const auto min = minimumHeight(); + const auto progress = (max > min) + ? ((e->size().height() - min) / float64(max - min)) + : 1.; _progress.top = 1. - std::clamp( (1. - progress) / kBodyAnimationPart, @@ -291,7 +326,12 @@ void TopBar::paintEvent(QPaintEvent *e) { } p.resetTransform(); - _star.render(&p, _starRect); + if (!_dollar.isNull()) { + auto hq = PainterHighQualityEnabler(p); + p.drawImage(_starRect, _dollar); + } else { + _star.render(&p, _starRect); + } const auto color = _light ? st::settingsPremiumUserTitle.textFg diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 9ccc4fa30..4ac3cc8be 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -64,6 +64,15 @@ private: }; +struct TopBarDescriptor { + Fn<QVariant()> clickContextOther; + QString logo; + rpl::producer<QString> title; + rpl::producer<TextWithEntities> about; + bool light = false; + bool optimizeMinistars = true; +}; + class TopBar final : public TopBarAbstract { public: TopBar( @@ -74,6 +83,10 @@ public: rpl::producer<TextWithEntities> about, bool light = false, bool optimizeMinistars = true); + TopBar( + not_null<QWidget*> parent, + const style::PremiumCover &st, + TopBarDescriptor &&descriptor); ~TopBar(); void setPaused(bool paused) override; @@ -87,11 +100,13 @@ protected: private: const bool _light = false; + const QString _logo; const style::font &_titleFont; const style::margins &_titlePadding; object_ptr<FlatLabel> _about; ColoredMiniStars _ministars; QSvgRenderer _star; + QImage _dollar; struct { float64 top = 0.; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index b0e0dda13..60fa215bf 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -137,6 +137,7 @@ menuIconAntispam: icon {{ "menu/antispam", menuIconColor }}; menuIconChatDiscuss: icon {{ "menu/chat_discuss", menuIconColor }}; menuIconBotCommands: icon {{ "menu/bot_commands", menuIconColor }}; menuIconPremium: icon {{ "menu/premium", menuIconColor }}; +menuIconShop: icon {{ "menu/shop", menuIconColor }}; menuIconIpAddress: icon {{ "menu/ip_address", menuIconColor }}; menuIconAddress: icon {{ "menu/payment_address", menuIconColor }}; menuIconShowAll: icon {{ "menu/all_media", menuIconColor }}; From 205479fcccca7ef463d1da290d821b340544ec9f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 20 Feb 2024 14:33:46 +0400 Subject: [PATCH 039/108] Layout chatbots editing section. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/animations/robot.tgs | Bin 0 -> 30782 bytes Telegram/Resources/langs/lang.strings | 18 ++ .../Resources/qrc/telegram/animations.qrc | 1 + .../boxes/peers/edit_linked_chat_box.cpp | 10 +- .../boxes/peers/edit_peer_color_box.cpp | 34 +-- .../SourceFiles/core/local_url_handlers.cpp | 17 +- .../history/history_inner_widget.cpp | 13 ++ .../settings/business/settings_chatbots.cpp | 201 ++++++++++++++++++ .../settings/business/settings_chatbots.h | 16 ++ .../settings_cloud_password_manage.cpp | 10 +- Telegram/SourceFiles/settings/settings.style | 16 +- .../settings/settings_business.cpp | 14 +- .../SourceFiles/settings/settings_common.cpp | 56 ++--- .../SourceFiles/settings/settings_common.h | 17 +- Telegram/SourceFiles/ui/vertical_list.cpp | 12 +- Telegram/SourceFiles/ui/vertical_list.h | 8 +- 17 files changed, 367 insertions(+), 78 deletions(-) create mode 100644 Telegram/Resources/animations/robot.tgs create mode 100644 Telegram/SourceFiles/settings/business/settings_chatbots.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_chatbots.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fbfbb55f2..0afd120d0 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1277,6 +1277,8 @@ PRIVATE profile/profile_block_widget.h profile/profile_cover_drop_area.cpp profile/profile_cover_drop_area.h + settings/business/settings_chatbots.cpp + settings/business/settings_chatbots.h settings/cloud_password/settings_cloud_password_common.cpp settings/cloud_password/settings_cloud_password_common.h settings/cloud_password/settings_cloud_password_email.cpp diff --git a/Telegram/Resources/animations/robot.tgs b/Telegram/Resources/animations/robot.tgs new file mode 100644 index 0000000000000000000000000000000000000000..0076344f4c799f30199f553d5402e5f8b8c1daf8 GIT binary patch literal 30782 zcma&N1FUE<`{uiC+qP}nwr%fywr$(CZQD58wrzXPH~)*7+{w&sn)H|UZL^xJtUl@U zF8nA6fd3RA;H#d^+AWDj(l=^I-Xy?2{fC~@ZWb)qY!7J?FfLlvk`t3s5J^?uW$^b~ z<EiN!K_-j_<FS+8NrmzyIP&x2-RYTBdgjkMx82{L>+M-r_t#lB{=TR%cZb_hH^0x1 zn^Tiex4ho&&y1gUncp=bzpq<2e(}ptx4ss?->)FN#;5);c>K4&ue-fIUvkgi50`g0 zudlx!Z>WC1t1j=0dH8R-yT8vjr*n9}Z*_6I<)QSqeZF6hpIv$XA~)50&-u^Se?4D8 ze?90ybymqyzWh|S2}^joi!!hDpCXhiY)ih-b>IKmkGi!V!7-K&fO<K&q!q+{$-(~^ zff4ib$@2eTo!C419*T}?cqDWW*X}0HeD5%+x_XO-*H98pN_$(-H>m0vee-51b=&`0 z&~Hkk-9Xy%(PLiFB?}h(@U2pHi&dbx4w=WL(mT_ss_v=pF=81zc%;q7a$n%1uA8dC z?wP3Zxkqq47t2g}jHQA1ng6q`?)3`VwrH)UyonP=zPqH)`BI@X+1MB8Hs$u*p}u`% ztSoigsbS-%ZzC*X%j~iHY73LXTG2yag1y;B8ttv)QfQlFVcdRCx;eP~TH5;B&c@jN z_<(1Rf6Ut}-*1o!QTK{l>@Z{f!6kKXaG#l;YIOE@b$AA}%Ux2!hfx))^-@33yotLl z<y-XCAn#~qVce|45#8>ChWo(&w$D5H8gui$tIw}Q_igc&8rq$ly)j8y_4dQHXI5@z z_D9GT-<EI9$`98!fq#x(g(oFe`Myr)k$w|B?7Gs|wB|HT-(o>4ZPVugIF3GYlb+|X zZ|D2#`;wpU=TA&xeXe0$M}YcxM@i~}w#VJ*;%P`w)mx_vL<h`Ms}4JswPjjc*wNsU z(Z-Vxl(+W&tsQIh7*(om8C5A(`*-c!>~tPN$9`BcuQ~;;;3}<tTeG4fpst%Eh9!Fm z?yVuOQ}(8Ag(qXrfw1+T0o*VlKka6n&fyk1;wS)JrE@(O$zr_DInr0<*IakZ_N|R0 z6QtpR6-(OS=N85{Z1iCvkI8*+ZA;}7Kw`=UJNu(~(vgX!YqrOxF>&;kgLy|!H85^` zZ?VsA8OLZBEQ`O*FPlyjnyC<RZagT~74YraFIqsyrca=@VT39-(8ht&$ud?H&tek+ z)-Yj0k%Twl-r^xUlgfoCUdnFiOnIo!9%=#>Rhd|kQ>mpL(BQkJ0=yj@3(XauF~7xI z{I5pkOzVpSQ@m)EmxL!f@Yhry0V}X*K!1W~xLrkbTtHa641^LT4c?N<(U?%g8UtZ_ zBV>@1<_#84$aU*dv~VmNUho=hxE^I&c12R2xWp<<vaKvrqLpcxL<b2mE*O>MU_BA6 z1WALw6cSJA9Ygq{$zp7<pGImOUeh()F7dju$(I^i)wO;gkftGP<QHhnOV$g*uUr9n z&5bziQs1s7+20A+3qlnu<FQ67plS^({k=$yT{RHnyx}WWQid^G^3dgpW&+st!%<L7 zT)a$iiN<riN{wq*Vj2SIxkv7^3nDPRwXXu2u}~iGUdg78T@MM4$8tNoHyz$t7mFu~ zNAeeq6SS$+P+GND+K#Ab7b3B6?TQEsY1xNA-DWtkT^6c{rV!6W<<#7oX#gWneG{-t zFKKk0NY4*!GOX3f-3l`Grbe|DCODQCj-%r+;g@OHNs+#q3w54svm{v>&bfYT!*mF( zU?`nl2EGsSCRPwgp>u>94exxF8ix)B&n|E;mQ!Y2_{EqPJ2ObIZ1mb1(d}3to;K<= zXSFozZCE1s9Fpn&TpG2eoz(P!1wr047tp0FhDz<b3a_q)&UltKu*@J3=?dl=J1Ux# zIh3K((yu+jZVLr0K!I{80^q{5wcDMlbS}@uL=CyBGL!g+&9trTX-mSb(wN0@_HZS( zX3KI~(qbVlUIsGzC)^h-{(Ap@x8*rFJa2}%@&8_b-|q4`B>|ZE{2t=6TY(je!fcF* zCa^9_&ZvYY@~?dyx_#~H^MBtX>pfjhXZ8B{jp4J0T`_q0l#i~MC>OK{cnKCTLe6I% zjf=cLAGJJC{(32@l^?lPX09yU{?h+G;``mP{xtgi{yabW`SPs#=6ma>>K_xY(~~Un z*zU905yS*4)^=W_XJgO(Iv!t2%G<IYZ2IMHy<C4wDqe2C`uTotp4Rzwe_pPtJ=yE? z{~k`spKmX%EsjzKS0I!Mr9&A~{qN!AGK^ssFp4gTPr>4`t^z=Vr2|C1iPbnu!QdXw zV4kwRy#L^*-^l+OP=$UAqCk{sSR(@39zw(WMo-Z|F+da)4OK(M;9e9Us{3P)9u?o8 z#xr1TG7A_DIjYoPGN!$-ZNW!&gSxi7z3?D1lA7xO2E9VDTqfefp@;;(PaojEV#!l) z7giMQdeQ_|Bw12n8jK5n;gY_XI=W?q*`N$0O%piHAN@Sv-|rZpR82r>P#V;Fm42oF z1MIRGbik58DOeDH8-xGk4Tk)_Jt%jBpyyg=OUEurHqu)z<h0ry#-7@q|Gr!4V^K(D zsy=HpJcy&C_PAt6nin2C9n|Zp>YK8_F_yXJef1wk9lTh(DLl2X-!b7?-6Qy=2gJMj zFTMHHzeq5bQj>-=30jp%Ho3-rT|J$-d#mB$mYJW%NjXof{aK)s=Vc7`ELWUUU~<_+ zwJ2uDJ2QfFg{R?YZ(DiMCj48LxfCvOomIjt!arl()y<SR@9Jo!4|nGSWsS!lNz#@c zBnQ(g{9*%lqJ`BALxaiIzG+vjGM9&666jc2pvoY}DzOL*ZyIzr!8F@oq^nFA=W@b1 zN1x;o$45coO);$Pd)3>Hz3H7<f$tueBynPjH@`#U%zv~@I{qhbk@z5UmJe7a4-ms} zm0aL9eu>P#d*2_~As4{wWdiD;xUHCIHwQzL2sx2)Hnd_-V6bqz!IcDxBY|7QZMdp9 zu_CrXb>R}DB+;?5M3qUdMdCExGOCCz9ldt02~%50vNorP^wJQB=|LZ^0PH3k1rK)b zso7cd=L+TRZUiVn%4lo8d-RPM7vwhD1yw&Sk^d$}Nzl*t2x?R>Naf$&@&nI3ZV;fo zsSS1GY5HL*LVU{r#SPAiGeNQ@moOt>eXpovZ^)@h$Hw~EE#ri+>!-k*CrX6#>FhcK zskb)RE?j6)`Bzbo&d>K9Uz&M%<m6}ZQ`ZR>2Irrb`eBX3)(Z3mfHbsv_2ay8ttYSH z&FM&UXYVN_9Tuc>#E{!32SSXUf4T`0PCpMHo;Y~_%GZ^I8=JNl@Wd`vc1X=9lW(fU zaszOnX?iURbzl#AXT}uA_yye+!@CoPj)0B_PmKz8J6(jCZ!lWw_+TPM_)R&;lMZER z;e(QsB&1t}($F75u)MpI;ypFOc3?x=P~tp5hSdxU`()qwf|J!?>K9G?F*wk2WmGw= zhThkr-})fxc3_6g0{r#R%AWKLaX=YVgb*jh`Jcc;8$qrVRLKL5^W>V;%3BtAy_%J= z0KE|qKpyI>NQM=lCEUQ9+PCO!X8BFFJHq>gDn1JzV%Yrx1k_=usa52(Aws$nDCs<} z*m3w~ls-AI@Gay3JyM^-x8Q#PBk=zEHjhNnZ%4&SM#~Dv_OuVP)J*+#=FL62>H8ds zcDSddVhzz%6x?g2CfF@uD(l&Flz&*8N{`th!^+IZ4u>ADICy#dT0e`^bV5BL4s3AO z2>I3A?c||4vTDKIostYf9-rYsS_Ld0aaPDb@(rQ_Y3rbE0uo3{zAzbs2hxIIZ^DAL zbL-hr92EOS0n=9D+Bq_Zh-FC0{iB$h)eG~m_XZK6aC=G#>EH-s+*2tJ3d5s_=q52d zipOVgayXsdPLK|*A={k-{o*B>n(r89{W#h3btO7<6Z+5@!ZoY&_1*nCx^mUT^`-cD z$}?}0TNP!WT|ZuQ)L2bV)me{!1Wi?u1LaGaa77wbYXP3fFI`HE-&`JaUu0DOJPRf@ zpa03H38}a}b+)xuh})*{qu*6Kn7Ax9%b<g=m}trL_BIgpZIg%c@=@BlPFg_CGr6rK z+5O<$gx(};a`utUisn{LbN%UNN;9FDcx3<TL}&g>X;(ANVLD2wQDsoWbeN1;CQt*^ z0<}cNESKRJxkx4V`Qo^7?XXO3wl%m3$Jo1$JqvCg2-awZQ9u=$Pcc{}j+i(mC2W+f zVd_=<hd<snquQp$gy)k-*G*z=Ps1ehU>7F$PNFA&d=Od`4Tfa`Fe0t8T&SK?LXd%! z&AKv_2Surqv;f>mAc85G?%0&^+(|wezmyzBOTl8_Rsfm>p93FH7&^BA^eIgMdVJsH zTVTWLpbp7|kh<~Tn(!es3Ry@#`9%fjRU5=q4jgYA-()zp*u5oTB9%G-PG=EO>t853 z?pb?-vUYB-K<G=Kw}|&~zY0gf3Gm1A6n;XAaR0p#gahSF=D%-Mh!ugCVm}VHE6NDm z4R~@Is{J*AZ8FCG+BLW!l;<wOQN!I%7AOFT#D_n07|_nsjouN;98R~uJF+4Em2qOs z(%MEwpa|rD^8$ouHckH;MvNTtQ4ZL|DgN^U!^6bt*SU{YeDrgpZH}Y$5!;s*6fNo2 z`LSKp+Ix(*ZHp#n?$xoWlOuk46IeEwBq7<j9N8-3$Kt{GA2F{eVylDGT6IA*xfpxs z)FtnGN5@2O*XKHxq^Ag62d4I=W%v;KqA%?!g_=Zv1cb}J8Z&Bi+2+rE!j&W+U{Bpq zF;H&Eqt;-<t;)DOhg7`qAI+Y{m)LlURYES2)$A?Y#4Ua)S^`;|EYg-{hr1<xnZC6# z^Fd4*jkX;KER;de@{CQnx<wm4iORXB(z`_6cAy~Nh+z=nP^7~RfIY5sNljE;M`(Ov zJ9PjjbMY2nGI5vgVS_tGCW>d!7$duF$IS2L?V?EipM%Y`d!o=}Xa&C!+GrqwQS@<C zDx0;RUQ;bH30pvf(&RS2G$VT3nlw3W9zPZaBCEZFjJ<y#VMfFaJ!vC)yf^$To|}e} zE#b3h$Kua!h$Qb!u7b&Vneg8;nlmeK`Z?zBmd?-!MSR4h1I+MQk>M)$zIpq&*z5I6 z@w_Y3H0lQ<ZbBKHLUQKh;XE2J_CJa<0nTSw1RV8rMLsMHP{Xz3ya!CfOf?dDobmE# zG?S&_V(59IBw<GmJNA=HlrC|n+UYRNQm{RNidGn9RK=l=l?Qo&5%ZqlO^L0;X7?Vz z#a0e76DHWPASMD~IF*+utWS;NNC)D;#b;g{wU(F&Le5k#Ev?=ONukrnU=aidBq^Eh zSGZ!9RJ%vQxABu%Rrw4wh@PlALn;AJvShaK=_r4yk293YNjYf9_6L+3=72NIWA{N> z%#>(=RHF`$l<<L58@DKO9|{Jdp2qq|0w<cq{!6z<B~?SW!KUdmN2o5KW2nm^VI>z= z;pC6}-TxE4HbOKN>8Oy{?C6&c1t@&%9A%xJZYns3z|~>c`B#XHnRFl()T*r)dihC< zZ6{D_v;uA2+fq3qV?cQ~nGz|u*cF@nGI1Do#-`nUsWixpAglRCDF*%q^H-)dtXvzn z;Gy?Yi;jlQKut%jxz?uilQGjH@Fc-zZ~8Wh{w72_hNp+kW`$XCN=G>s)TJfN9R=EZ z!_kv*Q~HzSYM2`vYs*qtm#PdlW$-$*whlJyS;p8hQ&mJNh-VC_C>X3UPp7%L{+l=9 zI5~BOhc_b_6@B$OTdEDK)6sFQ)+!X{uKs-m3+w{eX{inO_viw{LPJ!s9mvRc{ce<c zqL&=cpAA(DI`jz~D+!@S@-HRljoch_4@+dWP$rZmRdGdp>Hh)v=!!kyEdHt6Fn}1j z05MRkS8y!Os3h1z^PRvl(Z*4Y7-)NF3-6a$#eBv845M18<ll<|#-w@;6HhG12o%i( zvaGO2uuLYxmTp0?c)G}oJqi9-*#8r+7%*zxo>aCp@g|{Z0iW%^vaDb;P^#h-UW&Hy z8FLKN(n0`XDCCbhQv?9QOdVJOQA(5^X+ZVAhm*Jv13TiVK%5PU-}n~)UrOJ%IC}n( zPcg)28;6Yn!gvnZ&KQQmGB+N3>EBuPGEQ}t)5)sYj)Ti&H;~HGiKb~{leOn*;`-F` zcYE5)$-gzL-PH8Vw~@CUBwBWm>HDckF(S3Bqqz}<y0c+(i?ZE4)70`?*z}~YlXOns z=T5ct2wwejkB^td@x7-i9ZpAtK==%Is{lCwbGUx)2Guz`n92t>rL@Qr{yWeLCAcC2 z>n}^w#$yt=%BaRwMe2-FxW;wG%Zzd<Eai)n9ca&*HytLz(ZEPz5HP8-K_QJ-ER51h zC>!WPQueHmlNuIj!#w*ii{38@1}%$V2i^Tar8w21C$S{!SyX#&oJ3FeF{b*2&@zp6 zEZCbkLN(*PM0}sd(l*DN&%<fjTt%moy-IlGpOIBtsNx<%@Tw~`-4Mwpnf;P_e1Mr? zm*{Pf$?S=t$4q3+te->7wwW&oK9~*=q~r3UX!|bZYOyhwg!3etdSc`p`o8H~%-Bb* zBG?O%eKm7*PaF+^@G=(6y<);$$VnTGQ3Y7$x)7_KS1aiIjail>FmJQS=DqikClr}Q z7O{n#{{wW}v5W<31ex@LXW1TNzbk=R^&%GW&rdeMeG}gjB4!G%7IF(k+M~@Bn3;!? zlL#5yC%UiWxu2lU;R_<TB_42~c3xrjNsv+{7D@c|iN}#G`9#}`F8k{YV9sl8iC)Bu zV2z{+FH`E57>eJ8Jy9QNLXlV?5lV#lzlM8aLd;DaNMbtfiewGem3QqZlwyq_BiJ^L z%?!0)32tz4Zpm=ZLAdzB#~%dup&j1*^dKX+NKT5A{~0E8*W!2^FcX3gF~#BBb&R}e z5hdji01~YrXu~{yrWydRfxHpkBfok8fToQ=zX!Fax1XE2K9nBz1(#jaYhtXZgx*{1 z$u*m+DY^S9DPQ5KsxlFBD08j}G#tD`T-!u8m!#J3&X2mTGAb`Fg2*fvF9kHAly_%O zH&*iTx)gu(dSlqepa1_$tAhWJw33!7LZ@`9qG2?RpfsuquOVuUT%-CQ;Tk?<%}(4^ zuVEOC&jKQBhR=$R1DWN785m0S9%KYrk2hC9D?o&+Is3xAx>zlLy>@uTruVns5Z-bM zLNJhjqYapfM+}{!@s240bRiJw+CBgVbUr3Q1`-#c{d;15^fWFl0Rg5XUx=sJld_V$ zA_m>7@QZ=BpgwRA>a!mTl%8~5=c}e3hz6@z!aAa~7A6vhV+M+slpFHAkW)K-Hr9*0 z3>b<0u()GW7VZ4r!n9p!m!`a5z^l-VTQWBp+O{24EyX08y1Aco)+&KUKf;)_iBUGF zrhZU61eGR2su+*9wAmy%;sq9GbbK_S9Gx@C0Q`J>4eXu|pqMI>=F4GxQ1-|CRS0bf z_U7LBMnse!<y(O*q>&Qk@xPiEg|)+!Cd&AxoFsI0#w8xr_%8&QP0RWtAX)f+%9aMo z^1@c8t+=65fE&QAC6~SYIhyEt{~(h<CBBdc%4GTbp&~pw_sgcJdF}9I3}s0=bL@BX z#HjV9i(G&CfSOx8e<8fHUwIgcV6U>d8`~!Hy~IUqJ3+ad+vPu_vnBT5UhgyME@j|* zyjDe;9S?)&pLe}_9NI-PZQdV)rg`W1+5|gi&|;}?-t4{SL*Rgk7z$266d`KwZM!>u z-+p|4pO26HyT*mxzaRJVXB#SUG=u@!YRKaW!L@50G5jRxrnKFCk9b>8ngHehX1r4@ zTm{sxF2Db-B;prN33=ShM%MgB9ahC}SAJ1Q%QiXwU3I7hS_g%09svBTI#tOa5vSE? zt1$eYs`5ug%7^nSA7PClL{oF;HOC=6-?7)zFN7rr&BjqcJvMu?tc})R%y?3oI*n)5 zyHHsnmJFdYY;qp*mVi;mgun5|S0>IO)@8%1lofxDqS>HumAjNzQEi`!qBt&YnW8nB z9;YS5GZKv;^&x2`3C*1@kqS!UzSu8#+(TbmT)IppbbR7^62)TFhHuu^y>NBI#JJzj zydeW^G|R<bv43g9p=pGgl>sF&GnJkz<1S-84<j_uU|_U+=AC0>ODQkY$a@a+#|$sp zby6X_16(?1*}yfIn7?Ots~T?771^-2oB|*pt}k0_gnDXWD5j}an@_@J&E#9X?<Abc z2xW$MhoSPkH)#kMwWBvng*7zZ5#Hy5=DxtV;sJXN2J3jWWE3KvW|Q9*G*dUrP%g~* zgYyUJpV|)<vx?e{e+DfBe<F+l{kxlVe<?_iphfrxb_+iibpb1G$xM9GiFcCQs&I_6 z4>oasi5o9QNmLI%`c22(3kNnmo}%PhdO?hPnw#UWjMAEl_P1e<!11<hG8?KhXad(b zrn){U!0^4ZM7%->dOr$l+yaUKUEvae43HqVDx|mjZFBDp0iD8hKEQ(z82MVX*@dhQ z0VxnNtBs2Hc@HMm*S6(V-Me1DklJ=t4d++arY+p|(!y{GM5N*lIGzbjy!Q^xMs_pE zkbRMi&Mm1RCMpB0<hp-66z1rnaK~Amgh}USBc0BLb|u8kl&Yj6muM*QPf=FP?Ab~8 z_Dre?8*j8*=b;UL$)sbY@-SZw|17SpY43-WG^g|`wC*a<&vZy97pqt#C?JgT(ggF0 zN~RgfCGQ-qU=)xgJxf~=BoICdFeoi~?jjm+K+h3V<e%9e`OWWJ;<^i#B(A8Rxa^2r z5*f&*n>Ldo1?Pi^;DJDycu~UT>l^r`q3FyFrdyvS4mnjeE1dI31oWlNTCj8Ji)<yp z8b~1Pf|kWE01)CI)6^HMBpmB>$xw<y@gaduD;FQPP(0~pGQDU9LW!$*mxJR(jX<Tk zbf+1hcA>|*%^dt_g1wQvbMGk#%HgGkCW{{|3;vkD&1XL4=#CHsG4?AEdz^QH&P{;- zwQmu*(aazm2%#FG07KHTa3x_Ws0av8U9_d9_@~n6L8Z}FAZbtRh}#8}HY4KNrnSkG zXAT^M@mqmeP8IdX5W88|qSYCp*gC;)<1q1k_t5JJ$g}j}Qp@xUlLS#5;bf=Iyu=SN z`_hy3YbfAZots;X6E4b1oHDMCKEuZ`SGBoepcf?wq;owju84alEz}OdZWX|c05un> zj~pF;g41g>E^<hx%w?K0n(JQDk^q<0!Btv=P6H2iTD!C)U~|2K<S-Tk#@kLUAzc;C z>7cm3OvRDO;2Z=W@}`vVvQ*(&*2oXCquk^?qUDl;Y^vjL!9xgN!Trd8E~qg5<o$gw zp6?9O$+kci<9TRPKIx-|NlQ?Wz%Hg-V3OwwT%@zROC%{8cw}HJ4XXoAf|26_bPfSf zlS<%u%o6eQuek;gyQ8K*yGeK^&D`j`ns{)Y24w=7h21fpMgjv_101fjkva)(<BOSi znjAp_&r`X;QXQhNVhc)O?K8X&E^L(1f#pX{MhqxM4ua)u)Mb`4xFm=Y?s2pf+1h86 zQ*BVmwgMsTqs=9_RCpsSG%OxHvxDRFZSU_DLb!|s@=o7EO(q11xE)7YcEDd{WR_8> zei|z(<AzmK$Mp!kLatEh)cymap{R}O|5wld5`N|hKW$OAp>1*l-E}lv+aZ=wB3=z_ z<w)pjR8hXMP_`)}fAWtL_8@dLQ5g%vc8z^V5>MG$!+H+R?2*O&gpLdUz<8t1{{iJ| zsC|v7VyLW$GY}pxLje%Gi@nmu=e4nnb#7CnE&k|Oqv5xfh>>6+gsP&2AQ60l4pbN} z6BzV6((4AD>fQ1K3}rQr>fr1+K`^`y#s;BM(ON4Fz}~vRN=u2r1jh!p8|8z-OIQoT z7R<7OUk@;qomy<cFoswMSCF8ARU#QL4JL4;buS?vSsA291r2|b8pI$6Px#KJmi#LK ztVu`R3qM%eQsj1KF_MXDtyZnsU^bGOhi5(N;aL+eOh>s=t<hvO(`7s}3u-2i*ECIe zM=PNum`lTS(eb`U)}NYQOP(MdZmgu;?5>0XmE0r7DUfI)k4vPODq&0Fv0b-_hmdG1 zPyM%=<1q?g!4CF$i1QSmF;qS*!aC6Q537`n{IC<M7>-+Dc#_btRI{4DM#PF#^9rLv zVGB~tAA@9zn!UEbT_|7bxBnMJtVy+~0e+++R{VZ9yQl?j`u@qvTzd}5LH6)yGHzrr zl8@-oL_c#wyn9@eKF50GOgx2q%{qqIBKtajyKq*$+K^%Mw`URs>ArwrZQ7DC?!5TR zSA0I7f6lc$d*DM|U>B|{ROd~a;Og(!*YzRIMyIZ*_AbA_?*^y8ukQiChxrM@jQa*U z-B@1U_G9z?>8)Ik1Qgz_H|hJko`P*=6$g0n0KvKO-+N4XyZ+JUY7?FAOAC+rolH{g zC;$ip%muQ8{6M+$;_=u=gecy=orBkl0MnSyywb0B-PU#eG&!M{bE<AovvT<JTwTz| z%J9&IIm$*C9%Zeb=bADknv@04&?O%gnLb=?v0Oy~HBWDD0SDy}c$vC3zNR-tuglh@ zBzgl(EDXf@Jy-2eN43L{b<`U-oX0EOV{LU%GCK(0IjtSD)R`^TCE`S-dK{U(JQxM{ zH%$<;QfFkF%W{V>@nyDbhxhQKu<Poo3aplHnhw*ft%s%%jAHQashK810Xfy9Inf@$ zBMn=ps97QKQ|-7BvbL`nrJv5|e;&ZR(B`va@u4=mX@*50;ls2w@{IO*8wE2h>ZZ&g z+w#$K6c+xa!09^o-%!g4HDcaVkFsGb=7NFHsNBW{XS^`PR@@$g$v^F`#gx$Kg=)vd zIU~^qkIj#v9JgH%{P<d)&NLh3aW0jmb}#tLA{%&aYgZZLce8~=9~qVPI9uM2GJ0pc zsgCiJ%ZBF3II#{)R2<@#=J6b3pbjYOe<sNN3;dIM_NHJ#hAB;as&z`~4R#_(#u!6Q zl1Dg^OK}tZr2>*bwa*0A=G@H*c<D4H(<3)6Z&c|WvNM)nDLUlxe5@s=aqE+$uuH}c ziWfY0pt`MfgSBaY&6>9(f}J_!jo_&ACn^Sek_!3U++sSey=!L~G9|l7lUIHcboG*| zWpWMIxI6kp-s^k%WURVj*0U@4>0Q@oE0puR1WU=94wtlBXX&WX{85)-bUbfpY1T&U z8|q8J9SUc;rMGkvwIwzDXgCg52=<zH<m-=*;9e2}{pp9&U?fbbNJ&4wrV*vFuqqt; zP^nbQRSJ|sr2Y@^6eC=+d1gAEI5dZjAD~BN@D3zQLv8RL?q9-uQ%lz*tINA-`iM>U z2Xm+8rqLULet5@M>ELEj*XgsnwhdTiuowb~iX>u8K6up=Yp<CMOo+jOWLPa>Jv8R( z8$2Z)ytqCK5`c5@D&pcmWglCZDzBysrLvn|<y;?KMH0CjWWLWk&inv3zW}Yr|49q1 zo3vO2Kt<(_yJ5;s1}7e{wlSK9kV-UG+2>V^YDij)DzB;zrCcRnDL@L8Di5f9y8C*6 znAC*_LKB|D36N`Ph<nZ1&C%T>500diaF8QgGG8wcpSycod(X=rIm|qYk4mXRqJ$_V zQiV$CJ%}h}7PEU5)%)uiz@#w8zTdAlfS|{t2GJWeKxML6i7bVwB3ZIThRRflGD#{! zg+wVqLX_xVr$8}K0+a|9LWRK5CNR-0x)^J2s3LB80cwCE@mNZ1^Rk190TmsjpOYP5 zp2D=EWkvQ-d7(cTQh++L9SvN7s_gMENNjs1M{|pyXPorB8my3-J&1M))aCZohntz> zk87Yv?f@A+R{XOn1Md*^h$iDL=Zx|AW>qo@%|aqVFz0T)6t09>OPR5t5WPw6xUQO^ zs+dLfSd$_Dcd(HmhkSUt1j!1gw7*Ij!6k8E^$S<<GY~Esr7uDtPiK?M8V;wPPpta$ zqD(@Y0%pD!F>7#fUts3sN^?X>1A0+EGR{NZ&G42We=^PfebYofJy`G07slny_p!c! zALXPcfATXTf!eI4AF#|Wxd1HgX*!VFnKl!S97NE~p21Hq9#Mo#yl;CR1N;D0DC4yV zx~%FM?+P#U<cHX&bbuPD7OI76{xd014T7EofU(JdXEDM)jjrB<*mu4Fb>gq8Od|!S zp~Q3osJvJLqpG1=h$gC;YWg3*z2q){><1^nZ0(8MHRiqK8>3+bU~E~^j%?bowc?m? z`UNrZq+dtQ0!%Lr!ISyB#)8Ri<(u;QY24?rQ$O$DYsn<>k(?y)lC%V#$+78oPI}U~ z%x!1ghjQ_uf}||yhZ>o9?2ls__1pxOG}+-6eo?mOj1KrtuaPnVU7<p~4Qkty>dYSh z=#h!dGd}%4kYYWv;Dn|%Iezh`+DC)nIr#P7h}GV;bsK}A*Wdgx%^7>l<nZVspfqq} z>xh>n@KeW9)r~uIx)QK6JtO8c#1FcIM9k+4$({IU?jvsT>mxe@sbDivm|}f~{BPQ! z(|Um$xj)Wq;#E7pO%^TyC(*?KQjJO`+$T%fge%*8Hr%-1y3U{RIcqKjV(%<+2T{|7 ztT%7A@1dNrLFS17CNdz3aRpujDvY17r><Jp+`Tl%!qeG*cN``iQB;(MC$6N^daqok z-YS0r?@c(XY^NaQUj)Stbf_SeL1MgKG-cdQO?$r#n;|$+TkM@_814c3Zqtqa9X>N| z<ZQiPoEb9m7bQ|wyuD^088Up|>9ojJ_@GB8`I?DmD@#T~B*d8f>pYN*X!d!OfvM~y z3=*S)Va>-Ok5pI6k5oJHeAcm<)gElPD>%<0kKLY^ynRPjDR*G3taw(nb4p2ZfzN8# z?`}RyDGbb%{`(a?bGa(IzG%7#7&d#hqY%G)g#Qx1s3fR?B-a7$AB^}Ifa$x;mw^I^ z$SjyudnwzR?~gT~`TyOe=d_sJRJ~-KG&#>!{p`OXffbaLtSH+3Z3S-i_u>PsAHN5M z!+vX$aI7^FOj^H}nZ&o8#L*T#Q5y{fFnD{|^28aT$JIG$L+8tq16L8^A6hZ1jNJ8s zXnFoa;VXyQmxanddibUIdD)jI33^p`yRrK8iJ3AW{#NSaBFx!nt~<y1&XB!3Z*P3S zESL^DkmupKdPVw1Zeb0MJeX8jwZ!TVBdcxt(6!Yvho@-0{pjo}%s@ElJ}6>{DMg@; zb29o>-DzDyZxj@SOK~|sI+a9qy=K^EQHNOu{DvYe4Ub_+pU<9px~yLykp?od!Hn4T zO(uwU@km)W2oD^d;gn@WkQ^f<?nC#SvVwmG`c_GR^O9|%a+_`t7oy9h)l<9%-In%Z zM6pUDWvqz$Vup&g08CP{nm_m<O?71p^)ZUu*6czLC$^mt&_E1Ge4~}I-BIARdiqja z`&8powsBC~oT=M4bIK}N2K^CH+!RJoPCt=a1FrwQ^=Pw~NQ{2>6zb?Z7OCexwmr1| zDysmWK{w!x;cPgShjKJsz>e^jch)yfS<uTU8;_GxgTJxd$xVGh;$ppVB9^AC==aLq zbBLqQczMKai#OY6w^%d~6?x8c-hM185CIPJX%9lfNmHRRiyw%z!#(1?ndaXL)sH(l z-heI?`H?i{$x&m~RC65+?NglR2d=kzy&fE|jl_dCnww-T%AJ5v6kdO1L9YUbQG(+i zx(V3YE`K1}mr75>z$GYWrGP4uZr&t5DOGn)P#ggT=iz!=gY0=oi`mH0#A5|>fnIq5 zsOTa%Q*w$}X=EOZ*`dJbx{Dns5T!h*RPiGEiN+0dr_LGiVT7biTzd<wh%`iej^%g8 z{@x#ci3un~0E?ptdS!cCLo|_}`8;&;DKy{U5^1nN8gZ(Gv@3Yfw`vrljjF{Lx9QTx z2w|jei?otTvoR<hwV}50@j)Ay;qwRV`m+uebk-+H+ZL#k^cS~E4VQ_4k!q@Dz)DfM zA*y^Dp-TtwGVBwte1SmOx=DmBo*5Sy^k%iYwqWtDy`UkMXwe(WC$4reI7^I3kNv2D z>=5HZ#BQi!FeczfxZa@2-6pHcWzl3ne_hLVE{-#TQ<3GO%;S3tzu~;{Ny$(}uV}mg z4V`l3lBgtd5-aejxS*ec9K;pG4a+pJwChOMW)FZ=)vetr^nIX;lm$Hxf?Z+(B(b2D z_RRC2v65ssS?1e2=%o*{WZtb2^7>J}zfEU7!Qzo&SyO~N+**o^8{l%W<#7x|=X&Bh zI?Rcxg3+A}kA0Y)NFL?ma=GJ0;zI|MP(XZf;|U#4?ToE67aPIiSvJKI)#C*kR<}&1 zDGD0a;m0Frs@6!vlWzZ20Z(?P9<t;t1Pe_XX%HpCZ{{Gf)<FWuJ@%4X+yl-1VKFTo zsCsVbcHWm7-V8q0h3+*W5d>_%cQA}%faDIzsNu{kk!vN3EgIwE{E95wXX$1K5OTR3 z2wEhkRR}J+i{Z{;nAhDTA-g3d?y4}2M~StLT?Uq0J2e2lWA<(EGmW)8ns(fumx5ln zEfnhq#p1#Bt^&vb{96$K8-?Y01cRX4G5j*~w~H<RU|tF+bT$=;*3e9Kkh!&Sv)5b@ z(ZOY}#6TQ;9~DK;DCm>W<2*Ip1F>X{1p=EPPq^YSHwJ*QtRe9YIG%aIlQ>A4wv{{x zJhIC8wvKIjUQ<tG;&14HRxZv}i<KqdY$`5zzY%2C3B1btqyvjR5l+MVa1l$IUwU@B z6Alr8<auOuDn;!LCV2SO#kF?1csov0>iBgrR!nrhgx7{{e*S%=iM=vP@U+{}1^yH@ zrZ^`lZdmjvp|YE-F<9DHCl-?uku^^&#@XfAJ(I3NE}VM&Q?hNdKhx;5LBj$EESjr^ zaa>VI9IGR*o;S;$8Zel=JHE$=S%jRy`xdIpeXbmX_KpN|6w=q|2f<W7PRnZ`@(ly$ zdDnQB8+T&=-?85FQt(QX*(rZ*tB1IK<nNUcVxE918zxOpUIlXO0RSTFbK~IT3frp2 z8W6lnok6=P1q%^8Ee?k8slL<eX*POQcvJFlYhY|#h1=|5lsk0rTQKuO_P(kVU>RDz z8tYNUCi#lC&DxWovl<?ek`#^I&<2l1;bRIWi|(rVT+|JU$yzjB<fe*itNDpa(SW|u zL(PJTsJSC;lcES}YXk|sA@1BH&C5lH+;L|&U$tZz6ul5_kW}YxTDG7_mP{Uf`TZ+O z=PqX*)wx_}S154@?`NY7TDA2r9UkX#fJSNKtm}j&VB$W54B>FJ&<LYJc?UPqq5fNj z&z?>n9W^$(T@vX8(af#hFCW0XSazgm4b)6Zvxx653+x0Am45WoLz95`_tL}#9DWh) z6LGNK(Wanfdp!9{OZcwD(s16#+4NAG`<evf8=Ns3kbM+Df@YxWw%vrbHiPwm0&sAl z^zd;r;m1KyL8zv{W#GfkLC0^gD+$ot?!8>by{5PCyLs+?Ryr>bl1j}s&{4*(0?<vv zM_|Kkt&N>{x@eMbnDfj89TnzD>aiEO*qML{T&F9NI_@x3dpKx#@Hkn%A;OMz?XM>6 z;47(^m6FgI0y3!p(3Qef)Uk>q;UL4nGm=H*?1(iyYo4D}bSMCFE8Rq6vP^UsABh1u z*JVh4m9ZDnxa*l*^|K#N0do;dG;BV+NI{FM{xB?moT}`g@%F~mHrqi^EaliH*jaoM zp;k_0viG<C_DU#E_N``E9<1WZX~pg!+nmO?NlOQ9<u%etqZ5Ya{rBl-j!6l>5ilQ_ z3e%y3ynxdTVEj1t>6No##z-+Zr**Izt_<*s!_oDULu7y^p=?!7>Ui}=Lw_I~X2v8e z0PFL<TV!n4lE-X@;8iL3E*#e7pwb+F?Uph`xfS@mJ=T1Xrr&5Z5!m>lJC2dAu6&po zDj|xZBM!=Nk<KBjdY-p}nxYZ$+Gk=Lr{)Uc^^CbnL_(`l%6Z!m%P*tmr(Fnu{yxhr zvQMg3gMx+w1Hsrijvu5rRf9DWkA`mQv8xODJ4GSNF|4w{A1Rzq^s6lan|6}@y#_hZ z&)oCwRz^CmQTV+^;Ck7|ahn9qHIg#S+kQ!hV_#V3{x#5-SO{C30MV3p%>>l486re) z@8dX<3H1?8L&l@;eMeQ=CS8QHYVjD+9zq$w<&#}bSa<uFM<l%@5^noblaT(vLfA4K zO3nyP`0`Y8)z1w02ys?W&SIdSDU;yIF7BX=`Buf)8NZU0>O4&KKe1Knv5X6H*hlR_ zZ8n)0bP@rucId%Ra5K<+kT2!y3bXTpc3KF5(-Z5$>OI*p=qmJyz%vv*!p~a&-YjnN zWoScjYpx5uD*COJ9wy)^=3CL>@f3+_O|hg^nm=xcUCf>RVcFLvEAC>y-NAndBMhPX zm?59gM$9hZ4feV@;-o^91^o!|Jv{;Yr13X222~tnp9DuR@X(FhIgj>hEOz8F>IC`9 zPU8L7P#ZR55_eoN`=Eu}S9s4y_esbM(&Bu@TOCze8yaI$Y^!aIO|hvrHP%Nb+0=ll z8e{w<S{)DI_LnT>gdQHtWs}9HcKv&IVr*r4^pK4C)kbO8>rdv_i4m12MpqMKY(?7> z0*qP4*9hAH7NH-rQ;$O=Nn?uWkHQE}7GbMtW)%vbp-VaPh+39d;eY@XqdL4S%|Hfx zgj7l!m|{{at7Q($46yoW*WZliqIcJPOK}eI!;HIg!YfuxoFI3|grV;4-oO#-g1NF$ zV%0hH_T57cQUAeD%+S3FI`rze+6Q+qN7$jHC9NUcSkek*xk}h!LJKwCy!)D}A=qfp zbY83h56(lR(S78|r2!>coyM23(LKv5tvzCB*I&la{x#8vnWi9Q+A&N~c=M9MDAz!K zhSr%3cAfKD_*}@Uot!J}@irMx$U@<%?$Oj~fD(plvZq1kObBWLXQBjv%>h;Fa1-Z* zTEl%*AYb$9vFPW3MNy+?we?0IB^zyrQfKh|?3TdQs6;_hGDx|soU?JsdMp@nb7?q< z85+=lRr`F%3FDA2Z8fNL^O*5{s8MQ+Uts_H^@ih&5IShV+<52~wum{7UlxYHZ9vu@ z=B~9~%8pl#jn^TYf{hY7r<tpo0ta$(Z&>7GEyG^nv}z6RBL}Ea>NGz!#`pi-QbZiY zUE$(}m?InHPtut#UEuM6O2$jDxd~cGLS?!0%i~xrS1i{n@C>;?tyKR9AM9|;heIOR z%_$z&%H7}-nI@f{yYrsVRW)>Lk0u^tOvbZANVClSI|qvw(R}cc$q|w3)Mj--CFwmz zFSAkWn?T@H%wt!?wvEv|YJe^H_)THf-Yvk5POHx8ye+#1dBD+pLs!xW^#v=nG^+#_ zwy?dFV=3lPt-E;I0f$<g7<jFWx6&yrxk#RHIG!G*cjN%mMqkMLXxS+4xydR9s4ePC zdWxT)MK2_ZIU=-`1&Sw}GmK+R=YYpXCmb>@BA_>@?dp4a0iSJfTi|AMpI1?1!IOap z*dOqEB6ZBIWT#L<NW6Rwc?^Pjzz+6p{$8D@i|BNp|Nbumk`!{zSV^;VD?;b<Mxe+e z{HBQSwQu>2^Hb<Ut^CY-s__5HlkdH`48ahygaS3GgF<ql>$A+<jv3FI>ilBFQLGw! z=8(9I)Zu*O9plQ^JDGFdWgAI0>ykVjBG9$attw~1WD1q6PB(TZ-FiB%WJbxl**M?K z-3K~{^Kb|_4Db2;(KM_5dSm(401UA-R*y98rCc`tey#R~7KC?f*#DQrtK%WqzU{Tt zR{=$P>1L<aM>w;#<kd}}7hSqNq?2O$o5TU>)(Z@O(#h!HXDfl#snOK_Ai&|1it0T( zl55fs*1E>u`x{B|tN+jWeKVJ~?P<AqjZ`*slLPEA2RqYvskXT#+2>wuvtDt;824I9 zb<HJlJpEXxU2h+fWV>mf9Q82_BS2Pd4Y)YWDb2SI%ALsXN0cMWVjuB>7(BP7qq$B4 z3tYtiR<-?w$q>{_d*IdogSut+>FYPU>OjBBoO8b#dVzj0p-X<?VH2Bb4f%cD_Nvbp ziFq#|HPWJqy8u;mZ!*$Z6oZ%p;>U$1rP(GgR|k{z$Avq%njsFO>+j@1MbIqt-wlwn zVOEgbIB%^nL(svQ3Tn35VKKozb=YilI&61O)3s`2D*{;|mbn*Y-rFN@0U)+REIB~X z<i--+E>K79O3|vt;<m-<8sYT2X6Kd3>=xQHCV7p;4XRZD7N_5bWLEB0)Cwfu{Lv3n z<88u_db_J`hxN5Z>LD}So`z=y4rEb6>)LRT$PmD2DB^lu@UtNUEgp=lk7aSLWTQQr z9JY6jcDsj<$$HH(#A-eUWJ2~;A%Lst!uWb$C(MMApnj3Cqq3<kR!+LCY>vX;r;e1_ zoRbixl`QZ7dUhu%B#NBTj&Vv;7#7Mk!$#C*?O52;`sIS%2JNXd3c^SJEfOO{EnYTA zPa3<Do@edVP-a%#3~|V`cLaGjT3g^&)2f_WV%7)}>*9*pFe&i1!wbD@#XZpCq|sSv zb=%!`78`ZumM!3A{?;w$!Wt(VkG>u48)L+bHv6ed&(Xu+o41L?cQd`o5IeS0c{<q5 z<!LRrW*s;?AG_Gmahl<QtxZYQzKK%2-feQe@w3;f_~)5a^6deD+os5K)73)6I1GjL zmJQX-R+a@}53ap~Ah52s7QPCsP#pWM6#^DjwIL;Iw=>$~P_X%-h1|`SNBK@SPIqjl z1=NHMywmsWAZke&tzNx`UFf_blHEGHob~qQ6=`R}x;|gE)~~lD=pi-G#rG%+)g6~j zGKURH&F6NMBcLBiZ^OLd(yB+KkKMylrft6Vd$zK8*-8Wfg5|dh3A?mGaCS23L9ouZ zbjJjtX_{+YLfKo>QgaDc@!@jqSMCAL?YoVtPTc|lW4D?aUbyg1ZJ(~zl@ncSYVM$N zU(H<|*V{z2qrUH-&ocU5kzC#iuf_MGj#lyj<SrM^y+qN>=VjQ#uUOQ$z1Xg>rdw{M zL)l2+^Kr$0ABNrO)@Nas7^W@gQR4_6Cb`*azdW9XYG*h6GW3lR-{t?*e7D+8j(_#t z6kWbgK38L<qbv;ByOcx~oxKMnI28fUIzMMut&u7C05&T}*U+;nK^dVm=SwCcscLjI zl$#-Boh=Q0D~E(zkOv5~s9R@;cXCSH<CCHezwFfw9@araJH!Tm>ht*d`&tE5^)g>) zqD}C+EnC3V%&CYzEV#I`MYJ!R1;UajPizeEpCw;}CU6A;3;knZDW%J?Cxq{1WdMlJ zgwu@(*Ncd&?k?3|>W!D~??wE`rhiwuFyI{!<WaJz<}oi+_grS(OO?&z_LG%EYyl6^ zW3L6+*_1L)9B=s`qxC&{zi<$tHh0louQAxuT^lPKGBFd-d|YnbA{H+e$LkOnF~!HD zN$@X!qgN=91s1t%Ase<#SaF1~xWrYn4%XwEqo<8~r*<`XDDw{KT6~ASr?v(%bB#sB z1B(d!7nLTFt*__Ynn)_t^VBpjC@%NJn5_n~y@f;*Nono^<FnJr7VnLltr9zK#^p+{ ze@%Bry^b7OUSpD#9^#V~-rJIJi)82EORd8>d)JfdxYpCA{ZqM=C!0e9Q>oy6F;-u| zLT)@HJNQkT`Er$|eC8U&3j!y!hQu2z7RAa7FN5CyGHihg;A*U2*_L%rg$-uH3G@{= z2=&$b7$%Iv%v%G)Ow&BUKWN`{qB#0#!2`guo7DWoJT;C0S~Uu1^oN#3lQc&lJ}Lb@ zo+c0f*w)w&7e!6(?9xo-eA&EI*tNUa*p5%oee<c&k4>N0in=D2rpt$~{VX)u(}cO5 zE|DZcLX!TTtD-9bXx**P!W+q&MuL9fkwPPvLz9PZKX&gOa;i5NbCRu`WS{MHq$SUv zkxPT#wVtg;#jBB(u(+z_F?f$o;>1PW3vVwFkh!pQlKJp~(A5<E6in_eK&B_d8Tzxa zWOarL<2dB-NemiaX(AmLt#jvIdjBx79$$1(&hD=B-1gTd*U+I~LfO?3N6SR5-4@zQ zAThR;F<Wf{Rx#FvHxZ9rGD4Xx)%$7!0$lUQa@{Qj`NzP%CcVyS)QXm^2*I8k0npaD zxA)7cJ@OF=y#SZ^Z7?A8s5&Z$L2Q==#C{mL2v%zXCL%2LUGwj$vhx6MFj1{AVMN6t z6}b|i=>A!XGXvgd+}qwA)-jjPW=9TcUkpiZ9e5sRd(m4a@h|jekLLhfIsmM`7~YJM zW|u?Z+gw}HwHUO68SVYW(ZH46*$5e)^nG8$l_}5M0Tpc!j2fp4*@Bx@8-7*!1*_B% zn6k;chodFj*=m4p+4`{aUB+LHz*e3OM;}0EBv+Gb9&((*&lhG_+y>LMHVJV&0ds3f zd#YIh_aO~OVDqx!IqwXKx(A_X>%Yt-bxPdJyiyB_y#;<iop9@GoD42Oz8qYjTW+Gn z{=yn2_VZRpXBG8u$TD3vlHP8!Ebb;~Fm(wsm;FafUZrUsILK%CgfmN`&}Jm4G6X5i zsPA1m83L71^vlE0_2{BnQ4$X!hNuCeG4UEaPoZkcHQCsFGV&Qt5+qMS=F3lC3ag&9 zIPESpcD<*y>vbgVN}~P<)B!wJ2*Lt78vDXx-|#h5EgVqA8nAa`b~o>GJh3%Nd*IEj ziF5eQ#vLS*Rp&?3b>dtDRNO~@uj0oMG|AAG2izsgVCOc_`Ni(F%dVH|hsSA>puOV2 zk4R5%!^~0eXk&JjEm8v-&>y}18$=%C&s9aAlo}JQO^1mk1qp`&iy@9GQV3_C%*M&$ z2xuC0{dEA@`H+T6J9LItF=`-LfU_*^Iio>s^(r!696WKy9bV511ld-yk+q7aZSDq7 z0~fPx0f|@G@J542T%@uv&`dDGtSkcO>aB@lT+k4rP{E$+vf`>reBkOejB8oE=!<Qg zXB+Z}zDn60X}pDgf#4;RKF0>gnH!*w65fu7OE?dbdH!%oFn>BliR9JCdZ;KvZ%7Xr zc&N}~2<aS=00>af3V_O<Qf%Ua_gXSKLv>q(IU(?&UZc}A0*||Vf`W2HsGC8Ll|jJ1 z5%+)q+ibr81m(rWtg<0aH$+uOY{#FrS5UeM^M(7fl?)`8m=6CDk3_<7PIw7MgAEME zQ%19(8gmw=arAGr&8On_AZ7_ke}TIPRD%ER&s3qf2DUwExs=VE_eGEgCBvhz``T`% zgNDAOVKZSU4+rOzN+;6?to6h@+S1B8=aoyfvviCVvRsIE-92HEhp&hCyoqdl4S6IV zB$54WAhB(S(BBD}HK2`B$WFDV>9%dHCk3pMFwzQkCzoOl;9Uh|SyQbLKf{Y98>`FQ zX@;HiFIUyrI@!a@)uBP>pd1gKM{&?2Nfmzxv?tONhgMa6$ILD+5vye^90YLsYs(Bc z3iDCS6SPK<i(28ph%~(1RVAxU+y+xQ$lr!|=IBwi<-v~P^J+Y_oyQb3MTCiL09J~( zOXxvq!HH}MZ&tjsY^TH&7ZxpZ`$3HJhkT_1DHd@6wBgbJ4YGpR5f<Rr{)jMDdYA>~ z69jJq^DiXWx;O4}vp?sia(XiorKu1n@L2vOg?Z|8ONVc!K~6#T0k4CO2u_+|N9a+= zw-4UO1<X<MD(!0gqfNG)IYeqoBVmSQ6A7T?!_uGdMJRcObUXwI#&v}7lFP#Ip5dyp z7iZNSGVN%}Sao@I34q|nRmB!)De(3LxO!N_%o5MDRe~tkML=@d)`LyJvXCEsZH!pf zwsZd^7J6=FIAP_CSn%v@97C@C;p_LOVC9`1IgaD&?|<N@Lk#PGL7x5aBgvfniZ2gh z6F!D4w}$BGGbs6*2@qfHQj*<DJ|^Z3twcsKGM^umFfJ(U#J^XK<QdG;y>>8kFDY?1 z^WXY<=ip4buirbilZkB`S8N-TWMX5&E4FQGVrydCnAo;$>z(_4&-47w`_^+#RoAMn z&t6?!UEP1|Ui-VgyUj6b_eO7SYAYWIvdfs$-RdBYom4~VtZB}~l=Og@iuGsk-$~0O z69}V9=bAq}RZH4O$YnM@grwkB5?D-(`Nd5<%gSSvn)|y=v?lr-`tRI@R&hUh@uIPZ z*#lg(%O}Cj-zNoWc|=$g;pZjjA!nK+nfbGV%K<iWjf%eSL8;#DSO>1i#TT|F+cub& zeWH&S=2Xe94?WIvQTkP>>ZIn_QA17rqA#+=`5m7UoGqQ1&X2%h{2*+7sNu^crbLg8 zrnnY#co&T+-}cA%tNF?nc<`oRSP$U4M*T8j$``+`z^^AKWB}`80FlE5q3yC~|BdUx za%lZllq({K9HT=r0Y|y9T{-k@ZKZdi8Ca9BHAkidj7SzdjSo;y#S(UPkbhoAv`Gn4 zZZ^9PpD>n_;_BAsx2^=2`y0c=*>zp9Ukcf_F@>yf-vE?v-igu<dJYRm56VdHPT;`y z?E!4|hykOhVXCfAgA~dMn4FfHi^|<7hTmPJ-47|$4-_v+P3W?zJGn<ohOl4F(1U+0 zn=?(Bi!>N7kTixdIbtc69k*+;R8`@WT0HW~q;!wX<tG4qh^}0XvFCNaS9J41`ChCg znRR)S)^~kXT;nKW(e^#zKTIf7`7rRQU_E}=PujY-alNxbzS0>sl|+VPU3{CFR4+Lu zl#EBSW)Dk-Gr*|C_pMd6pWnv5r(4*n(7>0Y!{uY;>Iecp9Os@w4KSt7$?D@n4-cKA ziX~z!pujmt@XrsuLK%qG6-wYc2{dTj{&{>%>71l9KcsK4YxS?U9E!|B!`>fw(Z<t_ zYYGzO8FQ?gq1h}HEp}B~!o^4H%fY;tCL;^+tB>H>KJJ24W%%an03*v3nzBhT(4U(R zw>fgrF=?amQj)_!I5B7q^lq9y6jJRq{M1Zmm@&;}(3F{VU%9a^Yg9~`ri$z|t_yO4 z>@c0%lUBrMtv@BlmJwdQ=Ai?kz`Fw#2QBB7_!XGi5u#yj1}j@k9d*K$o6YC~kmm=f zp~t!uVkA_L9OfOX(&_d~nefkwmI<G6&}Pgw#e?9WIl%H%QD_QG??&yxMp2L%&bjLL zjo^s&TDB-eo=*7;B!ly2`|=m}vyX;mbK(Vj)K$=(pLerQxof!RDVDNXGsp4*ly|E^ z3G_oJ9X)$?cmMW#1GG$q_Bw7@BonCP$;cA#5y*Gx<eTope6GUp@gyBz2aRmLVtQPc z_K>*(?Y4FO1DYWk$8dufsn8L*c@+-#b|(`(Rs)sm?ubR;!?RYr#JAltQ@7_S*`ImH zO4-$oN+6Krnk>8$J>DTRdGN2qrJOQ{{B*ysO3*+SDZkZ#-o9y#T6<h!M7^olhZB%; zv5&V<3fy<P5So{IFOBauU>V>u%I}WZ1Cq)kQ7PUKnLsEu7~*=Xn9BtHrN?N6P79E0 zhhV1xptw{*dk_|xAOv#J=(#{Jb_Kt`xOVQ}0o^GM1wW*e|4-ikmi3>D|0n3jg-^9T zNPgz=@cE+L>h@<)l`-4lCMmK+hwosQ221fDG*|l{NI?JJ_&@Ofjy94148FZ>ZR(;n zTwk^9jJ4+&!&9{q*+lUqsIld`&X562BM~Aj@{(Po`gi^yS`jWo7vT$$Y(bX^r%3fh z2RCVN3%wJeIfL)kYllEy7!VeYhjw;zj5V9R><$p&UDsA_1Vf82FjXHLQ&FFNmkXpp zo=9}L;M<y*bDpdA%~H_;o#F5*fn$5zI}Pz8-ub)A>}t0E>C$J#-|zI%Tzw_z_4<7D zlw%tm_lm38?i1N4`ycf_QQ4hD4Ai7+0r=m-6d}`{d;PV$XvO>9TQw5@-6x$hF)UcQ zP*97K_Y+bu?pRf=l4p6QE^UR+)StdCVf~3oVUvj}Ws`~QJ?7r5cF1vg-lfzhYa1?~ zSX4LZKa6D8KHSv;+*@*;q8tadY31H8GPlA^-cKL(FnljJmanySTzl%}NOpKgj?7hD z6&$4detYA1dd?d&p|T(R4|?*uovoQ?G2M(%$=R2E@;TxR;($lHC#1$Mj{F;$c@tRf zzRcLxU&P1o-0^~cD>_{I|8GTy4U$jAzEWr31cX@9grtsr7nBMNC1m~P52Pp8X4b07 zU>!6AafmbeQScaLwjK`WS=!8e=Cwef{4Bc4D!int-|hqRh;O3Hr<oR4f$`A;#_z>! zg&E@=kBF*&Dn|F1m<mK}Jg!oW6Jmu&!X?ZnVpZVxJF58j1PeY7@iC`qmih|N51K86 z^AE{}^)V~LX>s2{GJ+)x!0CuYFOZsKq}yTBzvbf_6gk!8){OGEH~MfC`FDrYA6g@& zP-@#|CX}@#D+G1{Z2+osyVOLFNFW_L+CRf6YxofB)n;t#%8J;eM^wMR5@=7)oC+aP ztrcano9ngxoER!HY|nt57IY&_-9%ABI+}<(Q^iA=6Kl%2F3+TGt~Wi8PjwIF!zCpn zs3YhaXYb&=EQ`CWV0hv;s6PFJY|>n5Eyc)-^w96<_};wb`*nY?HjI@vs44960OP|= z*n5iPl?6?6?02`fK>Zm#cT?WHYv;_TCqLoq#G}`!@GkjwL;H*{9+fqz5&5kBaQB_+ zQ^Pg8S}&Ngl<GQ?Xt?6za31T~MSt7s<F|}EiXo6Ibk<Ylo82|VL;S|F>M8jr@vpHN z-rU3pD)ZS9r<Y(h%b$}4iLM>!L<<GHH1$7OON+Cr4_dcbKBU>+vRmCBjo)`+&r;wa zO2Y2&4!G0bF5hKZ4{LWW6}iFq{m;#xSDh5J9nLkASMDqz$mWH4;U|29s!Uv(OW)g+ zX+aQ`f5yRGYyS<*v?_sWDCmOS)q*D{8KWVvFNxLk)p@g7-xr_2yLXd1QTft_Z1<CQ z*AJ2tUTQx{<7(LonkMaFSbnx$yRVVPH_I`lM>aH|Sz0p8@;un@%lFpHs?qs}PFZ`? z^Ho<^V^=l(+Ub5~W_sc6kaV^n^t7l_+^h7-XIfLdH=${La1+05i#t>WP=*-GTH1c) zg;BYjpV8FO?VRJRVy`fb_eG0qLGiG;oHelM)gMW^ZvZ-{!Zc(o)$T1@4vA#YaW|<N zTUVprn@&t84#GSFX({R0!qJmMzx<7xL*~7d-Il@my2P@W8fL<%WPb0mLJz~@u-xe1 z2p59zo2;?thHfLBvRuh%HzxCd-@nx5G{?7g2)j#BJc5{(T#w1<(p1ieN1jJtQV}Ec zKjsaq0xITaNXhyA5o*<{T?9uP2DZh0F(N5IqlTxyvp5H!DoY&7pye>>_l7D?#B1m% zIfO6~wO8LyOK<D+MpAR;7~v|xm;BJ=lU_&U=e<278T#c~Ll={)Fw6u+=}zT&bp4%M zo4HQFj!-ar#Doktl`)EF&h1y5@5b{B{io=#A7g`;)<}7A@!O@aTARp)D)KF>jSV4u z((gG|{v#bnQkh{3ma2ACj{2qkt>t<niv;gVRd(l*%ag4{fJvydRd_f-U95!T!+9Kk z`bd)|aOBa27OsoiB(;D@$Qo%ybz!hx)J2kN=yZT+?Sgk!Fr#1p>FJDssI7aeL8rZ2 zU*G8P_-ctCqOE-k;`A|0ZfztXIm~fpV@Jk!Do-`Gd-(oYb&KVJ;D3c$u0!6(k62f$ zx+rcSob-c|;xnHy33!^!*S2W;kmd9Ju%g>avnOii<Mua8zL(4N1Q&zYv_rDzlNKI@ zC0V-oXIK2u3droFZWG!+XLkEtGj7OJoQJtK9;|`K`bT<{GnF<ZKq{ZP?0q`BC0mRS zX2Hw8<b>AG+cXukT7LR3&o(d4E6X7v(ni5&P#=s9rw~rok1GG~<$HE9$?WsfEqk2b zfnY*~oh^cfmP;uC4{?^&Z0K?*Khsi`>QT^a^hC<ZPq2d7<w%qZdP!8I1(GstZLcYV zVRG?eN-0Xw({(vS$((!jE%@ix29%WH2ZF+6L%`XgQkh((N!Pj%DB=;&@L_#3crXTD z(VQ_y48?@+5p67~jE0y=H`~Zn9qArws(I&tKJx;tA)B`zSi>Lq3;o88Jgib`C^gkt zwIn6N>8WaC8CR7-?6VksJu`Sk5D=)ZBN^PBtB`ANLxEFEYrA)QnH#KOb37qzNOLu( zzn||hSWl~dJc24616Mqef}Eb}kmnQP+@r^8zmtz`wlZV54NDV7p#WB`gq^6M{*Y1w z+12hViA`suIwS`&9X}&%TS<qOal=9L{Xw$NRRT3r&eu(TcFi+2&_)vZZ9Pd@)GpGs z4bF*hhP*=Ry`rfhz6g(aO=hJrhlDx$g?ZMAUBPeGr+#}aa;N$Z@$g#-&ktj3on_6O zf)G{6AC@9!LV6D3m^{GnEwUaG^%H-1l@{&W`>Af9DcbWqtjV`j7i<e;TjCRQ?PXyt z;$#St6ZsEvlpB4r0NhodfvZwp5E_k4sUiLj_$71rH0t4)-XEEr`T9NCBkqe#;i7!{ zweCX!r90rxA<2O>l6D1Ic!po1j+_H5N(yO0lyW?#B<-mKvY0W!;z1Hx8oNjjQrjOs zBFX2@25{VpRHmAoW#cmV?142c&~>U9N9$^q=atjHQR`fq)!T=EI+y1|&YUOIM5laK z+Uf~aY%UBkuopg){%o(VLgpxg(*&P6)YTN-v}wRbHN}BTNqbBe!1$@#It^ODM;04( zY{MG{z3IFmY_&D#5%juzZ5x=V0xvzfc!~H?>wnUPq<xK>WWPpK10a%C(TbFZLN&D1 z!f{a_0Bv?<nODCL`}7N^=QtYM8O(S1icx}CBKyYG?-X{4sMm}3$!OKmmMO<>4MBAW z1RhEAUkKNH%UGhfA$9x-Vp9wF(fgv{_(k!}gcy-xKKzK;UTW*mK{>YD&mwfM^PX*I z9g3wxc8;_#m%R%tl(z5RiS8C(bG2*RMsGo5-WUgq`TeSUre*J&l^M_j&EkdCUcrc6 z2Vr-{&JEP<)E23r9jx3Gu~ecvLbiSw1^R*i`<Cke3JXBB|33dON`U*nXZ#oaKd_Tq z-2RPjkDRH}N<>o|YAewncz;%>foh8(b(~3!dH)T0*#E35{r}<rj7}Vo7l*dK9xo$= zsC|bK^*2B8wZk$#V4&vXP0~T*VhBO)jC%A=P`Nu2E+H@weT|U=Lr4yK?4AXr5NCn^ zXu1s}5$qb1#Wwr^anIE^F3iX(jD4G183)?+UclX0#AnS#^uE9Vw-k;0+C-ZN+Tvae z$T#VB7ebr2leUwkZiP`LGJ%#FT%};q2cR>!O%~>2&rQtL_{Qr+5U6tq`h%VRmhbcZ z&D53P2Cnl7$94NVW_P6R%YE>ez?@){kbuh345s1g#<3qVQP9occs1#Gbq$RC<JQ*K z*`CH_>tBOL6~zRx+zhb{cwXxTXGRIu^RhE95v;r;fk-zA@2yLMq4PYvK<Ceh7SB|& z%Lhc9JT&RVdweNm;DboxbNbo2Yw3k6>@zQ;0|6gBrO9$F1!x&N6jlqvWE>?_98{P_ z#y2JK!k!eVRa!bIuBMUUq60%0>nP|WC}LuPjp8|d<<)TYpvSX9)c5c1-PM0AR|e<z z8XkBJ94ys^Ub``tGpC$yDoDp|hsR}C22@ZUXTGms@!4q1V4SPRVhdHno@!ve*M{3+ z>e2Ub_u;|Ym&UGvbudCc`+9v|SEsMX(6xR%h<J6d2dCF-NYt~L_pM}Aj}n1z$TzZ^ zvN`ljuAQCqIs&Pmb0a)-_`h<sj8vN?T)S|ojUK%8pUPD<p<iV!X?x}MC+&V|5Dhoi zLd3*}5aUxO*j{z{rX>ICN~vxi+zwWy*%6?eQeO>nV<=?d;973KHB%_@#@_kXtF^zU zpGhiTjfa|*QZNw+?ow-Wb6Fhh@M04=1VmBGR0flt(&)5ZXju;t1PH)@-5w}pP#)>m zGd9LAJf|t>NqZig@QVIW%3Rm2ZorS)_~qbLLLm<`9Noas?sOqkWiF4LF&bJw-+b08 z%~T6x>K(SIM>s;ag+<taR^=IPwQ;iRy~(PPr=Ih)G~{#CqhI(7MW%7%;XLDo)>(|t zBwaF*kwRSZIokJ)BA?Q94P-s*2^njXrwq6l4|)!5VcwMW%Nk=eN3@-t3HQkKV)W+u zSbgRs@$A+032WsP#-j+{)#>X!+p5j?$I(mk=JH3e%g^Cj^mw#FpmlLq71Wr)%*Sy4 z9M#gp)>P`g(PgO|7-%SA%BWaO<u|N&@+tOdUTlT&xDMUuAS++!G-NN@OzmQd+<hD> z(zQJ>PS6~Y=RG0KZB9U4S^bfPSK+Pt6Y3J@w=sw<_UJ@W6@*igx&S)pPYF0FLdwE1 zi+%_&BxmE%56xhw0?~vr!dGf#EBIKcx;f%xy`Y)hevC{r*3X<b>||2o{fz^bHin$! zm^szhIr$jk_%33j%yMod2#Mdj+l6f}E+SSqd_ci&<dSO^vXkM*T!1VZW-dj7+_-{6 zLL&9OVaiEZlDcA>FQ9M*Peg+GjdXaAmAu%$cw{N5kG#}=TZT-@Gb4W7J*rX~U6E=A z96){m@waDzetUe|`g_t(LJYBEJ8|{=LoED&*?o}!;+TvaI(9+$Kcl9Y;sBiF!Sd0H zI}avZ9nAiC<oHA8<MO*fkxsBS<DZ#};U-yl3lC0NQNLgHcqdKrzWft0F)v~4K|vK& zPc7_l-zxYfGkO`>cL4loOiFTZE2pE|3x;JaDa*D4^eGE_NQ3OS!ItP=S^}Ah&=t9I z6~hT#%~}E!jlZdTll_-u?r(MYs|fXqL>sx_xTj;8xL=OLIy+F??HQLsqgP4Q58W?X zAFEsjt9YWzfabbhLcY%6={k>^y7AuBejPsaMW6jGN=5Y((%8|_Sw8b;a3%N<CDqE2 zaq=b6V-3iSK$i78(mSm$QW6b1(Nz%~$gV|B(yho072P$Y6w#%!$Y*KIY3x0l_1YxH zwzT8Yjxc2)73t)TTKv9HnrX+5wHfLQxOq}63=)~6_L)v|K7U%efQw9!BM;xuQLmzV zb-OjaTvO@Yjx_!HvAECW<=;j*ip;}Fc1X|MA>u&Q#tBR#B5=ZNfT=gVR5`?BQ4VWP zz=9yD;IC(TO;seMV+?W)DGP|(RKwd`@><Nm4tq$h1-I+-88Cj5-Vd13+DBOzvlQ#G zV?gN1u^Fz;F|QJ*?cZfR2qkSZRlIgMwJp^7oBFfiyFSsc2}~3s4!ltXlS5;2Jfuv5 zKf4ual?0fPFw%E5A!s@(MpRlxh~bBHzvAezWy#i;luk10vbMp@*8=iF#&G+vcatL6 zZ&xE$KSgo~wRD$vYeFoPgIfyDq;wI`bIB3rDv!#z+|$+_(ll?#?<}}wS`ic4iTB0$ zHQ`#{qLneZQ?W#Yf_sc66+|I+(Z{$2qvON)a0@JjGSO$a4)<zW?dRe4rJ1MO)Thby z|9S5b+<8XHPW4IceY^wlFG8XwhXL>*wBF^~zsd9v;0w^+5y%dwS3qS#u!uWSjc6R% zI5@x3USKFz{;Glb!$$-E`jOg+XS&VRnOJEn$Jo%-d9#coEOeO|{e0E95T|(nJ%wUq z=iVBYD|cw}7jV}SW962jL3l%&ao_?YCdBk98u5|Mf>wA(C=<&>H?bi!+@8$ZU{J~F z8>Gm@DS#0)2L{O1n?mH#47~EKt@x3r1r+g&;lTN4h~VURv?D9mjK9?uC8Ql>L4hMY zvAl9|r1G!_T05OC4*bTUs~>~jY|}B!r@zIJ#EGU}hux+lJ=k_N|Al+8v#e~uz?;$~ zg&6t(d*f^9CDOrZb@kT?Z)noz+{J8b+U+F*!AW4s)#bd~KwV{Tb{ucakKsx<XNwS@ zJX!8~rPP^|eHwS!mu=Ds^&YAH1VT`oFZu2@phWq@`er)ce?|2+Xc1x(7{yQBG~jkD zJ&VDbVyw-F!&tm5BV`|EBUXoJ%MC`Y`Gfx&3Y9@&oR~b+Ic@|}Z#)=Fakp}B(MaN1 zH#2BMr76CvZ<nyHIH?nO#!ZbFi`&5XD|kJ?m_)$f@?Z$l!ide?=tg}-Ga{!jrTL81 z+~lqqz;TXgq59VcfL=*Z*tXT#dwe*7)=lxJCkDe9g@^700p{`dqwk+5$VmC;l%ot7 zFo3xf?h`MJMkabxKPyF)-4j@ms}WeRQUcJCvL}~0JnS4R@rGku^;7dSD&%TZza37Z z^8@B2x{%(qj&O;o(B3qM1Lj;`t9>cF-QbxZ{8@8J?qkNgUktI3>B3Kh@LH^lj&>_4 z*0?yWe-%IBgR>j2tOB9v*pkwp(ivD*WYawZ26-Hjy5LPE2LuUs0+MciJ-E*87iWf+ z1H`he0P$?85^qV!M1eoyJ=RJ!Fm9<Ec1jk*{kc6>RC4Lnrb@d&rE#EA-%q7=cIq1^ zB_lkHb3EHQZ8Bbd=FOv?XI!40zsn~CFF@WSrD!U4mdvsBL9RDvcl(cAnOB-~X^LMQ z%L{WK*4J$%&6`_O()N_j{LSYL*)f}n4<NV-!_~j)Pl*IkFFC-O%@GAEix-p<+7gLa z)0PB$Edg9w4r3ONKg|NU4{mg4ln9a<1>NlNChq5m1Py=qwj;}@dGq9#4aSWtj3bU^ z=50V8^)+VhwAsaNd^0~Os&o6=`)jRzCaA9yrn^CqtWe}cTXaI5B{M92q9#+K;t(@i z<}{G>4<`6lq)L+|^DpK}o-O+q{txKyGv_EY5@bzy7XO%LmBVLcvyW>)A%$L$8+4!5 zOsIWKzs2uHg9{nx9gh|5TSn;aEW5M~LK15u%OnDAzjh5l6T8hvfr5y;$0CJ?B3GhD z;VZ1IXGiFP4F!RS{@W+lmR4T`wkqq^2wAu$)iSuJoMNF=4Z=7*{&?wsGwkhFKIFOD zhCl3+A6tqKlS$8tW>yG;4;ESB2SGWK3yBwNqsv~GkO`Z~F@1{~2CX7a0OtI``Hi#~ zo@!lC0&Cqnz)Mxojk7<#$Tpkq)`=t+9AF1u)?mQ;xeHGOrTWXSX=HZ9`Y1SzAj`aK zaFl~Tc;qc1c7%765xi#aew*yRZ3a}zZg6zU%;lu@kd5>RFcu4&x{53zuaGWT6Om`Z zchm+ewXl0byFb2DCX5!qLYuqU%7Y21&;Xg?%9$%_P`w&D4ZKakm%^9D3BOlq$^Yv! zAnhUy|A^IJLE28(2=9NeOx=STQ-TKC?7y!fuL(}Vp;<SJ(L=y|0thG4ib;=)Bir_G zJ3D;2qPz0L0eyhQg-8#!uBmyRA<i5lt|6#{xbXyeHeM^p5cxIAVL+J(NpXUZn|(8j zR4BI**KNS-@_SSyM!u&S(2Q7G3x!QcUQ=&^Q78j`o3XomDGn`&_xcU&L`$KYe=JA9 zXLciA-Mb|}<4jw9?+1?ynX#bO`<D3uk|^80Md9%Gh<sK-G_mZeBGXX%8*`-}?yvig zE465XUbjPh_2u#77>^E&Sr)_Mx%SHOxesKY5YFQ~g$-vEDXNe&8v!>rIW(M6WuW`S zAP%T?#s_9MB(PvxS&<;VYVAdK_EBh8Q@B251d4rllA;TZ^lZT{@jc=RFJZhmEADxM zjr&D7YI577QUVr^SFZc(I5y=8Yl|Ml)PS@R7+5`RD(KvIH)ghFum(6w!(OV0RJr8P z*<h?t53J=9)eyc+@SU)dOn*J#BL?}PILjsDUpbmpVHWbV_*HHeSerPKc<#LTA^S)X zsJIjyJ@#Lh&_mFthRP{G&*6YJfbr-DhdPA11ZqoUCG%2tV6#0Ey@0JeG^H?@R7F-A zE<vEiG&^B3US(k2UGBOVgim@t&9yXD&HSXq0153CY)w><ivci<bK7joL=|**@<o;b zE&oqpF3UHaZ==dso+2z4qkGwY;Jx%~V%zu?m|KVM{2m{qGhDZ5*wUB5UeGoemLU|! z56*=%dpP$TYBP{n`)~o&Ik(sdAj=c$8|=#Qe-K6cXxEs$&Xod=_QtyqNR5AhBa8if z3*JeD>20IfAfHUsd32yet$0rDX)|o#<eJ1%$4Lk<D_oR~?Kxf8aZlK3eP#9(-|{{u z2!f*GnY8`>xF#S3W>S!GUsP;CLB3a^qh`J7{*`+(<kCyZ#!>+zxe;Q*8gnbtdZV_W zZ*+ay!*|-FW977*+TG|m56b<`)68`ld_>#uSF-{Xh@v;xd2>>@A+h^tZST>i8iou( zn2MZtep@{Z4X!d&Njw<uh!iX&jyR36wxWHoPicr}UwYRRl*k=Fb1HLZ7(tK=m^s<h z2knV>y!-|RBg+I5G(n3W$A;H&+SNb^p_gLDAJc6+Nc8wtiyfNgrwLP_h{jmV@Ccru zBkZ+=+vnYbKfWGi0&_a@Q5rWJ-1CVc?(_q3D>zcd%oUp!sMP%TA&VjThU7%xoC`s} z^`xfs63<biq>2x4UC2x_g#@gb{z&*4Z>ME6(%8>3P(rl-)2>HV_f9=5A8fC*bd>I} z%lJa=uwT2l;X6s<K5^MK!m$)~ZbrZH%_?2Gotzg&`KP!U?>hTQJ-FZTFnQD7gFSnp z?%+nR74#G<)tHy8z(K%{SslW!Z(ks%%A(DFsI;L?q<0x~jpT<eL&y{15a4HdoE+qI zbt>vsiMHEcFrnmrsyU(ONS;+h2laG0x-aKb1Lm>zUlM4We8w7cP*xUmkfs|&RC)C! zmO&E7wPoWk=N!9gG2v}ZG;nEJg-7w^w~ehT@}DLnNjTyR)d))%`yNMbpfPWLPesIU zI2#kAK|6bMAJ@it{9y+pKK=Dzap@JHidt6vUVm^kdP#HQ3E~>)jXfM`4_9bqK=+-L z45yA^=3{eZ=NxShr&Hohqgz8=829WKw=_u-ISn1K0jUmYbEWY(8B@G+j=rf`fw8J$ zqM{MAGU#E8nl|{ZxQt$mJws67^OHGr00UGsu=VpFwf)lm04#{-Wkmd!Z8!DE;ED6V zT^Argle_CjIt4kmN(P=g`5iIvXkOx264p3z>|H@r3*otdBO4EYR$)g1EiY<Zs_@$p z{xZ%xFt28k07^5^h473RN=irdXoRv%#h@Xu9F<ptU`0sT<Rv#~qcR#R#<^aE1kti6 z5^{fa2O+!)|2G243jf8jxSXn6@*j|1ni(iiHyz9p@F_{#(mQnu+w_x39Ycud7-<ml zl8x~@@JNvScwC})^k2gEeb;eh5=sxno#Ok81U1UqEWm9G?0^=MAgDrjd}h{19bEWL zs-`H-wMUy@tYHDD6c#atkl&i(oo#|lf+I=xQ$pfIZHGSW*-@tS`PhZ{uM+MZEsp4k zqw;cubzSpB+2boU((Y@s5Xq^nk5v&QGbN~R8lc~4X88WfVVTl{MPx5*GE@0SYwsmc z+ldWiD=p2L;(q%mMo%wNU@{mkzFYDfe0R<8&@*)5d(q;WAaY>iDvcKQi*(R}DFs8X zf3-ELBaAZMi6FQ<4ha<X#2*fI!58(OI=X3)ex~k+UP2eZl89};T$==g8>Vx3v|U5- z>m07NALt-3_?-aL`E=ji4;rLcw={6SfB5!t!mziS%|YPfEqEjq-66l+S`Avyv$9~w zh;>lN*K_2BITbmdg)zYPdlcUSt&l||#YCTSiXE2@Etnvu_k?eXFsNI)?5!&hVw72n z(J1~^)PiF4L5V9Qmca5ES5KM;)J^#r!~6qruLeIXd2d+uKn$Ws8xoz%RYfd#SX>zG zY550gc7~iPJZQK_x5iWeL`hUtJt-gEcA;SXWel3KoANv<Trbkdt${<;w`BT-e%^aH zB4mokb)oat5YFeP?#}jr$0yhI>w}G{Y&%V51jHDI+{9_$QR}>~S@mFm+OwV0GZPK- zGt>N|upg8TG;}-yfx>b7F6iU<;xfceFc&;jdk45rLh<Y5(64pc&HpOLC6>EYEWwOB zH7(>sw4}Kd!6!)Z^#^s7HBE-|9eQ3)_h7)gAXlg7=E-t6m4z^R1M_6BTseKj-({(h zmYE#yaN6-f=jz$qei8t;P|^UXq`ELO=)V~6%|wy~CR<vl@UV!zhzZ0>@}xP~Pc~!^ zE>tnmk-k`E>9zm`b_{Tq2E1UkE=C#762T<~sgU0ILKLOo%zLav1lunp&5p+x2WY}j z2ryR6M44bPx_%{t+>jGFrrYr0Mkqiztysz0Pm~%=5JfILT&i`QdC@24IveI7LOex0 z{Kznoc{3Du5y1%gBFGcEs3JpxS7LKt1?#J@@Guk+cHTh}P&JHKW@6o@pFPTmfbxQT zP)UyMQYicdzXF+Qm4Fnq2KMdaF$BOVka+-%!t&pw|D@pBtAWRKefSk17Z=)ts05Kf z;`&pl39}3LRP^|YzawUaf7mdD(JAy9uSDu388A938!*M$+;ylWnZp{KHkV>Mjg>Iz zJf?q=WZ4PPujELompt1#{oT4oWYQoZ#D+e)CXDUj-va1T4*u)$hsNn2S3z*3xD{f# z80Rf<l@QC{fE>;@3+pU$g5c~AdNrgD1<Y5~1gV}F!DuyCZuX58B`5MTQjSdIQhPsK zDJHR+L7q2)pKrRFzE}2Icuk?!;*94@XfD^+;3x03JB>q_4Miq`bh%IsV5?;XhN-i7 z+WUJH-gm;$pvym?HMF$hVpvnVjjX6vI6s!8$(}22V{|hyb)zi4o6NSxWZef?lmvKH zMcN@#5IB$+FPZCaLrQ#kzK6}M^`fB<*GPnkg?jgG7f2<gCF~Vdd$@z5J)vJ=fqih? zO!h@W@~uJAZ~L2NqF%euY)kL@bc$4M$@W!^4>g79g0UpWlV52@d;Aq2IMx5t2;+v0 z^ShgxM5o7FFR7RZeW`VZ^u4jrp!M{{#{RZ-YmHOuTl0umbN7y%=lT614pzT==H{IC zHox<jHe*AfBb=?z@Pgy-oM>JM$N~K2EBVx20RUNb;ApGpkuDFlF_XWxUhwj)D=8mA z-Xo#pkc-s$bgOxwk*1*sQl)Pl9#Gx-!h2t&6{;>mms;%5OPDjQW-Q20na5QieRChM zj6hBHl<4^Irwm2IuCHuIA(4;Cc&c#WSN_rQgllKG{G*%1`B!7@LiXmp!LJ_n`Xpy| zq-B^Z4y6_Hjhnol5`c<lUt#69>B?nAQkiLLd)MtX9vW)^AaB^7nsj^9Uvx4K>HDk* zQM)oupzqKE4)5$H4)0;$HQdF<4k+8ewQ#Xb_RsTJia)PvJPL!78$Nq~w{loW@*}x# zgnJ(Gck8>J>u|F^VB0=4+vqY22iOX~<XWDR6S|LZ@`vYttx{zygg-L7G241nz6kTp z(0b+rt(*Ma)%}oc45*o<(uWC<Ft<r?q>i)*hG{_?EO4|i${4!r;y<=gsEqNXP4vK9 znpp1yY&ZSv)i`t^&^CT$UfO=0`0(<O%DgK99w8?)$S!ul6|iP+sPai0#vDAEmX$qL zGvv~x757*muju%HODVn{#cr*D_!4WWXg|VS(Wgi$KmlUL>X59*U{V67;IhvBqS`6! zZ*M*{d_e&#TYK{t=fKmad%(qePx^4#5BE$xzXbbdvPY@E@|ALhetqyTu7*@@l#gI5 z@89vb7bzzevYx5HiZn`Lz#p!T(>ClJ#)qeQPY%mTD8NMwgRIqFmARnL50)d#di^2D z?S$a5)zg1$ATlI9UM_de-zd?L6FJC4NYzOkn0&QaC~9$j@X}$&QrwOJ`0GEtZwKlo zH$I&7EwJ9c|8s=8%?<(=21l9N-CqA4e0#ytvmcBuEDT5=y7ck~-)Is2rtT|dku0%* z%^w#AdfvKeqOn8sM^F8pjUot{SL{Z|0gw<-kYtzxwmzZ5-?pXD&+=#NV56YJE2%m5 z_7&(M*ij0=Uh^N#xP7hd*4NwGy4<GX2H<0iHvfRAfm-Aw5}^g(Se#F3hKJC4WZH*v z8w0xPo`8oO6AGE&D=htjUI!(F9sy8_hO8h@#!GJNcuDT?42k8hIv~(}M(cPjUY@sA z>^<)e-B4<5rBa?I+*8#Ns-+8zZC&#R?IZuT^8oFL&<f`RlcxOpx!biA(`Q>i{+@ih z0W!T_RIRq@;le~uCVd&~*4-&A2LDy;F+~yE(4wv%$MwIk0Dd6e4goz(kK(E{{QGII zMNAVl<n0nb3bPkMlV?TZXvQuP>QGj*AzV<bg7`<gqR_0@_Y~IhU2Z5Lwlv&FYOVhI zH*3QhrJ1>1KPf$A!&_X@Endb;=3x-jW3ieo9g%bEnj@KG*=EmTc5Ak@6fqlf>goVm z_a=cpXG6z2cgyxqCHFJbp-P`$k5vrqKmusnkw2(g`$mVpnD)NrSN-K+G_R<`7l649 z$9F8c1PgdDSg_#o5Xq}^9RGEs9Pz)-Y_9V;)YO^QdA&TCoez?>ZJ@_D&d?hmIsXLp z83L!kB=-;a_hRn@u5t8)*-y6$f;RiP-@*Bwib2gn(`qZ_$hkJ0%xN1>n3VKKLNP($ zc;Vu9^gJI&PeqUR7}2cIKT>nOY9S8BC@hEsdLj9kwXG%Fe)R8>Rdlx$4{mVr;uwLp zY>aj6sRTiEXX7w6KTaoz7gzgj_7!8&P8h3#SFcPmk^X3CE0UUrjb8^tf8c*)Kxg1u zX5Z$Ngvc`u$kK^6cj4)xStek$PKJToVqeFXPsG(qJLZ~s6#~h0cA^&t8aM?j*(KAH z>M=_`ruz91wq~$-L<vpK3F!0sIOQ`4RsSsP?YQoL4ye(PJK>NK_?*=Xk7Yc3_y#4+ zu=y>&<z`EuNaTSB(N~7v;CI8aC<v85cxpF-^-}bF#nPiz)mHw^PRz|K#^H*=k+$AM zqt6UwIOMvXpKBB+bYg36)`lGdePAgII4w4zW?}xLRl}Kcec-k>l&CRVFyHdz1R149 z5M!!x18ToCS5YU_2dVvgK*dVZ3BLAd`%=hjU)R=KeQw5>F55)J+l=V%D8j<PAc498 z5sw482ZL)6B%pOj{sM?CG;%QCbnl2B$2|T?;&vW68Y$x(DX!cb8&s5>XHJV1?cG9} zR`c|$;xG~;h^`E85SR;;LReEMT<Mpltabbx)G-@LNYA146>OLdidbO@csF!LK=nnc zQPIbG>Y*qsykpY)o#k8ECsfLt*RT&_bTp=hfSKSu$`Z#i{DyZ+%^)grNS&i8__?c? zr*OqWZ385Ly1Bwx?}#GK-rBNV+ta!1uofAK_%}?H(Br!?L;rXE0i5<k^km7JSWAez zR@{2}lnnGzH}Ru2z`_%(JTdk{RJ(E=1q89=s9$QerGFnqJ-Sc25|=kL7i@8lPnKd7 zEIq)F6HyNl@_Dk}{TmhM-=ZhDLPh(>nlrk|95ABtoRlJ02##B&fiT)q{!k*7*+aD* zC^?^ivQ)`?0wrMq@n%e=TNX5!?t&lTEg-_H!u2@f1~}vy=*j05b|fV@iHIJcDCBV~ zE9!1;4M`?mt|clfQL{|RyyfD1ti8}GEQVNQ_^KKHqrc~M9dDVxpnLE;G;-9tR=b|; z?|)bBj%12Pd-Jn&`Ka_Kym*{O2ICfMR+;hm1%^qNEoDyAovQQU(vQ+=CSLE4??|!v z*y81Zz~1<rIA|!UA?V`f4H0s$M+<C4>(gf0T-K%{PCKvwL4s}hY3?aHr`T~`Vw0O{ z<0Rlk)<}`BKLLuHhGPka#Ll0-be&|x8)H3iX^Dl!Jep*9TmR1rU>SudYbWLxb2Fqe z*yy0SP4Tr<RCT%T%X1a-r;2yn_nk5Ry2v?rp%nCS_*@Y!ET(IO&SI>%mdopan*e-0 zkhtd^C_tx9yT1e%*Yt5eBKzkKlwQ8nKPOJ$(Y`I9LQ8XqLK^9e8@V~{!;QJZzvqi` zca)O>bQb3-NdP!p{+}XPcTt}ahXDoIpwgLb^sMpJ4cZS;-R06}YeL<yi?aS|_vfD! z+10iD0I~OI3NZMV7&898I|IwyjAhAl(66=D1dbgrN8nFXHkf|ILL%`_KU%rwI3Kk} znKPWP>znsCID>oG{o|FPU3i$Gtu0Ntq;ZYwGJD7znW={?Lv9px>cY@)<3IIEdE6PX zSO$4BD2GEz;3;a$4*R`xx9CcFuk*}0=&Mx7d&0TXIa@;?Qg?S`F!PT1r-u$?@QjVZ zr^MD$p1QFOa_PwrFsyBel@No9$tSD*Jl71x*L7h}X1|>DmC)%p@-lCJn+^|h#c()Q z6X!gV=s-!d^}aj=3tN5h!;#rva54#GCtqQSI8(|Ubn_RfBC3j(g2G+c$H=P2R#o?* zAS$SWc5*Wxy}q_}3b#B25bsH1y!ud53|qYS(^;LBRcsQiT~@hW51=^f$p|swfe~K% z2L1&9NHIr-+t>D=>H&Ng(wzg3j(C`D(p)|nLWi`t&3z7Pg9ix8HzQ9ZeYpD10=kR4 z&~EiFB&>BR1Rv1bXgub@G7sHeJtKyXFmZYaa|lM=aDT$)j}3D2>?{Q(`0xWd5Fx7t z$hVC2QE17^`3n<N<S>CR_>KzRQ_F#6Ie`IzNNdnav>ojE?l=Lfp|Vl8noN3y5suLI zJ|1L0x58Gr@*g+Jd;)(~JGvoFp^ZlTkin;57}FzX0+`{bgLCoiq&b@~<tpN92%-BJ z=qB#}3Ad|Ps;h`=6ojqVAnAUQSp57x<YjE-i><A_mpHvT5@h{_7)A7DZZD6}7Om`y zR8M1V$~Y&MJ@`}<(T$u_lh8@@?G3z+29E%Soh#Ch$KYu>MBm*LIeU0{Gj-g3?eb~u z5>InYj`EL7u^2{fj@(Lxv%^>rOJ&LFxDEe<M~dfh-Q1GCsSK`{A3^Dr92Mp_<{^X| z?h1V&H0<E+jT&b)?OVzeC1Q;6glu8CNP{?#o@*!p8C7g8iw^KM5z=Ic^0z9BJIG1S z`|G%sq+`qps@b7->(@?R2ciKeiC;&+i5H5vEK=2<uP_0~lG~}TbeUB({zpr8y;7$( z0EkNI>7E*}t;One!H5zos=3Hr3rFUH7}%cFqS*X}E@t@@U>VmtIK(3QK#hcdr!X8f zNTvnreT1gb=XE=qHI;tmCtO2;2PVpcTWtdTIm(_@T;|)Frf+5KrqW3_GObOa^_#fr zC%cCDRKINmK2mpTk%|Ct_%qPN+r82V+|y8^qVMB4HE<N{X;h|}N@lv>jm4^BuNI8` zHgUW&LDe}KdBrQ~>AJ}w&;@Ktjg(--Jb?MD0443LXJ8!6hOI0?0_Rr5YH{eekM0`O zucj9?bpx$4ap*p=nr<$oSYrm<FDWnK`-6<1Jy8um>EAoo(x};)P=}iNtt;}ac>f6W z;A3G}-xc=DoBA%cd&qleeSyXOwz)mwcugr1o0cjylSiJe$kseRYzJYdpavCW4!e18 zLnt%R^UKsAvlJf#V|_VGi_DE}vf`2U-pmaR%uO$*6J&(y{($&dA%z`AsqBQ3mWvZ4 zCe!ymH<bb~4FVA)msFBkixc+psMmuhMY@EQH<@W*PAsU_M;fauJflY%#mFag<g6eQ zLT6Mf7H35=HI1M58B&84m_0aiyZtKxPbbT{r^Y<4=vojmU-9SH-6C{AEGlPl&nzNd z;08E)ef{v^jYJDSBAKjNX;idc64up1r4FJJMc96g!xniS`>E-^%JXb+ra*n&yhB8N zAzrd<J7{O+If+JEV1B|-GN<}m01^S={*t~nL*Cy#F3r5{%JXObv^TbHOEfs#X2jqn z*!^^(d9)%KI)r}bL7wk2uZKAOQ|Fb-i7`<rL>nYSC}I7>!JDimN8I2Dw3o4btiW6x z#kv~2z%YNflO+vpCCH$H4{vqAz}zk)aghO4o(jG<--99`*(ot0L9@J{4s(?Y<gFQ` zW-7H0ew!moKvmXuG+<$Sssm=$L>6tls*f%*Ya;<Xa+*GeyN2|t?S!B6HzW67FU^ep zV6CoOVno0YJd9-uMp_8(6S0A0e}bVz!MVct1B}4yeVTs_Vb<;k<t(1YCp?l2K`m-k zkU%Q*PQ~xjq3;g88he5pAI9GO6~3Oe&n^L0g07KVXm7D%MqfP1ZXNV5qHXt81v=UG zTgsQ&-mgv_doQFP@n&DN7R>9ntykMJk7)pj2U+Op4HjG>H(0eW=V)7ZFDSLu!2>v6 pgC{SLu!n5j%SpzoM?Y83o-bX`9tM{M-;bm2(kMpN0DVZ1{|D{0m(~CP literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8bded7e02..19ff138a1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2172,6 +2172,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_business_subtitle_chatbots" = "Chatbots"; "lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions."; +"lng_chatbots_title" = "Chatbots"; +"lng_chatbots_about" = "Add a bot to your account to help you automatically process and respond to the messages you receive. {link}"; +"lng_chatbots_about_link" = "Learn more..."; +"lng_chatbots_placeholder" = "Enter bot URL or username"; +"lng_chatbots_add_about" = "Enter the link to the Telegram bot that you want to automatically process your chats."; +"lng_chatbots_access_title" = "Chats accessible for the bot"; +"lng_chatbots_all_except" = "All 1-to-1 Chats Except..."; +"lng_chatbots_selected" = "Only Selected Chats"; +"lng_chatbots_excluded_title" = "Excluded chats"; +"lng_chatbots_exclude_button" = "Exclude Chats"; +"lng_chatbots_included_title" = "Included chats"; +"lng_chatbots_include_button" = "Select Chats"; +"lng_chatbots_exclude_about" = "Select chats or entire chat categories which the bot will not have access to."; +"lng_chatbots_permissions_title" = "Bot permissions"; +"lng_chatbots_reply" = "Reply to Messages"; +"lng_chatbots_reply_about" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; +"lng_chatbots_remove" = "Remove Bot"; + "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; "lng_boost_again_button" = "Boost Again"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index a129237ca..ede8feb2d 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -14,5 +14,6 @@ <file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file> <file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file> <file alias="palette.tgs">../../animations/palette.tgs</file> + <file alias="robot.tgs">../../animations/robot.tgs</file> </qresource> </RCC> diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index 1f24ecd76..f8a7ea6e8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -260,11 +260,11 @@ void Controller::choose(not_null<ChatData*> chat) { const auto init = [=](not_null<ListBox*> box) { auto above = object_ptr<Ui::VerticalLayout>(box); - Settings::AddDividerTextWithLottie( - above, - box->showFinishes(), - About(channel, chat), - u"discussion"_q); + Settings::AddDividerTextWithLottie(above, { + .lottie = u"discussion"_q, + .showFinished = box->showFinishes(), + .about = About(channel, chat), + }); if (!chat) { Assert(channel->isBroadcast()); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 8fccba41a..3f01fe269 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -1223,35 +1223,15 @@ void EditPeerColorBox( state->index = peer->colorIndex(); state->emojiId = peer->backgroundEmojiId(); state->statusId = peer->emojiStatusId(); - if (group) { - const auto divider = Ui::CreateChild<Ui::BoxContentDivider>( - box.get()); - const auto verticalLayout = box->verticalLayout()->add( - object_ptr<Ui::VerticalLayout>(box.get())); - - auto icon = CreateLottieIcon( - verticalLayout, - { - .name = u"palette"_q, - .sizeOverride = Size(st::settingsCloudPasswordIconSize), - }, - st::peerAppearanceIconPadding); - box->setShowFinishedCallback([animate = std::move(icon.animate)] { - animate(anim::repeat::once); + Settings::AddDividerTextWithLottie(box->verticalLayout(), { + .lottie = u"palette"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_boost_group_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, }); - verticalLayout->add(std::move(icon.widget)); - verticalLayout->add( - object_ptr<Ui::FlatLabel>( - verticalLayout, - tr::lng_boost_group_about(), - st::peerAppearanceCoverLabel), - st::peerAppearanceCoverLabelMargin); - - verticalLayout->geometryValue( - ) | rpl::start_with_next([=](const QRect &r) { - divider->setGeometry(r); - }, divider->lifetime()); } else { box->addRow(object_ptr<PreviewWrap>( box, diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index fbb9bb533..82c115774 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -669,6 +669,17 @@ bool ShowSearchTagsPromo( return true; } +bool ShowAboutBusinessChatbots( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + controller->showToast(u"Cool feature, yeah.."_q); AssertIsDebug(); + return true; +} + void ExportTestChatTheme( not_null<Window::SessionController*> controller, not_null<const Data::CloudTheme*> theme) { @@ -1036,7 +1047,11 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() { { u"about_tags"_q, ShowSearchTagsPromo - } + }, + { + u"about_business_chatbots"_q, + ShowAboutBusinessChatbots + }, }; return Result; } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index b85703cf0..0943e3ac1 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_inner_widget.h" +#include "chat_helpers/stickers_emoji_pack.h" #include "core/file_utilities.h" #include "core/click_handler_types.h" #include "history/history_item_helpers.h" @@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_options.h" +#include "ui/text/text_isolated_emoji.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" @@ -2239,6 +2241,17 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }; + if (const auto item = _dragStateItem) { + const auto emojiStickers = &session->emojiStickersPack(); + if (const auto view = item->mainView()) { + if (const auto isolated = view->isolatedEmoji()) { + if (const auto sticker = emojiStickers->stickerForEmoji(isolated)) { + addDocumentActions(sticker.document, item); + } + } + } + } + const auto asGroup = !Element::Moused() || (Element::Moused() != Element::Hovered()) || (Element::Moused()->pointState( diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp new file mode 100644 index 000000000..34969c7d9 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -0,0 +1,201 @@ +/* +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 "settings/business/settings_chatbots.h" + +#include "lang/lang_keys.h" +#include "settings/settings_common_session.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kAllExcept = 0; +constexpr auto kSelectedOnly = 1; + +class Chatbots : public Section<Chatbots> { +public: + Chatbots( + QWidget *parent, + not_null<Window::SessionController*> controller); + + [[nodiscard]] rpl::producer<QString> title() override; + + rpl::producer<> showFinishes() const { + return _showFinished.events(); + } + + const Ui::RoundRect *bottomSkipRounding() const { + return &_bottomSkipRounding; + } + +private: + void setupContent(not_null<Window::SessionController*> controller); + + void showFinished() override { + _showFinished.fire({}); + } + + rpl::event_stream<> _showFinished; + Ui::RoundRect _bottomSkipRounding; + +}; + +Chatbots::Chatbots( + QWidget *parent, + not_null<Window::SessionController*> controller) +: Section(parent) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { + setupContent(controller); +} + +rpl::producer<QString> Chatbots::title() { + return tr::lng_chatbots_title(); +} + +void Chatbots::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + struct State { + rpl::variable<bool> onlySelected = false; + rpl::variable<bool> replyAllowed = true; + }; + const auto state = content->lifetime().make_state<State>(); + + AddDividerTextWithLottie(content, { + .lottie = u"robot"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_chatbots_about( + lt_link, + tr::lng_chatbots_about_link( + ) | Ui::Text::ToLink(u"internal:about_business_chatbots"_q), + Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto username = content->add( + object_ptr<Ui::InputField>( + content, + st::settingsChatbotsUsername, + tr::lng_chatbots_placeholder()), + st::settingsChatbotsUsernameMargins); + + Ui::AddDividerText( + content, + tr::lng_chatbots_add_about(), + st::peerAppearanceDividerTextMargin); + Ui::AddSkip(content); + Ui::AddSubsectionTitle(content, tr::lng_chatbots_access_title()); + + const auto group = std::make_shared<Ui::RadiobuttonGroup>( + state->onlySelected.current() ? kSelectedOnly : kAllExcept); + const auto everyone = content->add( + object_ptr<Ui::Radiobutton>( + content, + group, + kAllExcept, + tr::lng_chatbots_all_except(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + const auto selected = content->add( + object_ptr<Ui::Radiobutton>( + content, + group, + kSelectedOnly, + tr::lng_chatbots_selected(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + + Ui::AddSkip(content, st::settingsChatbotsAccessSkip); + Ui::AddDivider(content); + + const auto excludeWrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content)) + )->setDuration(0); + const auto excludeInner = excludeWrap->entity(); + + Ui::AddSkip(excludeInner); + Ui::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title()); + const auto excludeAdd = AddButtonWithIcon( + excludeInner, + tr::lng_chatbots_exclude_button(), + st::settingsChatbotsAdd, + { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); + + excludeWrap->toggleOn(state->onlySelected.value() | rpl::map(!_1)); + excludeWrap->finishAnimating(); + + const auto includeWrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content)) + )->setDuration(0); + const auto includeInner = includeWrap->entity(); + + Ui::AddSkip(includeInner); + Ui::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title()); + const auto includeAdd = AddButtonWithIcon( + includeInner, + tr::lng_chatbots_include_button(), + st::settingsChatbotsAdd, + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + + includeWrap->toggleOn(state->onlySelected.value()); + includeWrap->finishAnimating(); + + group->setChangedCallback([=](int value) { + state->onlySelected = (value == kSelectedOnly); + }); + + Ui::AddSkip(content, st::settingsChatbotsAccessSkip); + Ui::AddDividerText( + content, + tr::lng_chatbots_exclude_about(), + st::peerAppearanceDividerTextMargin); + + Ui::AddSkip(content); + Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title()); + content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_chatbots_reply(), + st::settingsButtonNoIcon + ))->toggleOn(state->replyAllowed.value())->toggledChanges( + ) | rpl::start_with_next([=](bool value) { + state->replyAllowed = value; + }, content->lifetime()); + Ui::AddSkip(content); + + Ui::AddDividerText( + content, + tr::lng_chatbots_reply_about(), + st::settingsChatbotsBottomTextMargin, + RectPart::Top); + + Ui::ResizeFitChild(this, content); +} + +} // namespace + +Type ChatbotsId() { + return Chatbots::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.h b/Telegram/SourceFiles/settings/business/settings_chatbots.h new file mode 100644 index 000000000..06fab806c --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ChatbotsId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp index d2f8f9569..2d273895e 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp @@ -121,12 +121,12 @@ void Manage::setupContent() { showOther(type); }; - AddDividerTextWithLottie( - content, - showFinishes(), - tr::lng_settings_cloud_password_manage_about1( + AddDividerTextWithLottie(content, { + .lottie = u"cloud_password/intro"_q, + .showFinished = showFinishes(), + .about = tr::lng_settings_cloud_password_manage_about1( TextWithEntities::Simple), - u"cloud_password/intro"_q); + }); Ui::AddSkip(content); AddButtonWithIcon( diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index eb6bcb360..f4eebf4ff 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -589,9 +589,19 @@ peerAppearanceButton: SettingsButton(settingsButtonLight) { padding: margins(60px, 8px, 22px, 8px); iconLeft: 20px; } -peerAppearanceCoverLabel: FlatLabel(boxDividerLabel) { - align: align(top); -} peerAppearanceCoverLabelMargin: margins(22px, 0px, 22px, 17px); peerAppearanceIconPadding: margins(0px, 15px, 0px, 5px); peerAppearanceDividerTextMargin: margins(22px, 8px, 22px, 11px); + +settingsChatbotsUsername: InputField(defaultMultiSelectSearchField) { +} +settingsChatbotsAccess: Checkbox(defaultCheckbox) { + textPosition: point(18px, 2px); +} +settingsChatbotsUsernameMargins: margins(20px, 8px, 20px, 8px); +settingsChatbotsAccessMargins: margins(22px, 5px, 22px, 9px); +settingsChatbotsAccessSkip: 4px; +settingsChatbotsBottomTextMargin: margins(22px, 8px, 22px, 3px); +settingsChatbotsAdd: SettingsButton(settingsButton) { + iconLeft: 22px; +} diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 23a4d8914..fd56adb41 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/business/settings_chatbots.h" #include "settings/settings_common_session.h" #include "settings/settings_premium.h" #include "ui/effects/gradient.h" @@ -287,6 +288,7 @@ public: void setStepDataReference(std::any &data) override; [[nodiscard]] rpl::producer<> sectionShowBack() override final; + [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: void setupContent(); @@ -299,9 +301,10 @@ private: rpl::variable<bool> _backToggles; rpl::variable<Info::Wrap> _wrap; Fn<void(bool)> _setPaused; - std::shared_ptr<Ui::RadiobuttonGroup> _radioGroup; + rpl::event_stream<Type> _showOther; + rpl::event_stream<> _showBack; rpl::event_stream<> _showFinished; rpl::variable<QString> _buttonText; @@ -330,6 +333,10 @@ rpl::producer<> Business::sectionShowBack() { return _showBack.events(); } +rpl::producer<Type> Business::sectionShowOther() { + return _showOther.events(); +} + void Business::setStepDataReference(std::any &data) { using namespace Info::Settings; const auto my = std::any_cast<SectionCustomTopBarData>(&data); @@ -347,6 +354,11 @@ void Business::setupContent() { Ui::AddSkip(content, st::settingsFromFileTop); AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { + switch (feature) { + case BusinessFeature::Chatbots: + _showOther.fire(Settings::ChatbotsId()); + break; + } }); Ui::ResizeFitChild(this, content); diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index f9de195fc..7e4ac4180 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -170,39 +170,43 @@ not_null<Button*> AddButtonWithLabel( } void AddDividerTextWithLottie( - not_null<Ui::VerticalLayout*> parent, - rpl::producer<> showFinished, - rpl::producer<TextWithEntities> text, - const QString &lottie) { - const auto divider = Ui::CreateChild<Ui::BoxContentDivider>(parent.get()); - const auto verticalLayout = parent->add( - object_ptr<Ui::VerticalLayout>(parent.get())); - + not_null<Ui::VerticalLayout*> container, + DividerWithLottieDescriptor &&descriptor) { + const auto divider = Ui::CreateChild<Ui::BoxContentDivider>( + container.get()); + const auto verticalLayout = container->add( + object_ptr<Ui::VerticalLayout>(container.get())); + const auto size = descriptor.lottieSize.value_or( + st::settingsFilterIconSize); auto icon = CreateLottieIcon( verticalLayout, { - .name = lottie, - .sizeOverride = { - st::settingsFilterIconSize, - st::settingsFilterIconSize, - }, + .name = descriptor.lottie, + .sizeOverride = { size, size }, }, - st::settingsFilterIconPadding); - std::move( - showFinished - ) | rpl::start_with_next([animate = std::move(icon.animate)] { - animate(anim::repeat::once); - }, verticalLayout->lifetime()); + descriptor.lottieMargins.value_or(st::settingsFilterIconPadding)); + if (descriptor.showFinished) { + const auto repeat = descriptor.lottieRepeat.value_or( + anim::repeat::once); + std::move( + descriptor.showFinished + ) | rpl::start_with_next([animate = std::move(icon.animate), repeat] { + animate(repeat); + }, verticalLayout->lifetime()); + } verticalLayout->add(std::move(icon.widget)); - verticalLayout->add( - object_ptr<Ui::CenterWrap<>>( - verticalLayout, - object_ptr<Ui::FlatLabel>( + if (descriptor.about) { + verticalLayout->add( + object_ptr<Ui::CenterWrap<>>( verticalLayout, - std::move(text), - st::settingsFilterDividerLabel)), - st::settingsFilterDividerLabelPadding); + object_ptr<Ui::FlatLabel>( + verticalLayout, + std::move(descriptor.about), + st::settingsFilterDividerLabel)), + descriptor.aboutMargins.value_or( + st::settingsFilterDividerLabelPadding)); + } verticalLayout->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 838edcc64..00ecf2fe2 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/text_variant.h" #include "ui/rp_widget.h" #include "ui/round_rect.h" #include "base/object_ptr.h" @@ -151,11 +152,19 @@ void CreateRightLabel( rpl::producer<QString> label, const style::SettingsButton &st, rpl::producer<QString> buttonText); + +struct DividerWithLottieDescriptor { + QString lottie; + std::optional<anim::repeat> lottieRepeat; + std::optional<int> lottieSize; + std::optional<QMargins> lottieMargins; + rpl::producer<> showFinished; + rpl::producer<TextWithEntities> about; + std::optional<QMargins> aboutMargins; +}; void AddDividerTextWithLottie( - not_null<Ui::VerticalLayout*> parent, - rpl::producer<> showFinished, - rpl::producer<TextWithEntities> text, - const QString &lottie); + not_null<Ui::VerticalLayout*> container, + DividerWithLottieDescriptor &&descriptor); struct LottieIcon { object_ptr<Ui::RpWidget> widget; diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp index b1acce232..11347aa61 100644 --- a/Telegram/SourceFiles/ui/vertical_list.cpp +++ b/Telegram/SourceFiles/ui/vertical_list.cpp @@ -31,24 +31,28 @@ void AddDivider(not_null<Ui::VerticalLayout*> container) { void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, - const style::margins &margins) { + const style::margins &margins, + RectParts parts) { AddDividerText( container, std::move(text) | Ui::Text::ToWithEntities(), - margins); + margins, + parts); } void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<TextWithEntities> text, - const style::margins &margins) { + const style::margins &margins, + RectParts parts) { container->add(object_ptr<Ui::DividerLabel>( container, object_ptr<Ui::FlatLabel>( container, std::move(text), st::boxDividerLabel), - margins)); + margins, + parts)); } not_null<Ui::FlatLabel*> AddSubsectionTitle( diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h index 87deef178..7ab743bd3 100644 --- a/Telegram/SourceFiles/ui/vertical_list.h +++ b/Telegram/SourceFiles/ui/vertical_list.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/rect_part.h" + namespace style { struct FlatLabel; } // namespace style @@ -26,11 +28,13 @@ void AddDivider(not_null<Ui::VerticalLayout*> container); void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, - const style::margins &margins = st::defaultBoxDividerLabelPadding); + const style::margins &margins = st::defaultBoxDividerLabelPadding, + RectParts parts = RectPart::Top | RectPart::Bottom); void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<TextWithEntities> text, - const style::margins &margins = st::defaultBoxDividerLabelPadding); + const style::margins &margins = st::defaultBoxDividerLabelPadding, + RectParts parts = RectPart::Top | RectPart::Bottom); not_null<Ui::FlatLabel*> AddSubsectionTitle( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, From ad9107ca909c5c52b60059b5733e15549b4ff6c4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 20 Feb 2024 16:02:02 +0400 Subject: [PATCH 040/108] Implement chatbots section editing. --- Telegram/CMakeLists.txt | 7 + Telegram/Resources/langs/lang.strings | 4 + .../boxes/filters/edit_filter_box.cpp | 216 +----------------- .../boxes/filters/edit_filter_chats_list.cpp | 16 +- .../filters/edit_filter_chats_preview.cpp | 199 ++++++++++++++++ .../boxes/filters/edit_filter_chats_preview.h | 64 ++++++ .../boxes/filters/edit_filter_links.cpp | 3 +- .../data/business/data_business_chatbots.cpp | 34 +++ .../data/business/data_business_chatbots.h | 44 ++++ .../data/business/data_business_common.h | 31 +++ .../SourceFiles/data/data_chat_filters.cpp | 5 +- Telegram/SourceFiles/data/data_chat_filters.h | 4 + Telegram/SourceFiles/data/data_session.cpp | 4 +- Telegram/SourceFiles/data/data_session.h | 5 + .../business/settings_business_exceptions.cpp | 145 ++++++++++++ .../business/settings_business_exceptions.h | 37 +++ .../settings/business/settings_chatbots.cpp | 84 ++++++- Telegram/SourceFiles/window/window.style | 2 + 18 files changed, 670 insertions(+), 234 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp create mode 100644 Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h create mode 100644 Telegram/SourceFiles/data/business/data_business_chatbots.cpp create mode 100644 Telegram/SourceFiles/data/business/data_business_chatbots.h create mode 100644 Telegram/SourceFiles/data/business/data_business_common.h create mode 100644 Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_business_exceptions.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0afd120d0..e4a5c5eff 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -180,6 +180,8 @@ PRIVATE boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp boxes/filters/edit_filter_chats_list.h + boxes/filters/edit_filter_chats_preview.cpp + boxes/filters/edit_filter_chats_preview.h boxes/filters/edit_filter_links.cpp boxes/filters/edit_filter_links.h boxes/peers/add_bot_to_chat_box.cpp @@ -446,6 +448,9 @@ PRIVATE core/version.h countries/countries_manager.cpp countries/countries_manager.h + data/business/data_business_chatbots.cpp + data/business/data_business_chatbots.h + data/business/data_business_common.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -1277,6 +1282,8 @@ PRIVATE profile/profile_block_widget.h profile/profile_cover_drop_area.cpp profile/profile_cover_drop_area.h + settings/business/settings_business_exceptions.cpp + settings/business/settings_business_exceptions.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h settings/cloud_password/settings_cloud_password_common.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 19ff138a1..73b164dee 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2189,6 +2189,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_reply" = "Reply to Messages"; "lng_chatbots_reply_about" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; "lng_chatbots_remove" = "Remove Bot"; +"lng_chatbots_not_found" = "Chatbot not found"; +"lng_chatbots_add" = "Add"; "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; @@ -4341,6 +4343,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_type_non_contacts" = "Non-Contacts"; "lng_filters_type_groups" = "Groups"; "lng_filters_type_channels" = "Channels"; +"lng_filters_type_new" = "New Chats"; +"lng_filters_type_existing" = "Existing Chats"; "lng_filters_type_bots" = "Bots"; "lng_filters_type_no_archived" = "Archived"; "lng_filters_type_no_muted" = "Muted"; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 37b946d63..958d01436 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/filters/edit_filter_box.h" #include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" #include "boxes/filters/edit_filter_links.h" #include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -56,60 +57,6 @@ using Flags = Data::ChatFilter::Flags; using ExceptionPeersRef = const base::flat_set<not_null<History*>> &; using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const; -constexpr auto kAllTypes = { - Flag::Contacts, - Flag::NonContacts, - Flag::Groups, - Flag::Channels, - Flag::Bots, - Flag::NoMuted, - Flag::NoRead, - Flag::NoArchived, -}; - -class FilterChatsPreview final : public Ui::RpWidget { -public: - FilterChatsPreview( - not_null<QWidget*> parent, - Flags flags, - const base::flat_set<not_null<History*>> &peers); - - [[nodiscard]] rpl::producer<Flag> flagRemoved() const; - [[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const; - - void updateData( - Flags flags, - const base::flat_set<not_null<History*>> &peers); - - int resizeGetHeight(int newWidth) override; - -private: - using Button = base::unique_qptr<Ui::IconButton>; - struct FlagButton { - Flag flag = Flag(); - Button button; - }; - struct PeerButton { - not_null<History*> history; - Ui::PeerUserpicView userpic; - Ui::Text::String name; - Button button; - }; - - void paintEvent(QPaintEvent *e) override; - - void refresh(); - void removeFlag(Flag flag); - void removePeer(not_null<History*> history); - - std::vector<FlagButton> _removeFlag; - std::vector<PeerButton> _removePeer; - - rpl::event_stream<Flag> _flagRemoved; - rpl::event_stream<not_null<History*>> _peerRemoved; - -}; - struct NameEditing { not_null<Ui::InputField*> field; bool custom = false; @@ -167,167 +114,6 @@ not_null<FilterChatsPreview*> SetupChatsPreview( return preview; } -FilterChatsPreview::FilterChatsPreview( - not_null<QWidget*> parent, - Flags flags, - const base::flat_set<not_null<History*>> &peers) -: RpWidget(parent) { - updateData(flags, peers); -} - -void FilterChatsPreview::refresh() { - resizeToWidth(width()); -} - -void FilterChatsPreview::updateData( - Flags flags, - const base::flat_set<not_null<History*>> &peers) { - _removeFlag.clear(); - _removePeer.clear(); - const auto makeButton = [&](Fn<void()> handler) { - auto result = base::make_unique_q<Ui::IconButton>( - this, - st::windowFilterSmallRemove); - result->setClickedCallback(std::move(handler)); - return result; - }; - for (const auto flag : kAllTypes) { - if (flags & flag) { - _removeFlag.push_back({ - flag, - makeButton([=] { removeFlag(flag); }) }); - } - } - for (const auto &history : peers) { - _removePeer.push_back(PeerButton{ - .history = history, - .button = makeButton([=] { removePeer(history); }) - }); - } - refresh(); -} - -int FilterChatsPreview::resizeGetHeight(int newWidth) { - const auto right = st::windowFilterSmallRemoveRight; - const auto add = (st::windowFilterSmallItem.height - - st::windowFilterSmallRemove.height) / 2; - auto top = 0; - const auto moveNextButton = [&](not_null<Ui::IconButton*> button) { - button->moveToRight(right, top + add, newWidth); - top += st::windowFilterSmallItem.height; - }; - for (const auto &[flag, button] : _removeFlag) { - moveNextButton(button.get()); - } - for (const auto &[history, userpic, name, button] : _removePeer) { - moveNextButton(button.get()); - } - return top; -} - -void FilterChatsPreview::paintEvent(QPaintEvent *e) { - auto p = Painter(this); - auto top = 0; - const auto &st = st::windowFilterSmallItem; - const auto iconLeft = st.photoPosition.x(); - const auto iconTop = st.photoPosition.y(); - const auto nameLeft = st.namePosition.x(); - p.setFont(st::windowFilterSmallItem.nameStyle.font); - const auto nameTop = st.namePosition.y(); - for (const auto &[flag, button] : _removeFlag) { - PaintFilterChatsTypeIcon( - p, - flag, - iconLeft, - top + iconTop, - width(), - st.photoSize); - - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - FilterChatsTypeName(flag)); - top += st.height; - } - for (auto &[history, userpic, name, button] : _removePeer) { - const auto savedMessages = history->peer->isSelf(); - const auto repliesMessages = history->peer->isRepliesChat(); - if (savedMessages || repliesMessages) { - if (savedMessages) { - Ui::EmptyUserpic::PaintSavedMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } else { - Ui::EmptyUserpic::PaintRepliesMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - (savedMessages - ? tr::lng_saved_messages(tr::now) - : tr::lng_replies_messages(tr::now))); - } else { - history->peer->paintUserpicLeft( - p, - userpic, - iconLeft, - top + iconTop, - width(), - st.photoSize); - p.setPen(st::contactsNameFg); - if (name.isEmpty()) { - name.setText( - st::msgNameStyle, - history->peer->name(), - Ui::NameTextOptions()); - } - name.drawLeftElided( - p, - nameLeft, - top + nameTop, - button->x() - nameLeft, - width()); - } - top += st.height; - } -} - -void FilterChatsPreview::removeFlag(Flag flag) { - const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); - Assert(i != end(_removeFlag)); - _removeFlag.erase(i); - refresh(); - _flagRemoved.fire_copy(flag); -} - -void FilterChatsPreview::removePeer(not_null<History*> history) { - const auto i = ranges::find(_removePeer, history, &PeerButton::history); - Assert(i != end(_removePeer)); - _removePeer.erase(i); - refresh(); - _peerRemoved.fire_copy(history); -} - -rpl::producer<Flag> FilterChatsPreview::flagRemoved() const { - return _flagRemoved.events(); -} - -rpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const { - return _peerRemoved.events(); -} - void EditExceptions( not_null<Window::SessionController*> window, not_null<QObject*> context, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 989a9867b..25463f1e2 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -28,6 +28,8 @@ using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, Flag::Contacts, Flag::NonContacts, Flag::Groups, @@ -119,7 +121,7 @@ PaintRoundImageCallback TypeRow::generatePaintUserpicCallback( } Flag TypeRow::flag() const { - return static_cast<Flag>(id() & 0xFF); + return static_cast<Flag>(id() & 0xFFFF); } ExceptionRow::ExceptionRow(not_null<History*> history) : Row(history) { @@ -219,6 +221,8 @@ auto TypeController::rowSelectionChanges() const [[nodiscard]] QString FilterChatsTypeName(Flag flag) { switch (flag) { + case Flag::NewChats: return tr::lng_filters_type_new(tr::now); + case Flag::ExistingChats: return tr::lng_filters_type_existing(tr::now); case Flag::Contacts: return tr::lng_filters_type_contacts(tr::now); case Flag::NonContacts: return tr::lng_filters_type_non_contacts(tr::now); @@ -241,6 +245,8 @@ void PaintFilterChatsTypeIcon( int size) { const auto &color1 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg; + case Flag::ExistingChats: return st::historyPeer8UserpicBg; case Flag::Contacts: return st::historyPeer4UserpicBg; case Flag::NonContacts: return st::historyPeer7UserpicBg; case Flag::Groups: return st::historyPeer2UserpicBg; @@ -254,6 +260,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &color2 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg2; + case Flag::ExistingChats: return st::historyPeer8UserpicBg2; case Flag::Contacts: return st::historyPeer4UserpicBg2; case Flag::NonContacts: return st::historyPeer7UserpicBg2; case Flag::Groups: return st::historyPeer2UserpicBg2; @@ -267,6 +275,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &icon = [&]() -> const style::icon& { switch (flag) { + case Flag::NewChats: return st::windowFilterTypeNewChats; + case Flag::ExistingChats: return st::windowFilterTypeExistingChats; case Flag::Contacts: return st::windowFilterTypeContacts; case Flag::NonContacts: return st::windowFilterTypeNonContacts; case Flag::Groups: return st::windowFilterTypeGroups; @@ -469,6 +479,10 @@ object_ptr<Ui::RpWidget> EditFilterChatsListController::prepareTypesList() { auto EditFilterChatsListController::createRow(not_null<History*> history) -> std::unique_ptr<Row> { + const auto business = _options & (Flag::NewChats | Flag::ExistingChats); + if (business && (history->peer->isSelf() || !history->peer->isUser())) { + return nullptr; + } return history->inChatList() ? std::make_unique<ExceptionRow>(history) : nullptr; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp new file mode 100644 index 000000000..3e2efb87e --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp @@ -0,0 +1,199 @@ +/* +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 "boxes/filters/edit_filter_chats_preview.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "data/data_peer.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/text/text_options.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_window.h" + +namespace { + +using Flag = Data::ChatFilter::Flag; + +constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, + Flag::Contacts, + Flag::NonContacts, + Flag::Groups, + Flag::Channels, + Flag::Bots, + Flag::NoMuted, + Flag::NoRead, + Flag::NoArchived, +}; + +} // namespace + +FilterChatsPreview::FilterChatsPreview( + not_null<QWidget*> parent, + Flags flags, + const base::flat_set<not_null<History*>> &peers) +: RpWidget(parent) { + updateData(flags, peers); +} + +void FilterChatsPreview::refresh() { + resizeToWidth(width()); +} + +void FilterChatsPreview::updateData( + Flags flags, + const base::flat_set<not_null<History*>> &peers) { + _removeFlag.clear(); + _removePeer.clear(); + const auto makeButton = [&](Fn<void()> handler) { + auto result = base::make_unique_q<Ui::IconButton>( + this, + st::windowFilterSmallRemove); + result->setClickedCallback(std::move(handler)); + result->show(); + return result; + }; + for (const auto flag : kAllTypes) { + if (flags & flag) { + _removeFlag.push_back({ + flag, + makeButton([=] { removeFlag(flag); }) }); + } + } + for (const auto &history : peers) { + _removePeer.push_back(PeerButton{ + .history = history, + .button = makeButton([=] { removePeer(history); }) + }); + } + refresh(); +} + +int FilterChatsPreview::resizeGetHeight(int newWidth) { + const auto right = st::windowFilterSmallRemoveRight; + const auto add = (st::windowFilterSmallItem.height + - st::windowFilterSmallRemove.height) / 2; + auto top = 0; + const auto moveNextButton = [&](not_null<Ui::IconButton*> button) { + button->moveToRight(right, top + add, newWidth); + top += st::windowFilterSmallItem.height; + }; + for (const auto &[flag, button] : _removeFlag) { + moveNextButton(button.get()); + } + for (const auto &[history, userpic, name, button] : _removePeer) { + moveNextButton(button.get()); + } + return top; +} + +void FilterChatsPreview::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + auto top = 0; + const auto &st = st::windowFilterSmallItem; + const auto iconLeft = st.photoPosition.x(); + const auto iconTop = st.photoPosition.y(); + const auto nameLeft = st.namePosition.x(); + p.setFont(st::windowFilterSmallItem.nameStyle.font); + const auto nameTop = st.namePosition.y(); + for (const auto &[flag, button] : _removeFlag) { + PaintFilterChatsTypeIcon( + p, + flag, + iconLeft, + top + iconTop, + width(), + st.photoSize); + + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + FilterChatsTypeName(flag)); + top += st.height; + } + for (auto &[history, userpic, name, button] : _removePeer) { + const auto savedMessages = history->peer->isSelf(); + const auto repliesMessages = history->peer->isRepliesChat(); + if (savedMessages || repliesMessages) { + if (savedMessages) { + Ui::EmptyUserpic::PaintSavedMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } else { + Ui::EmptyUserpic::PaintRepliesMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + (savedMessages + ? tr::lng_saved_messages(tr::now) + : tr::lng_replies_messages(tr::now))); + } else { + history->peer->paintUserpicLeft( + p, + userpic, + iconLeft, + top + iconTop, + width(), + st.photoSize); + p.setPen(st::contactsNameFg); + if (name.isEmpty()) { + name.setText( + st::msgNameStyle, + history->peer->name(), + Ui::NameTextOptions()); + } + name.drawLeftElided( + p, + nameLeft, + top + nameTop, + button->x() - nameLeft, + width()); + } + top += st.height; + } +} + +void FilterChatsPreview::removeFlag(Flag flag) { + const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); + Assert(i != end(_removeFlag)); + _removeFlag.erase(i); + refresh(); + _flagRemoved.fire_copy(flag); +} + +void FilterChatsPreview::removePeer(not_null<History*> history) { + const auto i = ranges::find(_removePeer, history, &PeerButton::history); + Assert(i != end(_removePeer)); + _removePeer.erase(i); + refresh(); + _peerRemoved.fire_copy(history); +} + +rpl::producer<Flag> FilterChatsPreview::flagRemoved() const { + return _flagRemoved.events(); +} + +rpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const { + return _peerRemoved.events(); +} diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h new file mode 100644 index 000000000..c795bc493 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h @@ -0,0 +1,64 @@ +/* +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 "data/data_chat_filters.h" +#include "ui/rp_widget.h" +#include "ui/userpic_view.h" + +class History; + +namespace Ui { +class IconButton; +} // namespace Ui + +class FilterChatsPreview final : public Ui::RpWidget { +public: + using Flag = Data::ChatFilter::Flag; + using Flags = Data::ChatFilter::Flags; + + FilterChatsPreview( + not_null<QWidget*> parent, + Flags flags, + const base::flat_set<not_null<History*>> &peers); + + [[nodiscard]] rpl::producer<Flag> flagRemoved() const; + [[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const; + + void updateData( + Flags flags, + const base::flat_set<not_null<History*>> &peers); + + int resizeGetHeight(int newWidth) override; + +private: + using Button = base::unique_qptr<Ui::IconButton>; + struct FlagButton { + Flag flag = Flag(); + Button button; + }; + struct PeerButton { + not_null<History*> history; + Ui::PeerUserpicView userpic; + Ui::Text::String name; + Button button; + }; + + void paintEvent(QPaintEvent *e) override; + + void refresh(); + void removeFlag(Flag flag); + void removePeer(not_null<History*> history); + + std::vector<FlagButton> _removeFlag; + std::vector<PeerButton> _removePeer; + + rpl::event_stream<Flag> _flagRemoved; + rpl::event_stream<not_null<History*>> _peerRemoved; + +}; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index d60cc030d..5640c11b5 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -982,8 +982,7 @@ bool GoodForExportFilterLink( not_null<Window::SessionController*> window, const Data::ChatFilter &filter) { using Flag = Data::ChatFilter::Flag; - const auto listflags = Flag::Chatlist | Flag::HasMyLinks; - if (!filter.never().empty() || (filter.flags() & ~listflags)) { + if (!filter.never().empty() || (filter.flags() & Flag::RulesMask)) { window->showToast(tr::lng_filters_link_cant(tr::now)); return false; } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp new file mode 100644 index 000000000..26dd21687 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -0,0 +1,34 @@ +/* +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 "data/business/data_business_chatbots.h" + +namespace Data { + +Chatbots::Chatbots(not_null<Session*> session) +: _session(session) { +} + +Chatbots::~Chatbots() = default; + +const ChatbotsSettings &Chatbots::current() const { + return _settings.current(); +} + +rpl::producer<ChatbotsSettings> Chatbots::changes() const { + return _settings.changes(); +} + +rpl::producer<ChatbotsSettings> Chatbots::value() const { + return _settings.value(); +} + +void Chatbots::save(ChatbotsSettings settings) { + _settings = settings; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h new file mode 100644 index 000000000..adfe998d2 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -0,0 +1,44 @@ +/* +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 "data/business/data_business_common.h" + +class UserData; + +namespace Data { + +class Session; + +struct ChatbotsSettings { + UserData *bot = nullptr; + BusinessExceptions allowed; + BusinessExceptions disallowed; + bool repliesAllowed = false; + bool onlySelected = false; +}; + +class Chatbots final { +public: + explicit Chatbots(not_null<Session*> session); + ~Chatbots(); + + [[nodiscard]] const ChatbotsSettings ¤t() const; + [[nodiscard]] rpl::producer<ChatbotsSettings> changes() const; + [[nodiscard]] rpl::producer<ChatbotsSettings> value() const; + + void save(ChatbotsSettings settings); + +private: + const not_null<Session*> _session; + + rpl::variable<ChatbotsSettings> _settings; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h new file mode 100644 index 000000000..aed51fdf9 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -0,0 +1,31 @@ +/* +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/flags.h" + +class UserData; + +namespace Data { + +enum class BusinessChatType { + NewChats = (1 << 0), + ExistingChats = (1 << 1), + Contacts = (1 << 2), + NonContacts = (1 << 3), +}; +inline constexpr bool is_flag_type(BusinessChatType) { return true; } + +using BusinessChatTypes = base::flags<BusinessChatType>; + +struct BusinessExceptions { + BusinessChatTypes types; + std::vector<not_null<UserData*>> list; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 7e824cc33..237bf5c29 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -163,6 +163,7 @@ ChatFilter ChatFilter::withTitle(const QString &title) const { ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { auto result = *this; + result._flags &= Flag::RulesMask; if (chatlist) { result._flags |= Flag::Chatlist; if (hasMyLinks) { @@ -170,8 +171,6 @@ ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { } else { result._flags &= ~Flag::HasMyLinks; } - } else { - result._flags &= ~(Flag::Chatlist | Flag::HasMyLinks); } return result; } @@ -593,7 +592,7 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) { const auto id = filter.id(); const auto exceptionsChanged = filter.always() != updated.always(); - const auto rulesMask = ~(Flag::Chatlist | Flag::HasMyLinks); + const auto rulesMask = Flag() | Flag::RulesMask; const auto rulesChanged = exceptionsChanged || ((filter.flags() & rulesMask) != (updated.flags() & rulesMask)) || (filter.never() != updated.never()); diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 987d55ebe..7b5a96476 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -36,9 +36,13 @@ public: NoMuted = (1 << 5), NoRead = (1 << 6), NoArchived = (1 << 7), + RulesMask = ((1 << 8) - 1), Chatlist = (1 << 8), HasMyLinks = (1 << 9), + + NewChats = (1 << 10), // Telegram Business exceptions. + ExistingChats = (1 << 11), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags<Flag>; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index e7ec13242..4efd8fa8b 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "passport/passport_form_controller.h" #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name +#include "data/business/data_business_chatbots.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -268,7 +269,8 @@ Session::Session(not_null<Main::Session*> session) , _notifySettings(std::make_unique<NotifySettings>(this)) , _customEmojiManager(std::make_unique<CustomEmojiManager>(this)) , _stories(std::make_unique<Stories>(this)) -, _savedMessages(std::make_unique<SavedMessages>(this)) { +, _savedMessages(std::make_unique<SavedMessages>(this)) +, _chatbots(std::make_unique<Chatbots>(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index d391d1d31..4fc7b1db1 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -62,6 +62,7 @@ class NotifySettings; class CustomEmojiManager; class Stories; class SavedMessages; +class Chatbots; struct ReactionId; struct RepliesReadTillUpdate { @@ -142,6 +143,9 @@ public: [[nodiscard]] SavedMessages &savedMessages() const { return *_savedMessages; } + [[nodiscard]] Chatbots &chatbots() const { + return *_chatbots; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -1065,6 +1069,7 @@ private: const std::unique_ptr<CustomEmojiManager> _customEmojiManager; const std::unique_ptr<Stories> _stories; const std::unique_ptr<SavedMessages> _savedMessages; + const std::unique_ptr<Chatbots> _chatbots; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp b/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp new file mode 100644 index 000000000..568aca80f --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp @@ -0,0 +1,145 @@ +/* +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 "settings/business/settings_business_exceptions.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" + +namespace Settings { +namespace { + +using Flag = Data::ChatFilter::Flag; +using Flags = Data::ChatFilter::Flags; + +[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) { + using Type = Data::BusinessChatType; + return ((types & Type::Contacts) ? Flag::Contacts : Flag()) + | ((types & Type::NonContacts) ? Flag::NonContacts : Flag()) + | ((types & Type::NewChats) ? Flag::NewChats : Flag()) + | ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag()); +} + +[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) { + using Type = Data::BusinessChatType; + return ((flags & Flag::Contacts) ? Type::Contacts : Type()) + | ((flags & Flag::NonContacts) ? Type::NonContacts : Type()) + | ((flags & Flag::NewChats) ? Type::NewChats : Type()) + | ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type()); +} + +} // namespace + +void EditBusinessExceptions( + not_null<Window::SessionController*> window, + BusinessExceptionsDescriptor &&descriptor) { + const auto session = &window->session(); + const auto options = Flag::ExistingChats + | Flag::NewChats + | Flag::Contacts + | Flag::NonContacts; + auto &&peers = descriptor.current.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + auto controller = std::make_unique<EditFilterChatsListController>( + session, + (descriptor.allow + ? tr::lng_filters_include_title() + : tr::lng_filters_exclude_title()), + options, + TypesToFlags(descriptor.current.types) & options, + base::flat_set<not_null<History*>>(begin(peers), end(peers)), + [=](int count) { + return nullptr; AssertIsDebug(); + }); + const auto rawController = controller.get(); + const auto save = descriptor.save; + auto initBox = [=](not_null<PeerListBox*> box) { + box->setCloseByOutsideClick(false); + box->addButton(tr::lng_settings_save(), crl::guard(box, [=] { + const auto peers = box->collectSelectedRows(); + auto &&users = ranges::views::all( + peers + ) | ranges::views::transform([=](not_null<PeerData*> peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + save(Data::BusinessExceptions{ + .types = FlagsToTypes(rawController->chosenOptions()), + .list = std::move(users), + }); + box->closeBox(); + })); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + window->show( + Box<PeerListBox>(std::move(controller), std::move(initBox))); +} + +not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview( + not_null<Ui::VerticalLayout*> content, + not_null<rpl::variable<Data::BusinessExceptions>*> data) { + const auto rules = data->current(); + + const auto locked = std::make_shared<bool>(); + auto &&peers = data->current().list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + const auto preview = content->add(object_ptr<FilterChatsPreview>( + content, + TypesToFlags(data->current().types), + base::flat_set<not_null<History*>>(begin(peers), end(peers)))); + + preview->flagRemoved( + ) | rpl::start_with_next([=](Flag flag) { + *locked = true; + *data = Data::BusinessExceptions{ + data->current().types & ~FlagsToTypes(flag), + data->current().list + }; + *locked = false; + }, preview->lifetime()); + + preview->peerRemoved( + ) | rpl::start_with_next([=](not_null<History*> history) { + auto list = data->current().list; + list.erase( + ranges::remove(list, not_null(history->peer->asUser())), + end(list)); + + *locked = true; + *data = Data::BusinessExceptions{ + data->current().types, + std::move(list) + }; + *locked = false; + }, preview->lifetime()); + + data->changes( + ) | rpl::filter([=] { + return !*locked; + }) | rpl::start_with_next([=](const Data::BusinessExceptions &rules) { + auto &&peers = rules.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + preview->updateData( + TypesToFlags(rules.types), + base::flat_set<not_null<History*>>(begin(peers), end(peers))); + }, preview->lifetime()); + + return preview; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.h b/Telegram/SourceFiles/settings/business/settings_business_exceptions.h new file mode 100644 index 000000000..e60f1a01b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_business_exceptions.h @@ -0,0 +1,37 @@ +/* +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 "data/business/data_business_common.h" + +class FilterChatsPreview; + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +struct BusinessExceptionsDescriptor { + Data::BusinessExceptions current; + Fn<void(const Data::BusinessExceptions&)> save; + bool allow = false; +}; +void EditBusinessExceptions( + not_null<Window::SessionController*> window, + BusinessExceptionsDescriptor &&descriptor); + +not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview( + not_null<Ui::VerticalLayout*> content, + not_null<rpl::variable<Data::BusinessExceptions>*> data); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 34969c7d9..d358b5112 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -7,7 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" +#include "core/application.h" +#include "data/business/data_business_chatbots.h" +#include "data/data_session.h" +#include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_business_exceptions.h" #include "settings/settings_common_session.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" @@ -15,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" +#include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_settings.h" @@ -29,6 +36,7 @@ public: Chatbots( QWidget *parent, not_null<Window::SessionController*> controller); + ~Chatbots(); [[nodiscard]] rpl::producer<QString> title() override; @@ -42,24 +50,41 @@ public: private: void setupContent(not_null<Window::SessionController*> controller); + void save(); void showFinished() override { _showFinished.fire({}); } + const not_null<Window::SessionController*> _controller; + const not_null<Main::Session*> _session; + rpl::event_stream<> _showFinished; Ui::RoundRect _bottomSkipRounding; + rpl::variable<bool> _onlySelected = false; + rpl::variable<bool> _repliesAllowed = true; + rpl::variable<Data::BusinessExceptions> _allowed; + rpl::variable<Data::BusinessExceptions> _disallowed; + }; Chatbots::Chatbots( QWidget *parent, not_null<Window::SessionController*> controller) : Section(parent) +, _controller(controller) +, _session(&controller->session()) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } +Chatbots::~Chatbots() { + if (!Core::Quitting()) { + save(); + } +} + rpl::producer<QString> Chatbots::title() { return tr::lng_chatbots_title(); } @@ -69,12 +94,12 @@ void Chatbots::setupContent( using namespace rpl::mappers; const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + const auto current = controller->session().data().chatbots().current(); - struct State { - rpl::variable<bool> onlySelected = false; - rpl::variable<bool> replyAllowed = true; - }; - const auto state = content->lifetime().make_state<State>(); + _onlySelected = current.onlySelected; + _repliesAllowed = current.repliesAllowed; + _allowed = current.allowed; + _disallowed = current.disallowed; AddDividerTextWithLottie(content, { .lottie = u"robot"_q, @@ -93,7 +118,11 @@ void Chatbots::setupContent( object_ptr<Ui::InputField>( content, st::settingsChatbotsUsername, - tr::lng_chatbots_placeholder()), + tr::lng_chatbots_placeholder(), + (current.bot + ? current.bot->session().createInternalLink( + current.bot->username()) + : QString())), st::settingsChatbotsUsernameMargins); Ui::AddDividerText( @@ -104,7 +133,7 @@ void Chatbots::setupContent( Ui::AddSubsectionTitle(content, tr::lng_chatbots_access_title()); const auto group = std::make_shared<Ui::RadiobuttonGroup>( - state->onlySelected.current() ? kSelectedOnly : kAllExcept); + _onlySelected.current() ? kSelectedOnly : kAllExcept); const auto everyone = content->add( object_ptr<Ui::Radiobutton>( content, @@ -139,8 +168,18 @@ void Chatbots::setupContent( tr::lng_chatbots_exclude_button(), st::settingsChatbotsAdd, { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); + excludeAdd->setClickedCallback([=] { + EditBusinessExceptions(_controller, { + .current = _disallowed.current(), + .save = crl::guard(this, [=](Data::BusinessExceptions value) { + _disallowed = std::move(value); + }), + .allow = false, + }); + }); + SetupBusinessExceptionsPreview(excludeInner, &_disallowed); - excludeWrap->toggleOn(state->onlySelected.value() | rpl::map(!_1)); + excludeWrap->toggleOn(_onlySelected.value() | rpl::map(!_1)); excludeWrap->finishAnimating(); const auto includeWrap = content->add( @@ -157,12 +196,22 @@ void Chatbots::setupContent( tr::lng_chatbots_include_button(), st::settingsChatbotsAdd, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + includeAdd->setClickedCallback([=] { + EditBusinessExceptions(_controller, { + .current = _allowed.current(), + .save = crl::guard(this, [=](Data::BusinessExceptions value) { + _allowed = std::move(value); + }), + .allow = true, + }); + }); + SetupBusinessExceptionsPreview(includeInner, &_allowed); - includeWrap->toggleOn(state->onlySelected.value()); + includeWrap->toggleOn(_onlySelected.value()); includeWrap->finishAnimating(); group->setChangedCallback([=](int value) { - state->onlySelected = (value == kSelectedOnly); + _onlySelected = (value == kSelectedOnly); }); Ui::AddSkip(content, st::settingsChatbotsAccessSkip); @@ -177,9 +226,9 @@ void Chatbots::setupContent( content, tr::lng_chatbots_reply(), st::settingsButtonNoIcon - ))->toggleOn(state->replyAllowed.value())->toggledChanges( + ))->toggleOn(_repliesAllowed.value())->toggledChanges( ) | rpl::start_with_next([=](bool value) { - state->replyAllowed = value; + _repliesAllowed = value; }, content->lifetime()); Ui::AddSkip(content); @@ -192,6 +241,17 @@ void Chatbots::setupContent( Ui::ResizeFitChild(this, content); } +void Chatbots::save() { + const auto settings = Data::ChatbotsSettings{ + .bot = nullptr, + .allowed = _allowed.current(), + .disallowed = _disallowed.current(), + .repliesAllowed = _repliesAllowed.current(), + .onlySelected = _onlySelected.current(), + }; + _session->data().chatbots().save(settings); +} + } // namespace Type ChatbotsId() { diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 38621c69b..645c78285 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -303,6 +303,8 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }}; windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }}; windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; +windowFilterTypeNewChats: icon {{ "folders/folders_unread", historyPeerUserpicFg }}; +windowFilterTypeExistingChats: windowFilterTypeNoRead; windowFilterChatsSectionSubtitleHeight: 28px; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { From 0af131f144adc4421e7eff7133b76e39b025cd69 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 20 Feb 2024 18:34:05 +0400 Subject: [PATCH 041/108] Invert group / channel features list. --- Telegram/SourceFiles/ui/boxes/boost_box.cpp | 110 ++++++++++---------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index f4676dcc5..ff4c92c4e 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -201,57 +201,12 @@ void AddFeaturesList( lt_count, rpl::single(float64(i)))), st::boostLevelBadgePadding); - add( - tr::lng_feature_stories(lt_count, rpl::single(float64(i)), proj), - st::boostFeatureStories); - if (!group) { - add(tr::lng_feature_reactions( - lt_count, - rpl::single(float64(i)), - proj - ), st::boostFeatureCustomReactions); - if (const auto j = features.nameColorsByLevel.find(i) - ; j != end(features.nameColorsByLevel)) { - nameColors += j->second; - } - if (nameColors > 0) { - add(tr::lng_feature_name_color_channel( - lt_count, - rpl::single(float64(nameColors)), - proj - ), st::boostFeatureName); - } - if (const auto j = features.linkStylesByLevel.find(i) - ; j != end(features.linkStylesByLevel)) { - linkStyles += j->second; - } - if (linkStyles > 0) { - add(tr::lng_feature_link_style_channel( - lt_count, - rpl::single(float64(linkStyles)), - proj - ), st::boostFeatureLink); - } - if (i >= features.linkLogoLevel) { - add( - tr::lng_feature_link_emoji(proj), - st::boostFeatureCustomLink); - } - } - if (group && i >= features.emojiPackLevel) { + if (i >= features.customWallpaperLevel) { add( - tr::lng_feature_custom_emoji_pack(proj), - st::boostFeatureCustomEmoji); - } - if (group && i >= features.transcribeLevel) { - add( - tr::lng_feature_transcribe(proj), - st::boostFeatureTranscribe); - } - if (i >= features.emojiStatusLevel) { - add( - tr::lng_feature_emoji_status(proj), - st::boostFeatureEmojiStatus); + (group + ? tr::lng_feature_custom_background_group + : tr::lng_feature_custom_background_channel)(proj), + st::boostFeatureCustomBackground); } if (i >= features.wallpaperLevel) { add( @@ -263,13 +218,58 @@ void AddFeaturesList( proj), st::boostFeatureBackground); } - if (i >= features.customWallpaperLevel) { + if (i >= features.emojiStatusLevel) { add( - (group - ? tr::lng_feature_custom_background_group - : tr::lng_feature_custom_background_channel)(proj), - st::boostFeatureCustomBackground); + tr::lng_feature_emoji_status(proj), + st::boostFeatureEmojiStatus); } + if (group && i >= features.transcribeLevel) { + add( + tr::lng_feature_transcribe(proj), + st::boostFeatureTranscribe); + } + if (group && i >= features.emojiPackLevel) { + add( + tr::lng_feature_custom_emoji_pack(proj), + st::boostFeatureCustomEmoji); + } + if (!group) { + if (const auto j = features.linkStylesByLevel.find(i) + ; j != end(features.linkStylesByLevel)) { + linkStyles += j->second; + } + if (i >= features.linkLogoLevel) { + add( + tr::lng_feature_link_emoji(proj), + st::boostFeatureCustomLink); + } + if (linkStyles > 0) { + add(tr::lng_feature_link_style_channel( + lt_count, + rpl::single(float64(linkStyles)), + proj + ), st::boostFeatureLink); + } + if (const auto j = features.nameColorsByLevel.find(i) + ; j != end(features.nameColorsByLevel)) { + nameColors += j->second; + } + if (nameColors > 0) { + add(tr::lng_feature_name_color_channel( + lt_count, + rpl::single(float64(nameColors)), + proj + ), st::boostFeatureName); + } + add(tr::lng_feature_reactions( + lt_count, + rpl::single(float64(i)), + proj + ), st::boostFeatureCustomReactions); + } + add( + tr::lng_feature_stories(lt_count, rpl::single(float64(i)), proj), + st::boostFeatureStories); } } From 1e5f821c6f7d35741c2882895b25343759996a2b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 20 Feb 2024 19:06:01 +0400 Subject: [PATCH 042/108] Start all business sections implementation. --- Telegram/CMakeLists.txt | 14 +- Telegram/Resources/animations/greeting.tgs | Bin 0 -> 11908 bytes Telegram/Resources/animations/hours.tgs | Bin 0 -> 41195 bytes Telegram/Resources/animations/location.tgs | Bin 0 -> 64974 bytes Telegram/Resources/animations/phone.tgs | Bin 0 -> 7835 bytes Telegram/Resources/animations/sleep.tgs | Bin 0 -> 38806 bytes Telegram/Resources/animations/writing.tgs | Bin 0 -> 64148 bytes Telegram/Resources/langs/lang.strings | 64 ++++ .../Resources/qrc/telegram/animations.qrc | 8 +- .../data/business/data_business_chatbots.h | 4 +- .../data/business/data_business_common.h | 16 +- .../business/settings_away_message.cpp | 117 +++++++ .../settings/business/settings_away_message.h | 16 + .../business/settings_business_exceptions.cpp | 145 --------- .../business/settings_business_exceptions.h | 37 --- .../settings/business/settings_chatbots.cpp | 175 ++++------- .../settings/business/settings_greeting.cpp | 117 +++++++ .../settings/business/settings_greeting.h | 16 + .../settings/business/settings_location.cpp | 121 +++++++ .../settings/business/settings_location.h | 16 + .../business/settings_quick_replies.cpp | 107 +++++++ .../business/settings_quick_replies.h | 16 + .../business/settings_recipients_helper.cpp | 294 ++++++++++++++++++ .../business/settings_recipients_helper.h | 74 +++++ .../business/settings_working_hours.cpp | 104 +++++++ .../business/settings_working_hours.h | 16 + Telegram/SourceFiles/settings/settings.style | 2 + .../settings/settings_business.cpp | 21 +- .../SourceFiles/settings/settings_common.cpp | 5 +- .../SourceFiles/settings/settings_common.h | 1 + 30 files changed, 1196 insertions(+), 310 deletions(-) create mode 100644 Telegram/Resources/animations/greeting.tgs create mode 100644 Telegram/Resources/animations/hours.tgs create mode 100644 Telegram/Resources/animations/location.tgs create mode 100644 Telegram/Resources/animations/phone.tgs create mode 100644 Telegram/Resources/animations/sleep.tgs create mode 100644 Telegram/Resources/animations/writing.tgs create mode 100644 Telegram/SourceFiles/settings/business/settings_away_message.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_away_message.h delete mode 100644 Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp delete mode 100644 Telegram/SourceFiles/settings/business/settings_business_exceptions.h create mode 100644 Telegram/SourceFiles/settings/business/settings_greeting.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_greeting.h create mode 100644 Telegram/SourceFiles/settings/business/settings_location.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_location.h create mode 100644 Telegram/SourceFiles/settings/business/settings_quick_replies.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_quick_replies.h create mode 100644 Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_recipients_helper.h create mode 100644 Telegram/SourceFiles/settings/business/settings_working_hours.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_working_hours.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e4a5c5eff..9b3caa4ef 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1282,10 +1282,20 @@ PRIVATE profile/profile_block_widget.h profile/profile_cover_drop_area.cpp profile/profile_cover_drop_area.h - settings/business/settings_business_exceptions.cpp - settings/business/settings_business_exceptions.h + settings/business/settings_away_message.cpp + settings/business/settings_away_message.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h + settings/business/settings_greeting.cpp + settings/business/settings_greeting.h + settings/business/settings_location.cpp + settings/business/settings_location.h + settings/business/settings_quick_replies.cpp + settings/business/settings_quick_replies.h + settings/business/settings_recipients_helper.cpp + settings/business/settings_recipients_helper.h + settings/business/settings_working_hours.cpp + settings/business/settings_working_hours.h settings/cloud_password/settings_cloud_password_common.cpp settings/cloud_password/settings_cloud_password_common.h settings/cloud_password/settings_cloud_password_email.cpp diff --git a/Telegram/Resources/animations/greeting.tgs b/Telegram/Resources/animations/greeting.tgs new file mode 100644 index 0000000000000000000000000000000000000000..dd1ab78d28fda166f300238af72de46bf5dc1636 GIT binary patch literal 11908 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 B53T?J literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/hours.tgs b/Telegram/Resources/animations/hours.tgs new file mode 100644 index 0000000000000000000000000000000000000000..d49a48c326a452b128811d2f6ada933299029489 GIT binary patch literal 41195 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%0A<qSaW!HTzxPjF}gtBua4j&5{BnCcb} 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/location.tgs b/Telegram/Resources/animations/location.tgs new file mode 100644 index 0000000000000000000000000000000000000000..32ba54f16cd28f4f5143ac316b1eb66d1afbbcef GIT binary patch literal 64974 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%0Aec=XXiR({K5Zt(KG|%<NG~0p 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}a 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/phone.tgs b/Telegram/Resources/animations/phone.tgs new file mode 100644 index 0000000000000000000000000000000000000000..7541526afb0f9120bc9b77f1e4f2a6746104fb57 GIT binary patch literal 7835 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/sleep.tgs b/Telegram/Resources/animations/sleep.tgs new file mode 100644 index 0000000000000000000000000000000000000000..b766d6e438e6d7158becd5d07b24b47225d47154 GIT binary patch literal 38806 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 ZaaIFyRs(rf1OM{>{y%MOa-#vM1OV;Am4^TT literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/writing.tgs b/Telegram/Resources/animations/writing.tgs new file mode 100644 index 0000000000000000000000000000000000000000..47caac05a2fc28b951f68bb336c11a332dea94e2 GIT binary patch literal 64148 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 73b164dee..89508d8c5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2172,6 +2172,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_business_subtitle_chatbots" = "Chatbots"; "lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions."; +"lng_location_title" = "Location"; +"lng_location_about" = "Display the location of your business on your account."; +"lng_location_address" = "Enter Address"; +"lng_location_fallback" = "You can set your location on the map from your mobile device."; + +"lng_hours_title" = "Business Hours"; +"lng_hours_about" = "Turn this on to show your opening hours schedule to your customers."; +"lng_hours_show" = "Show Business Hours"; +"lng_hours_time_zone" = "Time Zone"; +"lng_hours_monday" = "Monday"; +"lng_hours_tuesday" = "Tuesday"; +"lng_hours_wednesday" = "Wednesday"; +"lng_hours_thursday" = "Thursday"; +"lng_hours_friday" = "Friday"; +"lng_hours_saturday" = "Saturday"; +"lng_hours_sunday" = "Sunday"; +"lng_hours_closed" = "Closed"; + +"lng_replies_title" = "Quick Replies"; +"lng_replies_about" = "Set up shortcuts with rich text and media to respond to messages faster."; +"lng_replies_add" = "Add Quick Reply"; +"lng_replies_add_title" = "New Quick Reply"; +"lng_replies_add_shortcut" = "Add a shortcut for your reply."; +"lng_replies_add_placeholder" = "Shortcut"; +"lng_replies_add_exists" = "This shortcut already exists."; +"lng_replies_empty_title" = "New Quick Reply"; +"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing / or using Attachment menu."; +"lng_replies_remove_title" = "Remove Shortcut"; +"lng_replies_remove_text" = "You didn't create a quick reply message. Do you want to remove the shortcut?"; +"lng_replies_edit_title" = "Edit Shortcut"; +"lng_replies_edit_about" = "Edit the name for this shortcut."; +"lng_replies_message_placeholder" = "Add a Quick Reply"; + +"lng_greeting_title" = "Greeting Message"; +"lng_greeting_about" = "Greet customers when they message you the first time or after a period of no activity."; +"lng_greeting_enable" = "Send Greeting Message"; +"lng_greeting_create" = "Create a Greeting Message"; +"lng_greeting_recipients" = "Recipients"; +"lng_greeting_select" = "Select chats or entire chat categories for sending a greeting message."; +"lng_greeting_period_title" = "Period of no activity"; +"lng_greeting_period_about" = "Choose how many days should pass after your last interaction with a recipient to send them a greeting in response to their message."; +"lng_greeting_empty_title" = "New Greeting Message"; +"lng_greeting_empty_about" = "Create greetings that will be automatically sent to new customers."; +"lng_greeting_message_placeholder" = "Add a Greeting"; + +"lng_away_title" = "Away Message"; +"lng_away_about" = "Automatically reply with a message when you are away."; +"lng_away_enable" = "Send Away Message"; +"lng_away_create" = "Create an Away Message"; +"lng_away_schedule" = "Schedule"; +"lng_away_schedule_always" = "Send Always"; +"lng_away_schedule_outside" = "Outside of Business Hours"; +"lng_away_schedule_custom" = "Custom Schedule"; +"lng_away_custom_start" = "Start Time"; +"lng_away_custom_end" = "End Time"; +"lng_away_recipients" = "Recipients"; +"lng_away_select" = "Select chats or entire chat categories for sending an away message."; +"lng_away_empty_title" = "New Away Message"; +"lng_away_empty_about" = "Add messages that will be automatically sent when you are off."; +"lng_away_message_placeholder" = "Add an Away Message"; + +"lng_business_limit_reached#one" = "Limit of {count} message reached."; +"lng_business_limit_reached#other" = "Limit of {count} messages reached."; + "lng_chatbots_title" = "Chatbots"; "lng_chatbots_about" = "Add a bot to your account to help you automatically process and respond to the messages you receive. {link}"; "lng_chatbots_about_link" = "Learn more..."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index ede8feb2d..12666b6fd 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -14,6 +14,12 @@ <file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file> <file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file> <file alias="palette.tgs">../../animations/palette.tgs</file> - <file alias="robot.tgs">../../animations/robot.tgs</file> + <file alias="sleep.tgs">../../animations/sleep.tgs</file> + <file alias="greeting.tgs">../../animations/greeting.tgs</file> + <file alias="location.tgs">../../animations/location.tgs</file> + <file alias="robot.tgs">../../animations/robot.tgs</file> + <file alias="writing.tgs">../../animations/writing.tgs</file> + <file alias="hours.tgs">../../animations/hours.tgs</file> + <file alias="phone.tgs">../../animations/phone.tgs</file> </qresource> </RCC> diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index adfe998d2..13b8a894b 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -17,10 +17,8 @@ class Session; struct ChatbotsSettings { UserData *bot = nullptr; - BusinessExceptions allowed; - BusinessExceptions disallowed; + BusinessRecipients recipients; bool repliesAllowed = false; - bool onlySelected = false; }; class Chatbots final { diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index aed51fdf9..743ddaa12 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -23,9 +23,23 @@ inline constexpr bool is_flag_type(BusinessChatType) { return true; } using BusinessChatTypes = base::flags<BusinessChatType>; -struct BusinessExceptions { +struct BusinessChats { BusinessChatTypes types; std::vector<not_null<UserData*>> list; + + friend inline bool operator==( + const BusinessChats &a, + const BusinessChats &b) = default; +}; + +struct BusinessRecipients { + BusinessChats included; + BusinessChats excluded; + bool onlyIncluded = false; + + friend inline bool operator==( + const BusinessRecipients &a, + const BusinessRecipients &b) = default; }; } // namespace Data diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp new file mode 100644 index 000000000..de2f1c454 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -0,0 +1,117 @@ +/* +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 "settings/business/settings_away_message.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class AwayMessage : public BusinessSection<AwayMessage> { +public: + AwayMessage( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~AwayMessage(); + + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + rpl::variable<Data::BusinessRecipients> _recipients; + +}; + +AwayMessage::AwayMessage( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +AwayMessage::~AwayMessage() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> AwayMessage::title() { + return tr::lng_away_title(); +} + +void AwayMessage::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + //const auto current = controller->session().data().chatbots().current(); + + //_recipients = current.recipients; + + AddDividerTextWithLottie(content, { + .lottie = u"sleep"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_away_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_away_enable(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single(false)); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + AddBusinessRecipientsSelector(inner, { + .controller = controller, + .title = tr::lng_away_recipients(), + .data = &_recipients, + }); + + Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + + Ui::ResizeFitChild(this, content); +} + +void AwayMessage::save() { +} + +} // namespace + +Type AwayMessageId() { + return AwayMessage::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.h b/Telegram/SourceFiles/settings/business/settings_away_message.h new file mode 100644 index 000000000..e9037b4f6 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_away_message.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type AwayMessageId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp b/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp deleted file mode 100644 index 568aca80f..000000000 --- a/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* -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 "settings/business/settings_business_exceptions.h" - -#include "boxes/filters/edit_filter_chats_list.h" -#include "boxes/filters/edit_filter_chats_preview.h" -#include "data/data_session.h" -#include "data/data_user.h" -#include "history/history.h" -#include "lang/lang_keys.h" -#include "ui/wrap/vertical_layout.h" -#include "window/window_session_controller.h" - -namespace Settings { -namespace { - -using Flag = Data::ChatFilter::Flag; -using Flags = Data::ChatFilter::Flags; - -[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) { - using Type = Data::BusinessChatType; - return ((types & Type::Contacts) ? Flag::Contacts : Flag()) - | ((types & Type::NonContacts) ? Flag::NonContacts : Flag()) - | ((types & Type::NewChats) ? Flag::NewChats : Flag()) - | ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag()); -} - -[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) { - using Type = Data::BusinessChatType; - return ((flags & Flag::Contacts) ? Type::Contacts : Type()) - | ((flags & Flag::NonContacts) ? Type::NonContacts : Type()) - | ((flags & Flag::NewChats) ? Type::NewChats : Type()) - | ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type()); -} - -} // namespace - -void EditBusinessExceptions( - not_null<Window::SessionController*> window, - BusinessExceptionsDescriptor &&descriptor) { - const auto session = &window->session(); - const auto options = Flag::ExistingChats - | Flag::NewChats - | Flag::Contacts - | Flag::NonContacts; - auto &&peers = descriptor.current.list | ranges::views::transform([=]( - not_null<UserData*> user) { - return user->owner().history(user); - }); - auto controller = std::make_unique<EditFilterChatsListController>( - session, - (descriptor.allow - ? tr::lng_filters_include_title() - : tr::lng_filters_exclude_title()), - options, - TypesToFlags(descriptor.current.types) & options, - base::flat_set<not_null<History*>>(begin(peers), end(peers)), - [=](int count) { - return nullptr; AssertIsDebug(); - }); - const auto rawController = controller.get(); - const auto save = descriptor.save; - auto initBox = [=](not_null<PeerListBox*> box) { - box->setCloseByOutsideClick(false); - box->addButton(tr::lng_settings_save(), crl::guard(box, [=] { - const auto peers = box->collectSelectedRows(); - auto &&users = ranges::views::all( - peers - ) | ranges::views::transform([=](not_null<PeerData*> peer) { - return not_null(peer->asUser()); - }) | ranges::to_vector; - save(Data::BusinessExceptions{ - .types = FlagsToTypes(rawController->chosenOptions()), - .list = std::move(users), - }); - box->closeBox(); - })); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - }; - window->show( - Box<PeerListBox>(std::move(controller), std::move(initBox))); -} - -not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview( - not_null<Ui::VerticalLayout*> content, - not_null<rpl::variable<Data::BusinessExceptions>*> data) { - const auto rules = data->current(); - - const auto locked = std::make_shared<bool>(); - auto &&peers = data->current().list | ranges::views::transform([=]( - not_null<UserData*> user) { - return user->owner().history(user); - }); - const auto preview = content->add(object_ptr<FilterChatsPreview>( - content, - TypesToFlags(data->current().types), - base::flat_set<not_null<History*>>(begin(peers), end(peers)))); - - preview->flagRemoved( - ) | rpl::start_with_next([=](Flag flag) { - *locked = true; - *data = Data::BusinessExceptions{ - data->current().types & ~FlagsToTypes(flag), - data->current().list - }; - *locked = false; - }, preview->lifetime()); - - preview->peerRemoved( - ) | rpl::start_with_next([=](not_null<History*> history) { - auto list = data->current().list; - list.erase( - ranges::remove(list, not_null(history->peer->asUser())), - end(list)); - - *locked = true; - *data = Data::BusinessExceptions{ - data->current().types, - std::move(list) - }; - *locked = false; - }, preview->lifetime()); - - data->changes( - ) | rpl::filter([=] { - return !*locked; - }) | rpl::start_with_next([=](const Data::BusinessExceptions &rules) { - auto &&peers = rules.list | ranges::views::transform([=]( - not_null<UserData*> user) { - return user->owner().history(user); - }); - preview->updateData( - TypesToFlags(rules.types), - base::flat_set<not_null<History*>>(begin(peers), end(peers))); - }, preview->lifetime()); - - return preview; -} - -} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.h b/Telegram/SourceFiles/settings/business/settings_business_exceptions.h deleted file mode 100644 index e60f1a01b..000000000 --- a/Telegram/SourceFiles/settings/business/settings_business_exceptions.h +++ /dev/null @@ -1,37 +0,0 @@ -/* -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 "data/business/data_business_common.h" - -class FilterChatsPreview; - -namespace Ui { -class VerticalLayout; -} // namespace Ui - -namespace Window { -class SessionController; -} // namespace Window - -namespace Settings { - -struct BusinessExceptionsDescriptor { - Data::BusinessExceptions current; - Fn<void(const Data::BusinessExceptions&)> save; - bool allow = false; -}; -void EditBusinessExceptions( - not_null<Window::SessionController*> window, - BusinessExceptionsDescriptor &&descriptor); - -not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview( - not_null<Ui::VerticalLayout*> content, - not_null<rpl::variable<Data::BusinessExceptions>*> data); - -} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d358b5112..5500ca539 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -6,18 +6,17 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" - +// #include "core/application.h" #include "data/business/data_business_chatbots.h" #include "data/data_session.h" #include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "settings/business/settings_business_exceptions.h" -#include "settings/settings_common_session.h" +#include "settings/business/settings_recipients_helper.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" -#include "ui/widgets/checkbox.h" +#include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" @@ -28,10 +27,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { namespace { -constexpr auto kAllExcept = 0; -constexpr auto kSelectedOnly = 1; +enum class LookupState { + Empty, + Loading, + Ready, +}; -class Chatbots : public Section<Chatbots> { +struct BotState { + UserData *bot = nullptr; + LookupState state = LookupState::Empty; +}; + +class Chatbots : public BusinessSection<Chatbots> { public: Chatbots( QWidget *parent, @@ -40,10 +47,6 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<> showFinishes() const { - return _showFinished.events(); - } - const Ui::RoundRect *bottomSkipRounding() const { return &_bottomSkipRounding; } @@ -52,29 +55,37 @@ private: void setupContent(not_null<Window::SessionController*> controller); void save(); - void showFinished() override { - _showFinished.fire({}); - } - - const not_null<Window::SessionController*> _controller; - const not_null<Main::Session*> _session; - - rpl::event_stream<> _showFinished; Ui::RoundRect _bottomSkipRounding; - rpl::variable<bool> _onlySelected = false; + rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<QString> _usernameValue; + rpl::variable<BotState> _botValue = nullptr; rpl::variable<bool> _repliesAllowed = true; - rpl::variable<Data::BusinessExceptions> _allowed; - rpl::variable<Data::BusinessExceptions> _disallowed; }; +[[nodiscard]] rpl::producer<QString> DebouncedValue( + not_null<Ui::InputField*> field) { + return rpl::single(field->getLastText()); +} + +[[nodiscard]] rpl::producer<BotState> LookupBot( + not_null<Main::Session*> session, + rpl::producer<QString> usernameChanges) { + return rpl::never<BotState>(); +} + +[[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview( + not_null<QWidget*> parent, + rpl::producer<BotState> state, + Fn<void()> resetBot) { + return object_ptr<Ui::RpWidget>(parent.get()); +} + Chatbots::Chatbots( QWidget *parent, not_null<Window::SessionController*> controller) -: Section(parent) -, _controller(controller) -, _session(&controller->session()) +: BusinessSection(parent, controller) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } @@ -96,10 +107,8 @@ void Chatbots::setupContent( const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); const auto current = controller->session().data().chatbots().current(); - _onlySelected = current.onlySelected; + _recipients = current.recipients; _repliesAllowed = current.repliesAllowed; - _allowed = current.allowed; - _disallowed = current.disallowed; AddDividerTextWithLottie(content, { .lottie = u"robot"_q, @@ -125,93 +134,31 @@ void Chatbots::setupContent( : QString())), st::settingsChatbotsUsernameMargins); + _usernameValue = DebouncedValue(username); + _botValue = rpl::single(BotState{ + current.bot, + current.bot ? LookupState::Ready : LookupState::Empty + }) | rpl::then( + LookupBot(&controller->session(), _usernameValue.changes()) + ); + + const auto resetBot = [=] { + username->setText(QString()); + username->setFocus(); + }; + content->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>( + content, + MakeBotPreview(content, _botValue.value(), resetBot))); + Ui::AddDividerText( content, tr::lng_chatbots_add_about(), st::peerAppearanceDividerTextMargin); - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_chatbots_access_title()); - const auto group = std::make_shared<Ui::RadiobuttonGroup>( - _onlySelected.current() ? kSelectedOnly : kAllExcept); - const auto everyone = content->add( - object_ptr<Ui::Radiobutton>( - content, - group, - kAllExcept, - tr::lng_chatbots_all_except(tr::now), - st::settingsChatbotsAccess), - st::settingsChatbotsAccessMargins); - const auto selected = content->add( - object_ptr<Ui::Radiobutton>( - content, - group, - kSelectedOnly, - tr::lng_chatbots_selected(tr::now), - st::settingsChatbotsAccess), - st::settingsChatbotsAccessMargins); - - Ui::AddSkip(content, st::settingsChatbotsAccessSkip); - Ui::AddDivider(content); - - const auto excludeWrap = content->add( - object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( - content, - object_ptr<Ui::VerticalLayout>(content)) - )->setDuration(0); - const auto excludeInner = excludeWrap->entity(); - - Ui::AddSkip(excludeInner); - Ui::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title()); - const auto excludeAdd = AddButtonWithIcon( - excludeInner, - tr::lng_chatbots_exclude_button(), - st::settingsChatbotsAdd, - { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); - excludeAdd->setClickedCallback([=] { - EditBusinessExceptions(_controller, { - .current = _disallowed.current(), - .save = crl::guard(this, [=](Data::BusinessExceptions value) { - _disallowed = std::move(value); - }), - .allow = false, - }); - }); - SetupBusinessExceptionsPreview(excludeInner, &_disallowed); - - excludeWrap->toggleOn(_onlySelected.value() | rpl::map(!_1)); - excludeWrap->finishAnimating(); - - const auto includeWrap = content->add( - object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( - content, - object_ptr<Ui::VerticalLayout>(content)) - )->setDuration(0); - const auto includeInner = includeWrap->entity(); - - Ui::AddSkip(includeInner); - Ui::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title()); - const auto includeAdd = AddButtonWithIcon( - includeInner, - tr::lng_chatbots_include_button(), - st::settingsChatbotsAdd, - { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); - includeAdd->setClickedCallback([=] { - EditBusinessExceptions(_controller, { - .current = _allowed.current(), - .save = crl::guard(this, [=](Data::BusinessExceptions value) { - _allowed = std::move(value); - }), - .allow = true, - }); - }); - SetupBusinessExceptionsPreview(includeInner, &_allowed); - - includeWrap->toggleOn(_onlySelected.value()); - includeWrap->finishAnimating(); - - group->setChangedCallback([=](int value) { - _onlySelected = (value == kSelectedOnly); + AddBusinessRecipientsSelector(content, { + .controller = controller, + .title = tr::lng_chatbots_access_title(), + .data = &_recipients, }); Ui::AddSkip(content, st::settingsChatbotsAccessSkip); @@ -243,13 +190,11 @@ void Chatbots::setupContent( void Chatbots::save() { const auto settings = Data::ChatbotsSettings{ - .bot = nullptr, - .allowed = _allowed.current(), - .disallowed = _disallowed.current(), + .bot = _botValue.current().bot, + .recipients = _recipients.current(), .repliesAllowed = _repliesAllowed.current(), - .onlySelected = _onlySelected.current(), }; - _session->data().chatbots().save(settings); + controller()->session().data().chatbots().save(settings); } } // namespace diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp new file mode 100644 index 000000000..599b25b2c --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -0,0 +1,117 @@ +/* +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 "settings/business/settings_greeting.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class Greeting : public BusinessSection<Greeting> { +public: + Greeting( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~Greeting(); + + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + rpl::variable<Data::BusinessRecipients> _recipients; + +}; + +Greeting::Greeting( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +Greeting::~Greeting() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> Greeting::title() { + return tr::lng_greeting_title(); +} + +void Greeting::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + //const auto current = controller->session().data().chatbots().current(); + + //_recipients = current.recipients; + + AddDividerTextWithLottie(content, { + .lottie = u"greeting"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_greeting_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_greeting_enable(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single(false)); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + AddBusinessRecipientsSelector(inner, { + .controller = controller, + .title = tr::lng_greeting_recipients(), + .data = &_recipients, + }); + + Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + + Ui::ResizeFitChild(this, content); +} + +void Greeting::save() { +} + +} // namespace + +Type GreetingId() { + return Greeting::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.h b/Telegram/SourceFiles/settings/business/settings_greeting.h new file mode 100644 index 000000000..2bb9afd59 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_greeting.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type GreetingId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp new file mode 100644 index 000000000..84e8d4e4b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -0,0 +1,121 @@ +/* +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 "settings/business/settings_location.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class Location : public BusinessSection<Location> { +public: + Location( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~Location(); + + [[nodiscard]] rpl::producer<QString> title() override; + + const Ui::RoundRect *bottomSkipRounding() const { + return mapSupported() ? nullptr : &_bottomSkipRounding; + } + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + [[nodiscard]] bool mapSupported() const; + + Ui::RoundRect _bottomSkipRounding; + +}; + +Location::Location( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { + setupContent(controller); +} + +Location::~Location() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> Location::title() { + return tr::lng_location_title(); +} + +void Location::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + AddDividerTextWithLottie(content, { + .lottie = u"location"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_location_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto address = content->add( + object_ptr<Ui::InputField>( + content, + st::settingsLocationAddress, + Ui::InputField::Mode::MultiLine, + tr::lng_location_address(), + QString()), + st::settingsChatbotsUsernameMargins); + + if (!mapSupported()) { + AddDividerTextWithLottie(content, { + .lottie = u"phone"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_location_fallback(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + .parts = RectPart::Top, + }); + } else { + + } + + Ui::ResizeFitChild(this, content); +} + +void Location::save() { +} + +bool Location::mapSupported() const { + return false; +} + +} // namespace + +Type LocationId() { + return Location::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_location.h b/Telegram/SourceFiles/settings/business/settings_location.h new file mode 100644 index 000000000..31e033253 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_location.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type LocationId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp new file mode 100644 index 000000000..dc8927bc2 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -0,0 +1,107 @@ +/* +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 "settings/business/settings_quick_replies.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class QuickReplies : public BusinessSection<QuickReplies> { +public: + QuickReplies( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~QuickReplies(); + + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + rpl::variable<Data::BusinessRecipients> _recipients; + +}; + +QuickReplies::QuickReplies( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +QuickReplies::~QuickReplies() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> QuickReplies::title() { + return tr::lng_replies_title(); +} + +void QuickReplies::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + AddDividerTextWithLottie(content, { + .lottie = u"writing"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_replies_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_replies_add(), + st::settingsButtonNoIcon + )); + + enabled->setClickedCallback([=] { + + }); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + + Ui::ResizeFitChild(this, content); +} + +void QuickReplies::save() { +} + +} // namespace + +Type QuickRepliesId() { + return QuickReplies::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.h b/Telegram/SourceFiles/settings/business/settings_quick_replies.h new file mode 100644 index 000000000..80cc2f129 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type QuickRepliesId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp new file mode 100644 index 000000000..a6288dbee --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -0,0 +1,294 @@ +/* +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 "settings/business/settings_recipients_helper.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "settings/settings_common.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kAllExcept = 0; +constexpr auto kSelectedOnly = 1; + +using Flag = Data::ChatFilter::Flag; +using Flags = Data::ChatFilter::Flags; + +[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) { + using Type = Data::BusinessChatType; + return ((types & Type::Contacts) ? Flag::Contacts : Flag()) + | ((types & Type::NonContacts) ? Flag::NonContacts : Flag()) + | ((types & Type::NewChats) ? Flag::NewChats : Flag()) + | ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag()); +} + +[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) { + using Type = Data::BusinessChatType; + return ((flags & Flag::Contacts) ? Type::Contacts : Type()) + | ((flags & Flag::NonContacts) ? Type::NonContacts : Type()) + | ((flags & Flag::NewChats) ? Type::NewChats : Type()) + | ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type()); +} + +} // namespace + +void EditBusinessChats( + not_null<Window::SessionController*> window, + BusinessChatsDescriptor &&descriptor) { + const auto session = &window->session(); + const auto options = Flag::ExistingChats + | Flag::NewChats + | Flag::Contacts + | Flag::NonContacts; + auto &&peers = descriptor.current.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + auto controller = std::make_unique<EditFilterChatsListController>( + session, + (descriptor.include + ? tr::lng_filters_include_title() + : tr::lng_filters_exclude_title()), + options, + TypesToFlags(descriptor.current.types) & options, + base::flat_set<not_null<History*>>(begin(peers), end(peers)), + [=](int count) { + return nullptr; AssertIsDebug(); + }); + const auto rawController = controller.get(); + const auto save = descriptor.save; + auto initBox = [=](not_null<PeerListBox*> box) { + box->setCloseByOutsideClick(false); + box->addButton(tr::lng_settings_save(), crl::guard(box, [=] { + const auto peers = box->collectSelectedRows(); + auto &&users = ranges::views::all( + peers + ) | ranges::views::transform([=](not_null<PeerData*> peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + save(Data::BusinessChats{ + .types = FlagsToTypes(rawController->chosenOptions()), + .list = std::move(users), + }); + box->closeBox(); + })); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + window->show( + Box<PeerListBox>(std::move(controller), std::move(initBox))); +} + +not_null<FilterChatsPreview*> SetupBusinessChatsPreview( + not_null<Ui::VerticalLayout*> container, + not_null<rpl::variable<Data::BusinessChats>*> data) { + const auto rules = data->current(); + + const auto locked = std::make_shared<bool>(); + auto &&peers = data->current().list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + const auto preview = container->add(object_ptr<FilterChatsPreview>( + container, + TypesToFlags(data->current().types), + base::flat_set<not_null<History*>>(begin(peers), end(peers)))); + + preview->flagRemoved( + ) | rpl::start_with_next([=](Flag flag) { + *locked = true; + *data = Data::BusinessChats{ + data->current().types & ~FlagsToTypes(flag), + data->current().list + }; + *locked = false; + }, preview->lifetime()); + + preview->peerRemoved( + ) | rpl::start_with_next([=](not_null<History*> history) { + auto list = data->current().list; + list.erase( + ranges::remove(list, not_null(history->peer->asUser())), + end(list)); + + *locked = true; + *data = Data::BusinessChats{ + data->current().types, + std::move(list) + }; + *locked = false; + }, preview->lifetime()); + + data->changes( + ) | rpl::filter([=] { + return !*locked; + }) | rpl::start_with_next([=](const Data::BusinessChats &rules) { + auto &&peers = rules.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + preview->updateData( + TypesToFlags(rules.types), + base::flat_set<not_null<History*>>(begin(peers), end(peers))); + }, preview->lifetime()); + + return preview; +} + +void AddBusinessRecipientsSelector( + not_null<Ui::VerticalLayout*> container, + BusinessRecipientsSelectorDescriptor &&descriptor) { + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, std::move(descriptor.title)); + + auto &lifetime = container->lifetime(); + const auto controller = descriptor.controller; + const auto data = descriptor.data; + const auto change = [=](Fn<void(Data::BusinessRecipients&)> modify) { + auto now = data->current(); + modify(now); + *data = std::move(now); + }; + const auto group = std::make_shared<Ui::RadiobuttonGroup>( + data->current().onlyIncluded ? kSelectedOnly : kAllExcept); + const auto everyone = container->add( + object_ptr<Ui::Radiobutton>( + container, + group, + kAllExcept, + tr::lng_chatbots_all_except(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + const auto selected = container->add( + object_ptr<Ui::Radiobutton>( + container, + group, + kSelectedOnly, + tr::lng_chatbots_selected(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + + Ui::AddSkip(container, st::settingsChatbotsAccessSkip); + Ui::AddDivider(container); + + const auto excludeWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container)) + )->setDuration(0); + const auto excludeInner = excludeWrap->entity(); + + Ui::AddSkip(excludeInner); + Ui::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title()); + const auto excludeAdd = AddButtonWithIcon( + excludeInner, + tr::lng_chatbots_exclude_button(), + st::settingsChatbotsAdd, + { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); + excludeAdd->setClickedCallback([=] { + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.excluded = std::move(value); + }); + }; + EditBusinessChats(controller, { + .current = data->current().excluded, + .save = crl::guard(excludeAdd, save), + .include = false, + }); + }); + + const auto excluded = lifetime.make_state< + rpl::variable<Data::BusinessChats> + >(data->current().excluded); + data->changes( + ) | rpl::start_with_next([=](const Data::BusinessRecipients &value) { + *excluded = value.excluded; + }, lifetime); + excluded->changes( + ) | rpl::start_with_next([=](Data::BusinessChats &&value) { + auto now = data->current(); + now.excluded = std::move(value); + *data = std::move(now); + }, lifetime); + + SetupBusinessChatsPreview(excludeInner, excluded); + + excludeWrap->toggleOn(data->value( + ) | rpl::map([](const Data::BusinessRecipients &value) { + return !value.onlyIncluded; + })); + excludeWrap->finishAnimating(); + + const auto includeWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container)) + )->setDuration(0); + const auto includeInner = includeWrap->entity(); + + Ui::AddSkip(includeInner); + Ui::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title()); + const auto includeAdd = AddButtonWithIcon( + includeInner, + tr::lng_chatbots_include_button(), + st::settingsChatbotsAdd, + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + includeAdd->setClickedCallback([=] { + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + }; + EditBusinessChats(controller, { + .current = data->current().included , + .save = crl::guard(includeAdd, save), + .include = true, + }); + }); + + const auto included = lifetime.make_state< + rpl::variable<Data::BusinessChats> + >(data->current().included); + data->changes( + ) | rpl::start_with_next([=](const Data::BusinessRecipients &value) { + *included = value.included; + }, lifetime); + included->changes( + ) | rpl::start_with_next([=](Data::BusinessChats &&value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + }, lifetime); + + SetupBusinessChatsPreview(includeInner, excluded); + + includeWrap->toggleOn(data->value( + ) | rpl::map([](const Data::BusinessRecipients &value) { + return value.onlyIncluded; + })); + includeWrap->finishAnimating(); + + group->setChangedCallback([=](int value) { + change([&](Data::BusinessRecipients &data) { + data.onlyIncluded = (value == kSelectedOnly); + }); + }); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h new file mode 100644 index 000000000..60efd7425 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -0,0 +1,74 @@ +/* +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 "data/business/data_business_common.h" +#include "settings/settings_common_session.h" + +class FilterChatsPreview; + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +template <typename SectionType> +class BusinessSection : public Section<SectionType> { +public: + BusinessSection( + QWidget *parent, + not_null<Window::SessionController*> controller) + : Section<SectionType>(parent) + , _controller(controller) { + } + + [[nodiscard]] not_null<Window::SessionController*> controller() const { + return _controller; + } + [[nodiscard]] rpl::producer<> showFinishes() const { + return _showFinished.events(); + } + +private: + void showFinished() override { + _showFinished.fire({}); + } + + const not_null<Window::SessionController*> _controller; + rpl::event_stream<> _showFinished; + +}; + +struct BusinessChatsDescriptor { + Data::BusinessChats current; + Fn<void(const Data::BusinessChats&)> save; + bool include = false; +}; +void EditBusinessChats( + not_null<Window::SessionController*> window, + BusinessChatsDescriptor &&descriptor); + +not_null<FilterChatsPreview*> SetupBusinessChatsPreview( + not_null<Ui::VerticalLayout*> container, + not_null<rpl::variable<Data::BusinessChats>*> data); + +struct BusinessRecipientsSelectorDescriptor { + not_null<Window::SessionController*> controller; + rpl::producer<QString> title; + not_null<rpl::variable<Data::BusinessRecipients>*> data; +}; +void AddBusinessRecipientsSelector( + not_null<Ui::VerticalLayout*> container, + BusinessRecipientsSelectorDescriptor &&descriptor); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp new file mode 100644 index 000000000..7b2e87873 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -0,0 +1,104 @@ +/* +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 "settings/business/settings_working_hours.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class WorkingHours : public BusinessSection<WorkingHours> { +public: + WorkingHours( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~WorkingHours(); + + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + +}; + +WorkingHours::WorkingHours( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +WorkingHours::~WorkingHours() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> WorkingHours::title() { + return tr::lng_hours_title(); +} + +void WorkingHours::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + AddDividerTextWithLottie(content, { + .lottie = u"hours"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_hours_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_hours_show(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single(false)); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + Ui::ResizeFitChild(this, content); +} + +void WorkingHours::save() { +} + +} // namespace + +Type WorkingHoursId() { + return WorkingHours::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.h b/Telegram/SourceFiles/settings/business/settings_working_hours.h new file mode 100644 index 000000000..213ef1488 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type WorkingHoursId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index f4eebf4ff..ef519dced 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -598,6 +598,8 @@ settingsChatbotsUsername: InputField(defaultMultiSelectSearchField) { settingsChatbotsAccess: Checkbox(defaultCheckbox) { textPosition: point(18px, 2px); } +settingsLocationAddress: InputField(defaultMultiSelectSearchField) { +} settingsChatbotsUsernameMargins: margins(20px, 8px, 20px, 8px); settingsChatbotsAccessMargins: margins(22px, 5px, 22px, 9px); settingsChatbotsAccessSkip: 4px; diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index fd56adb41..e72391272 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -14,7 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/business/settings_away_message.h" #include "settings/business/settings_chatbots.h" +#include "settings/business/settings_greeting.h" +#include "settings/business/settings_location.h" +#include "settings/business/settings_quick_replies.h" +#include "settings/business/settings_working_hours.h" #include "settings/settings_common_session.h" #include "settings/settings_premium.h" #include "ui/effects/gradient.h" @@ -354,11 +359,17 @@ void Business::setupContent() { Ui::AddSkip(content, st::settingsFromFileTop); AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { - switch (feature) { - case BusinessFeature::Chatbots: - _showOther.fire(Settings::ChatbotsId()); - break; - } + _showOther.fire([&] { + switch (feature) { + case BusinessFeature::AwayMessages: return AwayMessageId(); + case BusinessFeature::OpeningHours: return WorkingHoursId(); + case BusinessFeature::Location: return LocationId(); + case BusinessFeature::GreetingMessages: return GreetingId(); + case BusinessFeature::QuickReplies: return QuickRepliesId(); + case BusinessFeature::Chatbots: return ChatbotsId(); + } + Unexpected("Feature in Business::setupContent."); + }()); }); Ui::ResizeFitChild(this, content); diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 7e4ac4180..448061167 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -173,7 +173,10 @@ void AddDividerTextWithLottie( not_null<Ui::VerticalLayout*> container, DividerWithLottieDescriptor &&descriptor) { const auto divider = Ui::CreateChild<Ui::BoxContentDivider>( - container.get()); + container.get(), + 0, + st::boxDividerBg, + descriptor.parts); const auto verticalLayout = container->add( object_ptr<Ui::VerticalLayout>(container.get())); const auto size = descriptor.lottieSize.value_or( diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 00ecf2fe2..ea80b4573 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -161,6 +161,7 @@ struct DividerWithLottieDescriptor { rpl::producer<> showFinished; rpl::producer<TextWithEntities> about; std::optional<QMargins> aboutMargins; + RectParts parts = RectPart::Top | RectPart::Bottom; }; void AddDividerTextWithLottie( not_null<Ui::VerticalLayout*> container, From 1fe641c4582fd7c1b4f89447d4d11419ea30ab40 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 21 Feb 2024 17:54:01 +0400 Subject: [PATCH 043/108] Update API scheme to layer 176. --- Telegram/SourceFiles/api/api_editing.cpp | 3 +- Telegram/SourceFiles/api/api_polls.cpp | 6 ++- Telegram/SourceFiles/api/api_sending.cpp | 6 ++- Telegram/SourceFiles/api/api_updates.cpp | 6 ++- Telegram/SourceFiles/apiwrap.cpp | 18 ++++--- Telegram/SourceFiles/boxes/share_box.cpp | 3 +- .../data/data_scheduled_messages.cpp | 6 ++- Telegram/SourceFiles/data/data_session.cpp | 3 +- .../admin_log/history_admin_log_item.cpp | 3 +- .../media/stories/media_stories_share.cpp | 3 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 49 +++++++++++++++---- .../business/settings_away_message.cpp | 1 - .../settings/business/settings_location.cpp | 4 ++ .../settings/settings_privacy_controllers.cpp | 3 +- .../SourceFiles/window/window_peer_menu.cpp | 3 +- 15 files changed, 86 insertions(+), 31 deletions(-) diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 005effad9..126223bee 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -101,7 +101,8 @@ mtpRequestId EditMessage( inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())), MTPReplyMarkup(), sentEntities, - MTP_int(options.scheduled) + MTP_int(options.scheduled), + MTPint() // quick_reply_shortcut_id )).done([=]( const MTPUpdates &result, [[maybe_unused]] mtpRequestId requestId) { diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 7699894cf..58c959da5 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -84,7 +84,8 @@ void Polls::create( MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( @@ -173,7 +174,8 @@ void Polls::close(not_null<HistoryItem*> item) { PollDataToInputMedia(poll, true), MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), - MTP_int(0) // schedule_date + MTP_int(0), // schedule_date + MTPint() // quick_reply_shortcut_id )).done([=](const MTPUpdates &result) { _pollCloseRequestIds.erase(itemId); _session->updates().applyUpdates(result); diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 0e5d7c585..f06bba080 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -161,7 +161,8 @@ void SendExistingMedia( MTPReplyMarkup(), sentEntities, MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -325,7 +326,8 @@ bool SendDice(MessageToSend &message) { MTPReplyMarkup(), MTP_vector<MTPMessageEntity>(), MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 2b0450374..679faa063 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1133,7 +1133,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTP_int(d.vttl_period().value_or_empty())), + MTP_int(d.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id MessageFlags(), NewMessageType::Unread); } break; @@ -1166,7 +1167,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTP_int(d.vttl_period().value_or_empty())), + MTP_int(d.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id MessageFlags(), NewMessageType::Unread); } break; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 6766efe85..97400a701 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3264,7 +3264,8 @@ void ApiWrap::forwardMessages( peer->input, MTP_int(topMsgId), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut )).done([=](const MTPUpdates &result) { applyUpdates(result); if (shared && !--shared->requestsLeft) { @@ -3779,7 +3780,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTPReplyMarkup(), sentEntities, MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), done, fail); } else { histories.sendPreparedMessage( @@ -3795,7 +3797,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTPReplyMarkup(), sentEntities, MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), done, fail); } isFirst = false; @@ -3928,7 +3931,8 @@ void ApiWrap::sendInlineResult( MTP_long(data->getQueryId()), MTP_string(data->getId()), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4076,7 +4080,8 @@ void ApiWrap::sendMediaWithRandomId( MTPReplyMarkup(), sentEntities, MTP_int(options.scheduled), - (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()) + (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4174,7 +4179,8 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) { Data::Histories::ReplyToPlaceholder(), MTP_vector<MTPInputSingleMedia>(medias), MTP_int(album->options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); }, [=](const MTP::Error &error, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index dff268c26..37da38eee 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1558,7 +1558,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( peer->input, MTP_int(topMsgId), MTP_int(options.scheduled), - MTP_inputPeerEmpty() // send_as + MTP_inputPeerEmpty(), // send_as + MTPstring() // quick_reply_shortcut )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 9ea40996e..41f3cd22f 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -88,7 +88,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.vgrouped_id().value_or_empty()), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTP_int(data.vttl_period().value_or_empty())); + MTP_int(data.vttl_period().value_or_empty()), + MTPint()); // quick_reply_shortcut_id }); } @@ -238,7 +239,8 @@ void ScheduledMessages::sendNowSimpleMessage( MTPlong(), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTP_int(update.vttl_period().value_or_empty())), + MTP_int(update.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id localFlags, NewMessageType::Unread); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 4efd8fa8b..1b07cf568 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4470,7 +4470,8 @@ void Session::insertCheckedServiceNotification( MTPlong(), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTPint()), // ttl_period + MTPint(), // ttl_period + MTPint()), // quick_reply_shortcut_id localFlags, NewMessageType::Unread); } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 0642a1568..b71b0cd82 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -137,7 +137,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTP_long(0), // grouped_id MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTPint()); // ttl_period + MTPint(), // ttl_period + MTPint()); // quick_reply_shortcut_id }); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index deb291cc1..aa3ebb075 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -154,7 +154,8 @@ namespace Media::Stories { MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), - MTP_inputPeerEmpty() + MTP_inputPeerEmpty(), + MTPstring() // quick_reply_shortcut ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 4b453c9b8..2c4cca0ba 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -114,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#1e4c8a69 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int = Message; +message#a66c7efc flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -227,7 +227,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; +userFull#e218d7f0 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -403,6 +403,11 @@ updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = U updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update; updateSavedReactionTags#39c67432 = Update; updateSmsJob#f16269d4 job_id:string = Update; +updateQuickReplies#dc3b36d quick_replies:Vector<messages.QuickReply> = Update; +updateNewQuickReply#ad62c98d quick_reply:messages.QuickReply = Update; +updateDeleteQuickReply#53e6f1ec shortcut_id:int = Update; +updateQuickReplyMessage#3e050d0f message:Message = Update; +updateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector<int> = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1660,6 +1665,22 @@ smsjobs.status#2aee9191 flags:# allow_international:flags.0?true recent_sent:int smsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob; +businessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen; + +businessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector<BusinessWeeklyOpen> = BusinessWorkHours; + +businessLocation#be2bf843 geo_point:GeoPoint address:string = BusinessLocation; + +timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; + +help.timezonesListNotModified#970708cc = help.TimezonesList; +help.timezonesList#7b74ed71 timezones:Vector<Timezone> hash:int = help.TimezonesList; + +messages.quickReply#940ebc72 shortcut_id:int shortcut:string top_message:int count:int = messages.QuickReply; + +messages.quickReplies#7cd69880 quick_replies:Vector<messages.QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies; +messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1785,6 +1806,8 @@ account.updateColor#7cefa15d flags:# for_profile:flags.1?true color:flags.2?int account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; +account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool; +account.updateBusinessLocation#3dfd3b56 flags:# geo_point:flags.0?InputGeoPoint address:flags.0?string = Bool; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1826,9 +1849,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.sendMedia#72ccc23d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.forwardMessages#c661bbc4 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMessage#6854c960 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; +messages.sendMedia#ff5ff75d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; +messages.forwardMessages#d5ae95ce flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector<int> reason:ReportReason message:string = Bool; @@ -1871,9 +1894,9 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#f7bc68ba flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendInlineBotResult#e7bda5b7 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#48f71778 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates; +messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool; messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; @@ -1906,7 +1929,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#456e8987 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMultiMedia#87262568 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector<MessageRange>; @@ -2015,6 +2038,13 @@ messages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool; messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions; messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; +messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; +messages.reorderQuickReplies#60331907 order:Vector<int> = Bool; +messages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool; +messages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool; +messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages; +messages.sendQuickReplyMessages#33153ad4 peer:InputPeer shortcut_id:int = Updates; +messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2059,6 +2089,7 @@ help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; help.getPremiumPromo#b81b93d4 = help.PremiumPromo; help.getPeerColors#da80f42f hash:int = help.PeerColors; help.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors; +help.getTimezonesList#49b30240 hash:int = help.TimezonesList; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages; @@ -2267,4 +2298,4 @@ smsjobs.getStatus#10a698e8 = smsjobs.Status; smsjobs.getSmsJob#778d902f job_id:string = SmsJob; smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; -// LAYER 175 +// LAYER 176 diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index de2f1c454..0e22a4c2f 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -65,7 +65,6 @@ void AwayMessage::setupContent( //const auto current = controller->session().data().chatbots().current(); //_recipients = current.recipients; - AddDividerTextWithLottie(content, { .lottie = u"sleep"_q, .lottieSize = st::settingsCloudPasswordIconSize, diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index 84e8d4e4b..67a865696 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -88,6 +88,10 @@ void Location::setupContent( QString()), st::settingsChatbotsUsernameMargins); + showFinishes() | rpl::start_with_next([=] { + address->setFocus(); + }, address->lifetime()); + if (!mapSupported()) { AddDividerTextWithLottie(content, { .lottie = u"phone"_q, diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 503ea62e4..5828b2e0e 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -195,7 +195,8 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPlong(), // grouped_id MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTPint() // ttl_period + MTPint(), // ttl_period + MTPint() // quick_reply_shortcut_id ).match([&](const MTPDmessage &data) { return history->makeMessage( history->nextNonHistoryEntryId(), diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index bcba1a434..32f09ee80 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -132,7 +132,8 @@ void ShareBotGame( MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), MTP_int(0), // schedule_date - MTPInputPeer() // send_as + MTPInputPeer(), // send_as + MTPstring() // quick_reply_shortcut ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { history->session().api().sendMessageFail(error, history->peer); From 4d12f1c0ef2819a30380aab88bf4d24542ffbc39 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 21 Feb 2024 22:25:52 +0400 Subject: [PATCH 044/108] Initial working hours editing. --- Telegram/CMakeLists.txt | 3 + Telegram/Resources/langs/lang.strings | 8 + .../data/business/data_business_common.cpp | 158 +++++ .../data/business/data_business_common.h | 99 +++ .../data/business/data_business_info.cpp | 71 +++ .../data/business/data_business_info.h | 41 ++ Telegram/SourceFiles/data/data_session.cpp | 4 +- Telegram/SourceFiles/data/data_session.h | 5 + .../business/settings_working_hours.cpp | 580 +++++++++++++++++- Telegram/SourceFiles/settings/settings.style | 7 + .../settings/settings_business.cpp | 4 + 11 files changed, 978 insertions(+), 2 deletions(-) create mode 100644 Telegram/SourceFiles/data/business/data_business_common.cpp create mode 100644 Telegram/SourceFiles/data/business/data_business_info.cpp create mode 100644 Telegram/SourceFiles/data/business/data_business_info.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 9b3caa4ef..6010bf833 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -450,7 +450,10 @@ PRIVATE countries/countries_manager.h data/business/data_business_chatbots.cpp data/business/data_business_chatbots.h + data/business/data_business_common.cpp data/business/data_business_common.h + data/business/data_business_info.cpp + data/business/data_business_info.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 89508d8c5..dd5746e44 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2189,6 +2189,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_hours_saturday" = "Saturday"; "lng_hours_sunday" = "Sunday"; "lng_hours_closed" = "Closed"; +"lng_hours_open_full" = "Open 24 hours"; +"lng_hours_next_day" = "Next day, {time}"; +"lng_hours_time_zone_title" = "Choose Time Zone"; +"lng_hours_add_button" = "Add a Set of Hours"; +"lng_hours_opening" = "Opening Time"; +"lng_hours_closing" = "Closing Time"; +"lng_hours_remove" = "Remove"; +"lng_hours_about_day" = "Specify your working hours during the day."; "lng_replies_title" = "Quick Replies"; "lng_replies_about" = "Set up shortcuts with rich text and media to respond to messages faster."; diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp new file mode 100644 index 000000000..1de65c952 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -0,0 +1,158 @@ +/* +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 "data/business/data_business_common.h" + +namespace Data { +namespace { + +constexpr auto kDay = WorkingInterval::kDay; +constexpr auto kWeek = WorkingInterval::kWeek; +constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; + +[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) { + auto &list = intervals.list; + ranges::sort(list, ranges::less(), &WorkingInterval::start); + for (auto i = 0, count = int(list.size()); i != count; ++i) { + if (i && list[i].intersected(list[i - 1])) { + list[i - 1] = list[i - 1].united(list[i]); + list[i] = {}; + } + if (!list[i]) { + list.erase(list.begin() + i); + --i; + --count; + } + } + return intervals; +} + +[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) { + auto &list = intervals.list; + auto after = WorkingInterval{ kWeek, kWeek + kDay }; + while (!list.empty()) { + if (const auto tail = list.back().intersected(after)) { + list.back().end = tail.start; + if (!list.back()) { + list.pop_back(); + } + list.insert(begin(list), tail.shifted(-kWeek)); + } else { + break; + } + } + return intervals; +} + +} // namespace + +WorkingIntervals WorkingIntervals::normalized() const { + return SortAndMerge(MoveTailToFront(SortAndMerge(*this))); +} + +Data::WorkingIntervals ExtractDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex) { + Expects(dayIndex >= 0 && dayIndex < 7); + + auto result = Data::WorkingIntervals(); + auto &list = result.list; + for (const auto &interval : intervals.list) { + const auto now = interval.intersected( + { (dayIndex - 1) * kDay, (dayIndex + 2) * kDay }); + const auto after = interval.intersected( + { (dayIndex + 6) * kDay, (dayIndex + 9) * kDay }); + const auto before = interval.intersected( + { (dayIndex - 8) * kDay, (dayIndex - 5) * kDay }); + if (now) { + list.push_back(now.shifted(-dayIndex * kDay)); + } + if (after) { + list.push_back(after.shifted(-(dayIndex + 7) * kDay)); + } + if (before) { + list.push_back(before.shifted(-(dayIndex - 7) * kDay)); + } + } + result = result.normalized(); + + const auto outside = [&](Data::WorkingInterval interval) { + return (interval.end <= 0) || (interval.start >= kDay); + }; + list.erase(ranges::remove_if(list, outside), end(list)); + + if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) { + list.back() = { 0, kDay }; + } else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) { + list.back() = list.back().intersected({ 0, kDay }); + } + if (!list.empty() && list.front().start <= 0) { + if (list.front().start < 0 + && list.front().end <= kInNextDayMax + && list.front().start > -kDay) { + list.erase(begin(list)); + } else { + list.front() = list.front().intersected({ 0, kDay }); + if (!list.front()) { + list.erase(begin(list)); + } + } + } + + return result; +} + +Data::WorkingIntervals RemoveDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex) { + auto result = intervals.normalized(); + auto &list = result.list; + const auto day = Data::WorkingInterval{ 0, kDay }; + const auto shifted = day.shifted(dayIndex * kDay); + auto before = Data::WorkingInterval{ 0, shifted.start }; + auto after = Data::WorkingInterval{ shifted.end, kWeek }; + for (auto i = 0, count = int(list.size()); i != count; ++i) { + if (list[i].end <= shifted.start || list[i].start >= shifted.end) { + continue; + } else if (list[i].end <= shifted.start + kInNextDayMax + && (list[i].start < shifted.start + || (!dayIndex // This 'Sunday' finishing on next day <= 6:00. + && list[i].start == shifted.start + && list.back().end >= kWeek))) { + continue; + } else if (const auto first = list[i].intersected(before)) { + list[i] = first; + if (const auto second = list[i].intersected(after)) { + list.push_back(second); + } + } else if (const auto second = list[i].intersected(after)) { + list[i] = second; + } else { + list.erase(list.begin() + i); + --i; + --count; + } + } + return result.normalized(); +} + +Data::WorkingIntervals ReplaceDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex, + Data::WorkingIntervals replacement) { + auto result = RemoveDayIntervals(intervals, dayIndex); + const auto first = result.list.insert( + end(result.list), + begin(replacement.list), + end(replacement.list)); + for (auto &interval : ranges::subrange(first, end(result.list))) { + interval = interval.shifted(dayIndex * kDay); + } + return result.normalized(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 743ddaa12..41fcca431 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -42,4 +42,103 @@ struct BusinessRecipients { const BusinessRecipients &b) = default; }; +struct Timezone { + QString id; + QString name; + TimeId utcOffset = 0; + + friend inline bool operator==( + const Timezone &a, + const Timezone &b) = default; +}; + +struct Timezones { + std::vector<Timezone> list; + + friend inline bool operator==( + const Timezones &a, + const Timezones &b) = default; +};; + +struct WorkingInterval { + static constexpr auto kDay = 24 * 3600; + static constexpr auto kWeek = 7 * kDay; + static constexpr auto kInNextDayMax = 6 * 3600; + + TimeId start = 0; + TimeId end = 0; + + explicit operator bool() const { + return start < end; + } + + [[nodiscard]] WorkingInterval shifted(TimeId offset) const { + return { start + offset, end + offset }; + } + [[nodiscard]] WorkingInterval united(WorkingInterval other) const { + if (!*this) { + return other; + } else if (!other) { + return *this; + } + return { + std::min(start, other.start), + std::max(end, other.end), + }; + } + [[nodiscard]] WorkingInterval intersected(WorkingInterval other) const { + const auto result = WorkingInterval{ + std::max(start, other.start), + std::min(end, other.end), + }; + return result ? result : WorkingInterval(); + } + + friend inline bool operator==( + const WorkingInterval &a, + const WorkingInterval &b) = default; +}; + +struct WorkingIntervals { + std::vector<WorkingInterval> list; + + [[nodiscard]] WorkingIntervals normalized() const; + + explicit operator bool() const { + for (const auto &interval : list) { + if (interval) { + return true; + } + } + return false; + } + friend inline bool operator==( + const WorkingIntervals &a, + const WorkingIntervals &b) = default; +}; + +struct WorkingHours { + WorkingIntervals intervals; + QString timezoneId; + + [[nodiscard]] WorkingHours normalized() const { + return { intervals.normalized(), timezoneId }; + } + + friend inline bool operator==( + const WorkingHours &a, + const WorkingHours &b) = default; +}; + +[[nodiscard]] Data::WorkingIntervals ExtractDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex); +[[nodiscard]] Data::WorkingIntervals RemoveDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex); +[[nodiscard]] Data::WorkingIntervals ReplaceDayIntervals( + const Data::WorkingIntervals &intervals, + int dayIndex, + Data::WorkingIntervals replacement); + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp new file mode 100644 index 000000000..c4623bb1f --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -0,0 +1,71 @@ +/* +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 "data/business/data_business_info.h" + +#include "apiwrap.h" +#include "data/data_session.h" +#include "main/main_session.h" + +namespace Data { + +BusinessInfo::BusinessInfo(not_null<Session*> owner) +: _owner(owner) { +} + +BusinessInfo::~BusinessInfo() = default; + +const WorkingHours &BusinessInfo::workingHours() const { + return _workingHours.current(); +} + +rpl::producer<WorkingHours> BusinessInfo::workingHoursValue() const { + return _workingHours.value(); +} + +void BusinessInfo::saveWorkingHours(WorkingHours data) { + _workingHours = std::move(data); +} + +void BusinessInfo::preload() { + preloadTimezones(); +} + +void BusinessInfo::preloadTimezones() { + if (!_timezones.current().list.empty() || _timezonesRequestId) { + return; + } + _timezonesRequestId = _owner->session().api().request( + MTPhelp_GetTimezonesList(MTP_int(_timezonesHash)) + ).done([=](const MTPhelp_TimezonesList &result) { + result.match([&](const MTPDhelp_timezonesList &data) { + _timezonesHash = data.vhash().v; + const auto proj = [](const MTPtimezone &result) { + return Timezone{ + .id = qs(result.data().vid()), + .name = qs(result.data().vname()), + .utcOffset = result.data().vutc_offset().v, + }; + }; + _timezones = Timezones{ + .list = ranges::views::all( + data.vtimezones().v + ) | ranges::views::transform( + proj + ) | ranges::to_vector, + }; + }, [](const MTPDhelp_timezonesListNotModified &) { + }); + }).send(); +} + +rpl::producer<Timezones> BusinessInfo::timezonesValue() const { + const_cast<BusinessInfo*>(this)->preloadTimezones(); + return _timezones.value(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h new file mode 100644 index 000000000..f109165d7 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -0,0 +1,41 @@ +/* +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 "data/business/data_business_common.h" + +namespace Data { + +class Session; + +class BusinessInfo final { +public: + explicit BusinessInfo(not_null<Session*> owner); + ~BusinessInfo(); + + [[nodiscard]] const WorkingHours &workingHours() const; + [[nodiscard]] rpl::producer<WorkingHours> workingHoursValue() const; + void saveWorkingHours(WorkingHours data); + + void preload(); + void preloadTimezones(); + [[nodiscard]] rpl::producer<Timezones> timezonesValue() const; + +private: + const not_null<Session*> _owner; + + rpl::variable<WorkingHours> _workingHours; + + rpl::variable<Timezones> _timezones; + + mtpRequestId _timezonesRequestId = 0; + int32 _timezonesHash = 0; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 1b07cf568..37fb68e86 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_form_controller.h" #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name #include "data/business/data_business_chatbots.h" +#include "data/business/data_business_info.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -270,7 +271,8 @@ Session::Session(not_null<Main::Session*> session) , _customEmojiManager(std::make_unique<CustomEmojiManager>(this)) , _stories(std::make_unique<Stories>(this)) , _savedMessages(std::make_unique<SavedMessages>(this)) -, _chatbots(std::make_unique<Chatbots>(this)) { +, _chatbots(std::make_unique<Chatbots>(this)) +, _businessInfo(std::make_unique<BusinessInfo>(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 4fc7b1db1..aab939cb4 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -63,6 +63,7 @@ class CustomEmojiManager; class Stories; class SavedMessages; class Chatbots; +class BusinessInfo; struct ReactionId; struct RepliesReadTillUpdate { @@ -146,6 +147,9 @@ public: [[nodiscard]] Chatbots &chatbots() const { return *_chatbots; } + [[nodiscard]] BusinessInfo &businessInfo() const { + return *_businessInfo; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -1070,6 +1074,7 @@ private: const std::unique_ptr<Stories> _stories; const std::unique_ptr<SavedMessages> _savedMessages; const std::unique_ptr<Chatbots> _chatbots; + const std::unique_ptr<BusinessInfo> _businessInfo; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 7b2e87873..fbe4ccc60 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -7,22 +7,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_working_hours.h" +#include "base/event_filter.h" +#include "base/unixtime.h" #include "core/application.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/vertical_drum_picker.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { namespace { +constexpr auto kDay = Data::WorkingInterval::kDay; +constexpr auto kWeek = Data::WorkingInterval::kWeek; +constexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax; + class WorkingHours : public BusinessSection<WorkingHours> { public: WorkingHours( @@ -36,8 +49,492 @@ private: void setupContent(not_null<Window::SessionController*> controller); void save(); + rpl::variable<Data::WorkingHours> _hours; + }; +[[nodiscard]] QString TimezoneFullName(const Data::Timezone &data) { + const auto abs = std::abs(data.utcOffset); + const auto hours = abs / 3600; + const auto minutes = (abs % 3600) / 60; + const auto seconds = abs % 60; + const auto sign = (data.utcOffset < 0) ? '-' : '+'; + const auto prefix = u"(UTC"_q + + sign + + QString::number(hours) + + u":"_q + + QString::number(minutes).rightJustified(2, u'0') + + u")"_q; + return prefix + ' ' + data.name; +} + +[[nodiscard]] QString FindClosestTimezoneId( + const std::vector<Data::Timezone> &list) { + const auto local = QDateTime::currentDateTime(); + const auto utc = QDateTime(local.date(), local.time(), Qt::UTC); + const auto shift = base::unixtime::now() - (TimeId)::time(nullptr); + const auto delta = int(utc.toSecsSinceEpoch()) + - int(local.toSecsSinceEpoch()) + - shift; + const auto proj = [&](const Data::Timezone &value) { + auto distance = value.utcOffset - delta; + while (distance > 12 * 3600) { + distance -= 24 * 3600; + } + while (distance < -12 * 3600) { + distance += 24 * 3600; + } + return std::abs(distance); + }; + return ranges::min_element(list, ranges::less(), proj)->id; +} + +[[nodiscard]] QString FormatDayTime( + TimeId time, + bool showEndAsNextDay = false) { + const auto wrap = [](TimeId value) { + const auto hours = value / 3600; + const auto minutes = (value % 3600) / 60; + return QString::number(hours).rightJustified(2, u'0') + + ':' + + QString::number(minutes).rightJustified(2, u'0'); + }; + return (time > kDay || (showEndAsNextDay && time == kDay)) + ? tr::lng_hours_next_day(tr::now, lt_time, wrap(time - kDay)) + : wrap(time == kDay ? 0 : time); +} + +[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) { + auto result = QStringList(); + result.reserve(data.list.size()); + for (const auto &interval : data.list) { + const auto start = FormatDayTime(interval.start); + const auto end = FormatDayTime(interval.end); + result.push_back(start + u" - "_q + end); + } + return result.join(u", "_q); +} + +void EditTimeBox( + not_null<Ui::GenericBox*> box, + TimeId low, + TimeId high, + TimeId value, + Fn<void(TimeId)> save) { + Expects(low <= high); + + const auto values = (high - low + 60) / 60; + const auto startIndex = (value - low) / 60; + + const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>( + box, + st::settingsWorkingHoursPicker)); + + const auto font = st::boxTextFont; + const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; + auto paintCallback = [=]( + QPainter &p, + int index, + float64 y, + float64 distanceFromCenter, + int outerWidth) { + const auto r = QRectF(0, y, outerWidth, itemHeight); + const auto progress = std::abs(distanceFromCenter); + const auto revProgress = 1. - progress; + p.save(); + p.translate(r.center()); + constexpr auto kMinYScale = 0.2; + const auto yScale = kMinYScale + + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); + p.scale(1., yScale); + p.translate(-r.center()); + p.setOpacity(revProgress); + p.setFont(font); + p.setPen(st::defaultFlatLabel.textFg); + p.drawText(r, FormatDayTime(low + index * 60, true), style::al_center); + p.restore(); + }; + + const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>( + content, + std::move(paintCallback), + values, + itemHeight, + startIndex); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + picker->resize(s.width(), s.height()); + picker->moveToLeft((s.width() - picker->width()) / 2, 0); + }, content->lifetime()); + + content->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(content); + + p.fillRect(r, Qt::transparent); + + const auto lineRect = QRect( + 0, + content->height() / 2, + content->width(), + st::defaultInputField.borderActive); + p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); + p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); + }, content->lifetime()); + + base::install_event_filter(content, [=](not_null<QEvent*> e) { + if ((e->type() == QEvent::MouseButtonPress) + || (e->type() == QEvent::MouseButtonRelease) + || (e->type() == QEvent::MouseMove)) { + picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get())); + } else if (e->type() == QEvent::Wheel) { + picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get())); + } + return base::EventFilterResult::Continue; + }); + base::install_event_filter(box, [=](not_null<QEvent*> e) { + if (e->type() == QEvent::KeyPress) { + picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); + } + return base::EventFilterResult::Continue; + }); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(std::clamp(low + picker->index() * 60, low, high)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void EditDayBox( + not_null<Ui::GenericBox*> box, + rpl::producer<QString> title, + Data::WorkingIntervals intervals, + Fn<void(Data::WorkingIntervals)> save) { + box->setTitle(std::move(title)); + box->setWidth(st::boxWideWidth); + struct State { + rpl::variable<Data::WorkingIntervals> data; + }; + const auto state = box->lifetime().make_state<State>(State{ + .data = std::move(intervals), + }); + + const auto container = box->verticalLayout(); + const auto rows = container->add( + object_ptr<Ui::VerticalLayout>(container)); + const auto makeRow = [=]( + Data::WorkingInterval interval, + TimeId min, + TimeId max) { + auto result = object_ptr<Ui::VerticalLayout>(rows); + const auto raw = result.data(); + AddDivider(raw); + AddSkip(raw); + AddButtonWithLabel( + raw, + tr::lng_hours_opening(), + rpl::single(FormatDayTime(interval.start, true)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto max = std::max(min, interval.end - 60); + const auto now = std::clamp(interval.start, min, max); + const auto save = crl::guard(box, [=](TimeId value) { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + i->start = value; + state->data = now.normalized(); + } + }); + box->getDelegate()->show(Box(EditTimeBox, min, max, now, save)); + }); + AddButtonWithLabel( + raw, + tr::lng_hours_closing(), + rpl::single(FormatDayTime(interval.end, true)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto min = std::min(max, interval.start + 60); + const auto now = std::clamp(interval.end, min, max); + const auto save = crl::guard(box, [=](TimeId value) { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + i->end = value; + state->data = now.normalized(); + } + }); + box->getDelegate()->show(Box(EditTimeBox, min, max, now, save)); + }); + raw->add(object_ptr<Ui::SettingsButton>( + raw, + tr::lng_hours_remove(), + st::settingsAttentionButton + ))->setClickedCallback([=] { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + now.list.erase(i); + state->data = std::move(now); + } + }); + AddSkip(raw); + + return result; + }; + + const auto addWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + AddDivider(addWrap->entity()); + AddSkip(addWrap->entity()); + const auto add = addWrap->entity()->add( + object_ptr<Ui::SettingsButton>( + container, + tr::lng_hours_add_button(), + st::settingsButtonLightNoIcon)); + add->setClickedCallback([=] { + auto now = state->data.current(); + if (now.list.empty()) { + now.list.push_back({ 8 * 3600, 20 * 3600 }); + } else if (const auto last = now.list.back().end; last + 60 < kDay) { + const auto from = std::max( + std::min(last + 30 * 60, kDay - 30 * 60), + last + 60); + const auto till = std::min(from + 4 * 3600, kDay + 30 * 60); + now.list.push_back({ from, from + 4 * 3600 }); + } + state->data = std::move(now); + }); + + state->data.value( + ) | rpl::start_with_next([=](const Data::WorkingIntervals &data) { + const auto count = int(data.list.size()); + for (auto i = 0; i != count; ++i) { + const auto min = (i == 0) ? 0 : (data.list[i - 1].end + 60); + const auto max = (i == count - 1) + ? (kDay + kInNextDayMax) + : (data.list[i + 1].start - 60); + rows->insert(i, makeRow(data.list[i], min, max)); + if (rows->count() > i + 1) { + delete rows->widgetAt(i + 1); + } + } + while (rows->count() > count) { + delete rows->widgetAt(count); + } + rows->resizeToWidth(st::boxWideWidth); + addWrap->toggle(data.list.empty() + || data.list.back().end + 60 < kDay, anim::type::instant); + add->clearState(); + }, add->lifetime()); + addWrap->finishAnimating(); + + AddSkip(container); + AddDividerText(container, tr::lng_hours_about_day()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(state->data.current()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void ChooseTimezoneBox( + not_null<Ui::GenericBox*> box, + std::vector<Data::Timezone> list, + QString id, + Fn<void(QString)> save) { + Expects(!list.empty()); + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_hours_time_zone_title()); + + const auto height = st::boxWideWidth; + box->setMaxHeight(height); + + ranges::sort(list, ranges::less(), [](const Data::Timezone &value) { + return std::pair(value.utcOffset, value.name); + }); + + if (!ranges::contains(list, id, &Data::Timezone::id)) { + id = FindClosestTimezoneId(list); + } + const auto i = ranges::find(list, id, &Data::Timezone::id); + const auto value = int(i - begin(list)); + const auto group = std::make_shared<Ui::RadiobuttonGroup>(value); + const auto radioPadding = st::defaultCheckbox.margin; + const auto max = std::max(radioPadding.top(), radioPadding.bottom()); + auto index = 0; + auto padding = st::boxRowPadding + QMargins(0, max, 0, max); + auto selected = (Ui::Radiobutton*)nullptr; + for (const auto &entry : list) { + const auto button = box->addRow( + object_ptr<Ui::Radiobutton>( + box, + group, + index++, + TimezoneFullName(entry)), + padding); + if (index == value + 1) { + selected = button; + } + padding = st::boxRowPadding + QMargins(0, 0, 0, max); + } + if (selected) { + box->verticalLayout()->resizeToWidth(st::boxWideWidth); + const auto y = selected->y() - (height - selected->height()) / 2; + box->setInitScrollCallback([=] { + box->scrollToY(y); + }); + } + group->setChangedCallback([=](int index) { + const auto weak = Ui::MakeWeak(box); + save(list[index].id); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); +} + +void AddWeekButton( + not_null<Ui::VerticalLayout*> container, + not_null<Window::SessionController*> controller, + int index, + not_null<rpl::variable<Data::WorkingHours>*> data) { + auto label = [&] { + switch (index) { + case 0: return tr::lng_hours_monday(); + case 1: return tr::lng_hours_tuesday(); + case 2: return tr::lng_hours_wednesday(); + case 3: return tr::lng_hours_thursday(); + case 4: return tr::lng_hours_friday(); + case 5: return tr::lng_hours_saturday(); + case 6: return tr::lng_hours_sunday(); + } + Unexpected("Index in AddWeekButton."); + }(); + const auto &st = st::settingsWorkingHoursWeek; + const auto button = AddButtonWithIcon( + container, + rpl::duplicate(label), + st); + button->setClickedCallback([=] { + const auto done = [=](Data::WorkingIntervals intervals) { + auto now = data->current(); + now.intervals = ReplaceDayIntervals( + now.intervals, + index, + std::move(intervals)); + *data = now.normalized(); + }; + controller->show(Box( + EditDayBox, + rpl::duplicate(label), + ExtractDayIntervals(data->current().intervals, index), + crl::guard(button, done))); + }); + + const auto toggleButton = Ui::CreateChild<Ui::SettingsButton>( + container.get(), + nullptr, + st); + const auto checkView = button->lifetime().make_state<Ui::ToggleView>( + st.toggle, + false, + [=] { toggleButton->update(); }); + + auto status = data->value( + ) | rpl::map([=](const Data::WorkingHours &data) { + using namespace Data; + + const auto intervals = ExtractDayIntervals(data.intervals, index); + const auto empty = intervals.list.empty(); + if (checkView->checked() == empty) { + checkView->setChecked(!empty, anim::type::instant); + } + if (!intervals) { + return tr::lng_hours_closed(); + } else if (intervals.list.front() == WorkingInterval{ 0, kDay }) { + return tr::lng_hours_open_full(); + } + return rpl::single(JoinIntervals(intervals)); + }) | rpl::flatten_latest(); + const auto details = Ui::CreateChild<Ui::FlatLabel>( + button.get(), + std::move(status), + st::settingsWorkingHoursDetails); + details->show(); + details->moveToLeft( + st.padding.left(), + st.padding.top() + st.height - details->height()); + details->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto separator = Ui::CreateChild<Ui::RpWidget>(container.get()); + separator->paintRequest( + ) | rpl::start_with_next([=, bg = st.textBgOver] { + auto p = QPainter(separator); + p.fillRect(separator->rect(), bg); + }, separator->lifetime()); + const auto separatorHeight = st.height - 2 * st.toggle.border; + button->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + const auto w = st::rightsButtonToggleWidth; + toggleButton->setGeometry( + r.x() + r.width() - w, + r.y(), + w, + r.height()); + separator->setGeometry( + toggleButton->x() - st::lineWidth, + r.y() + (r.height() - separatorHeight) / 2, + st::lineWidth, + separatorHeight); + }, toggleButton->lifetime()); + + const auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton); + checkWidget->resize(checkView->getSize()); + checkWidget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(checkWidget); + checkView->paint(p, 0, 0, checkWidget->width()); + }, checkWidget->lifetime()); + toggleButton->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + checkWidget->moveToRight( + st.toggleSkip, + (s.height() - checkWidget->height()) / 2); + }, toggleButton->lifetime()); + + toggleButton->setClickedCallback([=] { + const auto enabled = !checkView->checked(); + checkView->setChecked(enabled, anim::type::normal); + auto now = data->current(); + now.intervals = ReplaceDayIntervals( + now.intervals, + index, + (enabled + ? Data::WorkingIntervals{ { { 0, kDay } } } + : Data::WorkingIntervals())); + *data = now.normalized(); + }); +} + WorkingHours::WorkingHours( QWidget *parent, not_null<Window::SessionController*> controller) @@ -56,11 +553,21 @@ rpl::producer<QString> WorkingHours::title() { } void WorkingHours::setupContent( - not_null<Window::SessionController*> controller) { + not_null<Window::SessionController*> controller) { using namespace rpl::mappers; const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + struct State { + rpl::variable<Data::Timezones> timezones; + bool timezoneEditPending = false; + }; + const auto info = &controller->session().data().businessInfo(); + const auto state = content->lifetime().make_state<State>(State{ + .timezones = info->timezonesValue(), + }); + _hours = info->workingHours(); + AddDividerTextWithLottie(content, { .lottie = u"hours"_q, .lottieSize = st::settingsCloudPasswordIconSize, @@ -85,6 +592,75 @@ void WorkingHours::setupContent( Ui::AddSkip(inner); Ui::AddDivider(inner); + Ui::AddSkip(inner); + + for (auto i = 0; i != 7; ++i) { + AddWeekButton(inner, controller, i, &_hours); + } + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + state->timezones.value( + ) | rpl::filter([=](const Data::Timezones &value) { + return !value.list.empty(); + }) | rpl::start_with_next([=](const Data::Timezones &value) { + const auto now = _hours.current().timezoneId; + if (!ranges::contains(value.list, now, &Data::Timezone::id)) { + auto copy = _hours.current(); + copy.timezoneId = FindClosestTimezoneId(value.list); + _hours = std::move(copy); + } + }, inner->lifetime()); + + auto timezoneLabel = rpl::combine( + _hours.value(), + state->timezones.value() + ) | rpl::map([]( + const Data::WorkingHours &hours, + const Data::Timezones &timezones) { + const auto i = ranges::find( + timezones.list, + hours.timezoneId, + &Data::Timezone::id); + return (i != end(timezones.list)) ? TimezoneFullName(*i) : QString(); + }); + const auto editTimezone = [=](const std::vector<Data::Timezone> &list) { + const auto was = _hours.current().timezoneId; + controller->show(Box(ChooseTimezoneBox, list, was, [=](QString id) { + if (id != was) { + auto copy = _hours.current(); + copy.timezoneId = id; + _hours = std::move(copy); + } + })); + }; + AddButtonWithLabel( + inner, + tr::lng_hours_time_zone(), + std::move(timezoneLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto &list = state->timezones.current().list; + if (!list.empty()) { + editTimezone(list); + } else { + state->timezoneEditPending = true; + } + }); + + if (state->timezones.current().list.empty()) { + state->timezones.value( + ) | rpl::filter([](const Data::Timezones &value) { + return !value.list.empty(); + }) | rpl::start_with_next([=](const Data::Timezones &value) { + if (state->timezoneEditPending) { + state->timezoneEditPending = false; + editTimezone(value.list); + } + }, inner->lifetime()); + } wrap->toggleOn(enabled->toggledValue()); wrap->finishAnimating(); @@ -93,6 +669,8 @@ void WorkingHours::setupContent( } void WorkingHours::save() { + controller()->session().data().businessInfo().saveWorkingHours( + _hours.current()); } } // namespace diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index ef519dced..3e57d64cc 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -607,3 +607,10 @@ settingsChatbotsBottomTextMargin: margins(22px, 8px, 22px, 3px); settingsChatbotsAdd: SettingsButton(settingsButton) { iconLeft: 22px; } +settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) { + height: 40px; + padding: margins(22px, 4px, 22px, 4px); +} +settingsWorkingHoursDetails: settingsNotificationTypeDetails; +settingsWorkingHoursPicker: 200px; +settingsWorkingHoursPickerItemHeight: 40px; diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index e72391272..f93646606 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "core/click_handler_types.h" #include "data/data_peer_values.h" // AmPremiumValue. +#include "data/data_session.h" +#include "data/business/data_business_info.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -356,6 +358,8 @@ void Business::setStepDataReference(std::any &data) { void Business::setupContent() { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + _controller->session().data().businessInfo().preloadTimezones(); + Ui::AddSkip(content, st::settingsFromFileTop); AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { From dd0bdd62fb9208b1a553694f54916ebbcb5db794 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 22 Feb 2024 22:00:11 +0400 Subject: [PATCH 045/108] Support business working hours API. --- .../data/business/data_business_common.cpp | 26 ++++----- .../data/business/data_business_common.h | 45 ++++++++++++--- .../data/business/data_business_info.cpp | 40 ++++++++++--- .../data/business/data_business_info.h | 4 -- Telegram/SourceFiles/data/data_changes.h | 57 ++++++++++--------- Telegram/SourceFiles/data/data_location.h | 2 +- Telegram/SourceFiles/data/data_user.cpp | 51 +++++++++++++++++ Telegram/SourceFiles/data/data_user.h | 7 +++ .../business/settings_working_hours.cpp | 10 +++- 9 files changed, 177 insertions(+), 65 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 1de65c952..956807f5d 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -18,7 +18,7 @@ constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; auto &list = intervals.list; ranges::sort(list, ranges::less(), &WorkingInterval::start); for (auto i = 0, count = int(list.size()); i != count; ++i) { - if (i && list[i].intersected(list[i - 1])) { + if (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) { list[i - 1] = list[i - 1].united(list[i]); list[i] = {}; } @@ -54,12 +54,12 @@ WorkingIntervals WorkingIntervals::normalized() const { return SortAndMerge(MoveTailToFront(SortAndMerge(*this))); } -Data::WorkingIntervals ExtractDayIntervals( - const Data::WorkingIntervals &intervals, +WorkingIntervals ExtractDayIntervals( + const WorkingIntervals &intervals, int dayIndex) { Expects(dayIndex >= 0 && dayIndex < 7); - auto result = Data::WorkingIntervals(); + auto result = WorkingIntervals(); auto &list = result.list; for (const auto &interval : intervals.list) { const auto now = interval.intersected( @@ -80,7 +80,7 @@ Data::WorkingIntervals ExtractDayIntervals( } result = result.normalized(); - const auto outside = [&](Data::WorkingInterval interval) { + const auto outside = [&](WorkingInterval interval) { return (interval.end <= 0) || (interval.start >= kDay); }; list.erase(ranges::remove_if(list, outside), end(list)); @@ -106,15 +106,15 @@ Data::WorkingIntervals ExtractDayIntervals( return result; } -Data::WorkingIntervals RemoveDayIntervals( - const Data::WorkingIntervals &intervals, +WorkingIntervals RemoveDayIntervals( + const WorkingIntervals &intervals, int dayIndex) { auto result = intervals.normalized(); auto &list = result.list; - const auto day = Data::WorkingInterval{ 0, kDay }; + const auto day = WorkingInterval{ 0, kDay }; const auto shifted = day.shifted(dayIndex * kDay); - auto before = Data::WorkingInterval{ 0, shifted.start }; - auto after = Data::WorkingInterval{ shifted.end, kWeek }; + auto before = WorkingInterval{ 0, shifted.start }; + auto after = WorkingInterval{ shifted.end, kWeek }; for (auto i = 0, count = int(list.size()); i != count; ++i) { if (list[i].end <= shifted.start || list[i].start >= shifted.end) { continue; @@ -140,10 +140,10 @@ Data::WorkingIntervals RemoveDayIntervals( return result.normalized(); } -Data::WorkingIntervals ReplaceDayIntervals( - const Data::WorkingIntervals &intervals, +WorkingIntervals ReplaceDayIntervals( + const WorkingIntervals &intervals, int dayIndex, - Data::WorkingIntervals replacement) { + WorkingIntervals replacement) { auto result = RemoveDayIntervals(intervals, dayIndex); const auto first = result.list.insert( end(result.list), diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 41fcca431..be281d0ad 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/flags.h" +#include "data/data_location.h" class UserData; @@ -125,20 +126,50 @@ struct WorkingHours { return { intervals.normalized(), timezoneId }; } + explicit operator bool() const { + return !timezoneId.isEmpty(); + } + friend inline bool operator==( const WorkingHours &a, const WorkingHours &b) = default; }; -[[nodiscard]] Data::WorkingIntervals ExtractDayIntervals( - const Data::WorkingIntervals &intervals, +[[nodiscard]] WorkingIntervals ExtractDayIntervals( + const WorkingIntervals &intervals, int dayIndex); -[[nodiscard]] Data::WorkingIntervals RemoveDayIntervals( - const Data::WorkingIntervals &intervals, +[[nodiscard]] WorkingIntervals RemoveDayIntervals( + const WorkingIntervals &intervals, int dayIndex); -[[nodiscard]] Data::WorkingIntervals ReplaceDayIntervals( - const Data::WorkingIntervals &intervals, +[[nodiscard]] WorkingIntervals ReplaceDayIntervals( + const WorkingIntervals &intervals, int dayIndex, - Data::WorkingIntervals replacement); + WorkingIntervals replacement); + +struct BusinessLocation { + QString address; + LocationPoint point; + + explicit operator bool() const { + return !address.isEmpty(); + } + + friend inline bool operator==( + const BusinessLocation &a, + const BusinessLocation &b) = default; +}; + +struct BusinessDetails { + WorkingHours hours; + BusinessLocation location; + + explicit operator bool() const { + return hours || location; + } + + friend inline bool operator==( + const BusinessDetails &a, + const BusinessDetails &b) = default; +}; } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index c4623bb1f..804b23deb 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -8,10 +8,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_business_info.h" #include "apiwrap.h" +#include "data/business/data_business_common.h" #include "data/data_session.h" +#include "data/data_user.h" #include "main/main_session.h" namespace Data { +namespace { + +[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) { + const auto list = data.intervals.normalized().list; + const auto proj = [](const WorkingInterval &data) { + return MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen( + MTP_int(data.start / 60), + MTP_int(data.end / 60))); + }; + return MTP_businessWorkHours( + MTP_flags(0), + MTP_string(data.timezoneId), + MTP_vector_from_range(list | ranges::views::transform(proj))); +} + +} // namespace BusinessInfo::BusinessInfo(not_null<Session*> owner) : _owner(owner) { @@ -19,16 +37,20 @@ BusinessInfo::BusinessInfo(not_null<Session*> owner) BusinessInfo::~BusinessInfo() = default; -const WorkingHours &BusinessInfo::workingHours() const { - return _workingHours.current(); -} - -rpl::producer<WorkingHours> BusinessInfo::workingHoursValue() const { - return _workingHours.value(); -} - void BusinessInfo::saveWorkingHours(WorkingHours data) { - _workingHours = std::move(data); + auto details = _owner->session().user()->businessDetails(); + if (details.hours == data) { + return; + } + details.hours = std::move(data); + + using Flag = MTPaccount_UpdateBusinessWorkHours::Flag; + _owner->session().api().request(MTPaccount_UpdateBusinessWorkHours( + MTP_flags(details.hours ? Flag::f_business_work_hours : Flag()), + ToMTP(details.hours) + )).send(); + + _owner->session().user()->setBusinessDetails(std::move(details)); } void BusinessInfo::preload() { diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index f109165d7..ee4a2e043 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -18,8 +18,6 @@ public: explicit BusinessInfo(not_null<Session*> owner); ~BusinessInfo(); - [[nodiscard]] const WorkingHours &workingHours() const; - [[nodiscard]] rpl::producer<WorkingHours> workingHoursValue() const; void saveWorkingHours(WorkingHours data); void preload(); @@ -29,8 +27,6 @@ public: private: const not_null<Session*> _owner; - rpl::variable<WorkingHours> _workingHours; - rpl::variable<Timezones> _timezones; mtpRequestId _timezonesRequestId = 0; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2c29ef8fc..bd85b02fa 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -73,42 +73,43 @@ struct PeerUpdate { TranslationDisabled = (1ULL << 13), Color = (1ULL << 14), BackgroundEmoji = (1ULL << 15), + StoriesState = (1ULL << 16), // For users - CanShareContact = (1ULL << 16), - IsContact = (1ULL << 17), - PhoneNumber = (1ULL << 18), - OnlineStatus = (1ULL << 19), - BotCommands = (1ULL << 20), - BotCanBeInvited = (1ULL << 21), - BotStartToken = (1ULL << 22), - CommonChats = (1ULL << 23), - HasCalls = (1ULL << 24), - SupportInfo = (1ULL << 25), - IsBot = (1ULL << 26), - EmojiStatus = (1ULL << 27), - StoriesState = (1ULL << 28), + CanShareContact = (1ULL << 17), + IsContact = (1ULL << 18), + PhoneNumber = (1ULL << 19), + OnlineStatus = (1ULL << 20), + BotCommands = (1ULL << 21), + BotCanBeInvited = (1ULL << 22), + BotStartToken = (1ULL << 23), + CommonChats = (1ULL << 24), + HasCalls = (1ULL << 25), + SupportInfo = (1ULL << 26), + IsBot = (1ULL << 27), + EmojiStatus = (1ULL << 28), + BusinessDetails = (1ULL << 29), // For chats and channels - InviteLinks = (1ULL << 29), - Members = (1ULL << 30), - Admins = (1ULL << 31), - BannedUsers = (1ULL << 32), - Rights = (1ULL << 33), - PendingRequests = (1ULL << 34), - Reactions = (1ULL << 35), + InviteLinks = (1ULL << 30), + Members = (1ULL << 31), + Admins = (1ULL << 32), + BannedUsers = (1ULL << 33), + Rights = (1ULL << 34), + PendingRequests = (1ULL << 35), + Reactions = (1ULL << 36), // For channels - ChannelAmIn = (1ULL << 36), - StickersSet = (1ULL << 37), - EmojiSet = (1ULL << 38), - ChannelLinkedChat = (1ULL << 39), - ChannelLocation = (1ULL << 40), - Slowmode = (1ULL << 41), - GroupCall = (1ULL << 42), + ChannelAmIn = (1ULL << 37), + StickersSet = (1ULL << 38), + EmojiSet = (1ULL << 39), + ChannelLinkedChat = (1ULL << 40), + ChannelLocation = (1ULL << 41), + Slowmode = (1ULL << 42), + GroupCall = (1ULL << 43), // For iteration - LastUsedBit = (1ULL << 42), + LastUsedBit = (1ULL << 43), }; using Flags = base::flags<Flag>; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h index 7d9a59b4a..a5e0090db 100644 --- a/Telegram/SourceFiles/data/data_location.h +++ b/Telegram/SourceFiles/data/data_location.h @@ -26,7 +26,6 @@ public: [[nodiscard]] size_t hash() const; -private: friend inline bool operator==( const LocationPoint &a, const LocationPoint &b) { @@ -39,6 +38,7 @@ private: return (a._lat < b._lat) || ((a._lat == b._lat) && (a._lon < b._lon)); } +private: float64 _lat = 0; float64 _lon = 0; uint64 _access = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 5014bdfb9..45f4f9e28 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "storage/storage_user_photos.h" #include "main/main_session.h" +#include "data/business/data_business_common.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -30,6 +31,34 @@ constexpr auto kSetOnlineAfterActivity = TimeId(30); using UpdateFlag = Data::PeerUpdate::Flag; +[[nodiscard]] Data::BusinessDetails FromMTP( + const tl::conditional<MTPBusinessWorkHours> &hours, + const tl::conditional<MTPBusinessLocation> &location) { + auto result = Data::BusinessDetails(); + if (hours) { + const auto &data = hours->data(); + result.hours.timezoneId = qs(data.vtimezone_id()); + result.hours.intervals.list = ranges::views::all( + data.vweekly_open().v + ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { + const auto &data = open.data(); + return Data::WorkingInterval{ + data.vstart_minute().v * 60, + data.vend_minute().v * 60, + }; + }) | ranges::to_vector; + } + if (location) { + const auto &data = location->data(); + result.location.address = qs(data.vaddress()); + data.vgeo_point().match([&](const MTPDgeoPoint &data) { + result.location.point = Data::LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } + return result; +} + } // namespace BotInfo::BotInfo() = default; @@ -62,6 +91,8 @@ UserData::UserData(not_null<Data::Session*> owner, PeerId id) , _flags((id == owner->session().userPeerId()) ? Flag::Self : Flag(0)) { } +UserData::~UserData() = default; + bool UserData::canShareThisContact() const { return canShareThisContactFast() || !owner().findContactPhone(peerToUser(id)).isEmpty(); @@ -174,6 +205,22 @@ void UserData::setStoriesState(StoriesState state) { } } +const Data::BusinessDetails &UserData::businessDetails() const { + static const auto empty = Data::BusinessDetails(); + return _businessDetails ? *_businessDetails : empty; +} + +void UserData::setBusinessDetails(Data::BusinessDetails details) { + if ((!details && !_businessDetails) + || (details && _businessDetails && details == *_businessDetails)) { + return; + } + _businessDetails = details + ? std::make_unique<Data::BusinessDetails>(std::move(details)) + : nullptr; + session().changes().peerUpdated(this, UpdateFlag::BusinessDetails); +} + void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) { bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty(); @@ -572,6 +619,10 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) { user->setWallPaper({}); } + user->setBusinessDetails(FromMTP( + update.vbusiness_work_hours(), + update.vbusiness_location())); + user->owner().stories().apply(user, update.vstories()); user->fullUpdated(); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index f6ba39749..cf9eafef7 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct BotCommand; +struct BusinessDetails; } // namespace Data struct BotInfo { @@ -84,6 +85,8 @@ public: using Flags = Data::Flags<UserDataFlags>; UserData(not_null<Data::Session*> owner, PeerId id); + ~UserData(); + void setPhoto(const MTPUserProfilePhoto &photo); void setName( @@ -192,6 +195,9 @@ public: [[nodiscard]] bool hasUnreadStories() const; void setStoriesState(StoriesState state); + [[nodiscard]] const Data::BusinessDetails &businessDetails() const; + void setBusinessDetails(Data::BusinessDetails details); + private: auto unavailableReasons() const -> const std::vector<Data::UnavailableReason> & override; @@ -201,6 +207,7 @@ private: Data::UsernamesInfo _username; + std::unique_ptr<Data::BusinessDetails> _businessDetails; std::vector<Data::UnavailableReason> _unavailableReasons; QString _phone; QString _privateForwardName; diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index fbe4ccc60..42865a2c0 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/business/data_business_info.h" #include "data/data_session.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" @@ -50,6 +51,7 @@ private: void save(); rpl::variable<Data::WorkingHours> _hours; + rpl::variable<bool> _enabled; }; @@ -566,7 +568,7 @@ void WorkingHours::setupContent( const auto state = content->lifetime().make_state<State>(State{ .timezones = info->timezonesValue(), }); - _hours = info->workingHours(); + _hours = controller->session().user()->businessDetails().hours; AddDividerTextWithLottie(content, { .lottie = u"hours"_q, @@ -582,7 +584,9 @@ void WorkingHours::setupContent( content, tr::lng_hours_show(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(false)); + ))->toggleOn(rpl::single(bool(_hours.current()))); + + _enabled = enabled->toggledValue(); const auto wrap = content->add( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( @@ -670,7 +674,7 @@ void WorkingHours::setupContent( void WorkingHours::save() { controller()->session().data().businessInfo().saveWorkingHours( - _hours.current()); + _enabled.current() ? _hours.current() : Data::WorkingHours()); } } // namespace From c5139069969ae8f14b86a396b34b48924a758467 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 09:17:13 +0400 Subject: [PATCH 046/108] Change default for business recipients. --- Telegram/SourceFiles/data/business/data_business_common.h | 2 +- .../settings/business/settings_recipients_helper.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index be281d0ad..c480a8579 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -36,7 +36,7 @@ struct BusinessChats { struct BusinessRecipients { BusinessChats included; BusinessChats excluded; - bool onlyIncluded = false; + bool allButExcluded = false; friend inline bool operator==( const BusinessRecipients &a, diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index a6288dbee..83c11a9cd 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -165,7 +165,7 @@ void AddBusinessRecipientsSelector( *data = std::move(now); }; const auto group = std::make_shared<Ui::RadiobuttonGroup>( - data->current().onlyIncluded ? kSelectedOnly : kAllExcept); + data->current().allButExcluded ? kAllExcept : kSelectedOnly); const auto everyone = container->add( object_ptr<Ui::Radiobutton>( container, @@ -231,7 +231,7 @@ void AddBusinessRecipientsSelector( excludeWrap->toggleOn(data->value( ) | rpl::map([](const Data::BusinessRecipients &value) { - return !value.onlyIncluded; + return value.allButExcluded; })); excludeWrap->finishAnimating(); @@ -280,13 +280,13 @@ void AddBusinessRecipientsSelector( includeWrap->toggleOn(data->value( ) | rpl::map([](const Data::BusinessRecipients &value) { - return value.onlyIncluded; + return !value.allButExcluded; })); includeWrap->finishAnimating(); group->setChangedCallback([=](int value) { change([&](Data::BusinessRecipients &data) { - data.onlyIncluded = (value == kSelectedOnly); + data.allButExcluded = (value == kAllExcept); }); }); } From e05eb63476a6638423609d7f7c9aff8f23dbf3ce Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 13:26:59 +0400 Subject: [PATCH 047/108] Update API scheme on layer 176. --- Telegram/SourceFiles/api/api_polls.cpp | 2 +- Telegram/SourceFiles/api/api_sending.cpp | 4 +- Telegram/SourceFiles/apiwrap.cpp | 12 +++--- Telegram/SourceFiles/boxes/share_box.cpp | 2 +- .../media/stories/media_stories_share.cpp | 2 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 42 +++++++++++++------ .../SourceFiles/window/window_peer_menu.cpp | 2 +- 7 files changed, 42 insertions(+), 24 deletions(-) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 58c959da5..f465d7f37 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -85,7 +85,7 @@ void Polls::create( MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index f06bba080..babce9329 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -162,7 +162,7 @@ void SendExistingMedia( sentEntities, MTP_int(message.action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -327,7 +327,7 @@ bool SendDice(MessageToSend &message) { MTP_vector<MTPMessageEntity>(), MTP_int(message.action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 97400a701..0d603a044 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3265,7 +3265,7 @@ void ApiWrap::forwardMessages( MTP_int(topMsgId), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() )).done([=](const MTPUpdates &result) { applyUpdates(result); if (shared && !--shared->requestsLeft) { @@ -3781,7 +3781,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(message.action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), done, fail); } else { histories.sendPreparedMessage( @@ -3798,7 +3798,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), done, fail); } isFirst = false; @@ -3932,7 +3932,7 @@ void ApiWrap::sendInlineResult( MTP_string(data->getId()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4081,7 +4081,7 @@ void ApiWrap::sendMediaWithRandomId( sentEntities, MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4180,7 +4180,7 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) { MTP_vector<MTPInputSingleMedia>(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); }, [=](const MTP::Error &error, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 37da38eee..8577aad62 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1559,7 +1559,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(topMsgId), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index aa3ebb075..05fceb77b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -155,7 +155,7 @@ namespace Media::Stories { MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), MTP_inputPeerEmpty(), - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 2c4cca0ba..9fdf2c153 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -227,7 +227,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#e218d7f0 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation = UserFull; +userFull#22ff3e85 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -403,8 +403,8 @@ updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = U updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update; updateSavedReactionTags#39c67432 = Update; updateSmsJob#f16269d4 job_id:string = Update; -updateQuickReplies#dc3b36d quick_replies:Vector<messages.QuickReply> = Update; -updateNewQuickReply#ad62c98d quick_reply:messages.QuickReply = Update; +updateQuickReplies#f9470ab2 quick_replies:Vector<QuickReply> = Update; +updateNewQuickReply#f53da717 quick_reply:QuickReply = Update; updateDeleteQuickReply#53e6f1ec shortcut_id:int = Update; updateQuickReplyMessage#3e050d0f message:Message = Update; updateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector<int> = Update; @@ -1669,16 +1669,31 @@ businessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen businessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector<BusinessWeeklyOpen> = BusinessWorkHours; -businessLocation#be2bf843 geo_point:GeoPoint address:string = BusinessLocation; +businessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation; + +businessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule; +businessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule; +businessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule; + +inputBusinessGreetingMessage#7d4a3609 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int users:flags.4?Vector<InputUser> no_activity_days:int = InputBusinessGreetingMessage; + +businessGreetingMessage#a098d54c flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int users:flags.4?Vector<long> no_activity_days:int = BusinessGreetingMessage; + +inputBusinessAwayMessage#ce6fda48 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int schedule:BusinessAwayMessageSchedule users:flags.4?Vector<InputUser> = InputBusinessAwayMessage; + +businessAwayMessage#9acd7a15 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int schedule:BusinessAwayMessageSchedule users:flags.4?Vector<long> = BusinessAwayMessage; timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; help.timezonesListNotModified#970708cc = help.TimezonesList; help.timezonesList#7b74ed71 timezones:Vector<Timezone> hash:int = help.TimezonesList; -messages.quickReply#940ebc72 shortcut_id:int shortcut:string top_message:int count:int = messages.QuickReply; +quickReply#697102b shortcut_id:int shortcut:string top_message:int count:int = QuickReply; -messages.quickReplies#7cd69880 quick_replies:Vector<messages.QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies; +inputQuickReplyShortcut#24596d41 shortcut:string = InputQuickReplyShortcut; +inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; + +messages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; ---functions--- @@ -1807,7 +1822,9 @@ account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool; -account.updateBusinessLocation#3dfd3b56 flags:# geo_point:flags.0?InputGeoPoint address:flags.0?string = Bool; +account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; +account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; +account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1849,9 +1866,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#6854c960 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; -messages.sendMedia#ff5ff75d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; -messages.forwardMessages#d5ae95ce flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; +messages.sendMessage#dff8042c flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMedia#7bd66041 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector<int> reason:ReportReason message:string = Bool; @@ -1894,7 +1911,7 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#e7bda5b7 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; +messages.sendInlineBotResult#3ebee86a flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool; @@ -1929,7 +1946,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#87262568 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?string = Updates; +messages.sendMultiMedia#c964709 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector<MessageRange>; @@ -2042,6 +2059,7 @@ messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; messages.reorderQuickReplies#60331907 order:Vector<int> = Bool; messages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool; messages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool; +messages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool; messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages; messages.sendQuickReplyMessages#33153ad4 peer:InputPeer shortcut_id:int = Updates; messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 32f09ee80..44ddebc9d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -133,7 +133,7 @@ void ShareBotGame( MTPVector<MTPMessageEntity>(), MTP_int(0), // schedule_date MTPInputPeer(), // send_as - MTPstring() // quick_reply_shortcut + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { history->session().api().sendMessageFail(error, history->peer); From f85c3c88f72f7835810ed20937495e278f6976cd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 13:29:17 +0400 Subject: [PATCH 048/108] Add rpl interface to RadiobuttonGroup. --- Telegram/SourceFiles/boxes/add_contact_box.cpp | 6 +++--- Telegram/SourceFiles/boxes/auto_lock_box.cpp | 8 +++++--- Telegram/SourceFiles/boxes/connection_box.cpp | 8 ++++---- Telegram/SourceFiles/boxes/download_path_box.cpp | 8 +++++--- Telegram/SourceFiles/boxes/edit_privacy_box.cpp | 2 +- Telegram/SourceFiles/boxes/gift_premium_box.cpp | 4 ++-- .../boxes/peers/edit_participant_box.cpp | 2 +- .../peers/edit_peer_history_visibility_box.cpp | 2 +- .../SourceFiles/boxes/peers/edit_peer_type_box.cpp | 14 +++++++------- Telegram/SourceFiles/boxes/premium_limits_box.cpp | 2 +- Telegram/SourceFiles/boxes/ringtones_box.cpp | 2 +- .../SourceFiles/boxes/self_destruction_box.cpp | 2 +- .../export/view/export_view_settings.cpp | 2 +- .../info/boosts/create_giveaway_box.cpp | 10 +++++----- .../passport/passport_panel_edit_document.cpp | 2 +- .../SourceFiles/settings/settings_business.cpp | 2 +- Telegram/SourceFiles/settings/settings_calls.cpp | 2 +- .../SourceFiles/settings/settings_global_ttl.cpp | 4 ++-- Telegram/SourceFiles/settings/settings_premium.cpp | 2 +- .../settings/settings_privacy_controllers.cpp | 2 +- .../SourceFiles/ui/effects/premium_graphics.cpp | 8 ++++---- .../window/themes/window_themes_cloud_list.cpp | 2 +- 22 files changed, 50 insertions(+), 46 deletions(-) diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 9819c855c..d3715e884 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -1018,7 +1018,7 @@ void SetupChannelBox::prepare() { cancel); connect(_link, &Ui::MaskedInputField::changed, [=] { handleChange(); }); - _link->setVisible(_privacyGroup->value() == Privacy::Public); + _link->setVisible(_privacyGroup->current() == Privacy::Public); _privacyGroup->setChangedCallback([=](Privacy value) { privacyChanged(value); @@ -1063,7 +1063,7 @@ void SetupChannelBox::updateMaxHeight() { : 0) + st::newGroupPadding.bottom(); if (!_channel->isMegagroup() - || _privacyGroup->value() == Privacy::Public) { + || _privacyGroup->current() == Privacy::Public) { newHeight += st::newGroupLinkPadding.top() + _link->height() + st::newGroupLinkPadding.bottom(); @@ -1264,7 +1264,7 @@ void SetupChannelBox::save() { }; if (_saveRequestId) { return; - } else if (_privacyGroup->value() == Privacy::Private) { + } else if (_privacyGroup->current() == Privacy::Private) { closeBox(); } else { const auto link = _link->text().trimmed(); diff --git a/Telegram/SourceFiles/boxes/auto_lock_box.cpp b/Telegram/SourceFiles/boxes/auto_lock_box.cpp index 70c09ec9e..cc7cdb427 100644 --- a/Telegram/SourceFiles/boxes/auto_lock_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_lock_box.cpp @@ -81,9 +81,9 @@ void AutoLockBox::prepare() { const auto timeInput = Ui::CreateChild<Ui::TimeInput>( this, - (group->value() == kCustom) + (group->current() == kCustom ? TimeString(currentTime) - : kDefaultCustom.utf8(), + : kDefaultCustom.utf8()), st::autolockTimeField, st::autolockDateField, st::scheduleTimeSeparator, @@ -115,7 +115,9 @@ void AutoLockBox::prepare() { }); rpl::merge( - boxClosing() | rpl::filter([=] { return group->value() == kCustom; }), + boxClosing() | rpl::filter( + [=] { return group->current() == kCustom; } + ), timeInput->submitRequests() ) | rpl::start_with_next([=] { if (const auto result = collect()) { diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 4403f56fa..cf9d91f8c 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -717,7 +717,7 @@ void ProxiesBox::refreshProxyForCalls() { return; } _proxyForCalls->toggle( - (_proxySettings->value() == ProxyData::Settings::Enabled + (_proxySettings->current() == ProxyData::Settings::Enabled && _currentProxySupportsCallsId != 0), anim::type::normal); } @@ -864,7 +864,7 @@ void ProxyBox::refreshButtons() { addButton(tr::lng_settings_save(), [=] { save(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); - const auto type = _type->value(); + const auto type = _type->current(); if (type == Type::Socks5 || type == Type::Mtproto) { addLeftButton(tr::lng_proxy_share(), [=] { share(); }); } @@ -885,7 +885,7 @@ void ProxyBox::share() { ProxyData ProxyBox::collectData() { auto result = ProxyData(); - result.type = _type->value(); + result.type = _type->current(); result.host = _host->getLastText().trimmed(); result.port = _port->getLastText().trimmed().toInt(); result.user = (result.type == Type::Mtproto) @@ -1053,7 +1053,7 @@ void ProxyBox::setupControls(const ProxyData &data) { handleType(type); refreshButtons(); }); - handleType(_type->value()); + handleType(_type->current()); } void ProxyBox::addLabel( diff --git a/Telegram/SourceFiles/boxes/download_path_box.cpp b/Telegram/SourceFiles/boxes/download_path_box.cpp index 69179e9f4..f61c0e6e7 100644 --- a/Telegram/SourceFiles/boxes/download_path_box.cpp +++ b/Telegram/SourceFiles/boxes/download_path_box.cpp @@ -44,7 +44,9 @@ void DownloadPathBox::prepare() { setTitle(tr::lng_download_path_header()); - _group->setChangedCallback([this](Directory value) { radioChanged(value); }); + _group->setChangedCallback([this](Directory value) { + radioChanged(value); + }); _pathLink->addClickHandler([=] { editPath(); }); if (!_path.isEmpty() && _path != FileDialog::Tmp()) { @@ -54,7 +56,7 @@ void DownloadPathBox::prepare() { } void DownloadPathBox::updateControlsVisibility() { - auto custom = (_group->value() == Directory::Custom); + auto custom = (_group->current() == Directory::Custom); _pathLink->setVisible(custom); auto newHeight = st::boxOptionListPadding.top() + (_default ? _default->getMargins().top() + _default->heightNoMargins() : 0) + st::boxOptionListSkip + _temp->heightNoMargins() + st::boxOptionListSkip + _dir->heightNoMargins(); @@ -122,7 +124,7 @@ void DownloadPathBox::editPath() { void DownloadPathBox::save() { #ifndef OS_WIN_STORE - auto value = _group->value(); + auto value = _group->current(); auto computePath = [this, value] { if (value == Directory::Custom) { return _path; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 7ad5798ec..367742971 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -582,7 +582,7 @@ void EditMessagesPrivacyBox( box->addButton(tr::lng_settings_save(), [=] { if (controller->session().premium()) { privacy->updateNewRequirePremium( - group->value() == kOptionPremium); + group->current() == kOptionPremium); box->closeBox(); } else { showToast(); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 7e0e6b5dc..f544b5647 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -386,7 +386,7 @@ void GiftBox( state->buttonText.events(), Ui::Premium::GiftGradientStops(), [=] { - const auto value = group->value(); + const auto value = group->current(); return (value < options.size() && value >= 0) ? options[value].botUrl : QString(); @@ -665,7 +665,7 @@ void GiftsBox( } auto invoice = api->invoice( users.size(), - api->monthsFromPreset(group->value())); + api->monthsFromPreset(group->current())); invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users }; state->confirmButtonBusy = true; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 51d969d0e..ff9398bf0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -840,7 +840,7 @@ void EditRestrictedBox::createUntilGroup() { void EditRestrictedBox::createUntilVariants() { auto addVariant = [&](int value, const QString &text) { - if (!canSave() && _untilGroup->value() != value) { + if (!canSave() && _untilGroup->current() != value) { return; } _untilVariants.emplace_back( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp index 697066416..9086dfce8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp @@ -25,7 +25,7 @@ void EditPeerHistoryVisibilityBox( box->setTitle(tr::lng_manage_history_visibility_title()); box->addButton(tr::lng_settings_save(), [=] { - savedCallback(historyVisibility->value()); + savedCallback(historyVisibility->current()); box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 46fb2619c..6cafe4d2c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -76,7 +76,7 @@ public: } [[nodiscard]] Privacy getPrivacy() const { - return _controls.privacy->value(); + return _controls.privacy->current(); } [[nodiscard]] bool noForwards() const { @@ -238,7 +238,7 @@ void Controller::createContent() { }, wrap->lifetime()); } else { _controls.whoSendWrap->toggle( - (_controls.privacy->value() == Privacy::HasUsername), + (_controls.privacy->current() == Privacy::HasUsername), anim::type::instant); } auto joinToWrite = _controls.joinToWrite @@ -299,7 +299,7 @@ void Controller::createContent() { if (_linkOnly) { _controls.inviteLinkWrap->show(anim::type::instant); } else { - if (_controls.privacy->value() == Privacy::NoUsername) { + if (_controls.privacy->current() == Privacy::NoUsername) { checkUsernameAvailability(); } const auto forShowing = _dataSavedValue @@ -474,7 +474,7 @@ object_ptr<Ui::RpWidget> Controller::createUsernameEdit() { &Ui::UsernameInput::changed, [this] { usernameChanged(); }); - const auto shown = (_controls.privacy->value() == Privacy::HasUsername); + const auto shown = (_controls.privacy->current() == Privacy::HasUsername); result->toggle(shown, anim::type::instant); return result; @@ -539,7 +539,7 @@ void Controller::checkUsernameAvailability() { if (!_controls.usernameInput) { return; } - const auto initial = (_controls.privacy->value() != Privacy::HasUsername); + const auto initial = (_controls.privacy->current() != Privacy::HasUsername); const auto checking = initial ? u".bad."_q : getUsernameInput(); @@ -573,11 +573,11 @@ void Controller::checkUsernameAvailability() { _controls.privacy->setValue(Privacy::NoUsername); } else if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) { _usernameState = UsernameState::TooMany; - if (_controls.privacy->value() == Privacy::HasUsername) { + if (_controls.privacy->current() == Privacy::HasUsername) { askUsernameRevoke(); } } else if (initial) { - if (_controls.privacy->value() == Privacy::HasUsername) { + if (_controls.privacy->current() == Privacy::HasUsername) { showUsernameEmpty(); setFocusUsername(); } diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index ad8d313df..2a9c0d4b1 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -1142,7 +1142,7 @@ void AccountsLimitBox( const auto ref = QString(); const auto wasAccount = &session->account(); - const auto nowAccount = accounts[group->value()]; + const auto nowAccount = accounts[group->current()]; if (wasAccount == nowAccount) { Settings::ShowPremium(session, ref); return; diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp index 74e8f510b..d8097222e 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.cpp +++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp @@ -327,7 +327,7 @@ void RingtonesBox( box->setWidth(st::boxWideWidth); box->addButton(tr::lng_settings_save(), [=] { - const auto value = state->group->value(); + const auto value = state->group->current(); auto sound = (value == kDefaultValue) ? Data::NotifySound() : (value == kNoSoundValue) diff --git a/Telegram/SourceFiles/boxes/self_destruction_box.cpp b/Telegram/SourceFiles/boxes/self_destruction_box.cpp index 03d8a6155..0ab869ae5 100644 --- a/Telegram/SourceFiles/boxes/self_destruction_box.cpp +++ b/Telegram/SourceFiles/boxes/self_destruction_box.cpp @@ -95,7 +95,7 @@ void SelfDestructionBox::showContent() { clearButtons(); addButton(tr::lng_settings_save(), [=] { - const auto value = _ttlGroup->value(); + const auto value = _ttlGroup->current(); switch (_type) { case Type::Account: _session->api().selfDestruct().updateAccountTTL(value); diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index bbc91c2dc..8f546260a 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -78,7 +78,7 @@ void ChooseFormatBox( addFormatOption( tr::lng_export_option_html_and_json(tr::now), Format::HtmlAndJson); - box->addButton(tr::lng_settings_save(), [=] { done(group->value()); }); + box->addButton(tr::lng_settings_save(), [=] { done(group->current()); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp index 6c5583723..01462aa3b 100644 --- a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp @@ -630,9 +630,9 @@ void CreateGiveawayBox( const auto createCallback = [=](GiveawayType type) { return [=] { - const auto was = membersGroup->value(); + const auto was = membersGroup->current(); membersGroup->setValue(type); - const auto now = membersGroup->value(); + const auto now = membersGroup->current(); if (was == now) { base::call_delayed( st::defaultRippleAnimation.hideDuration, @@ -990,7 +990,7 @@ void CreateGiveawayBox( if (state->confirmButtonBusy.current()) { return; } - const auto type = typeGroup->value(); + const auto type = typeGroup->current(); const auto isSpecific = (type == GiveawayType::SpecificUsers); const auto isRandom = (type == GiveawayType::Random); if (!isSpecific && !isRandom) { @@ -1003,7 +1003,7 @@ void CreateGiveawayBox( prepaid ? prepaid->months : state->apiOptions.monthsFromPreset( - durationGroup->value())); + durationGroup->current())); if (isSpecific) { if (state->selectedToAward.empty()) { return; @@ -1029,7 +1029,7 @@ void CreateGiveawayBox( .countries = state->countriesValue.current(), .additionalPrize = state->additionalPrize.current(), .untilDate = state->dateValue.current(), - .onlyNewSubscribers = (membersGroup->value() + .onlyNewSubscribers = (membersGroup->current() == GiveawayType::OnlyNewMembers), .showWinners = state->showWinners.current(), }; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index c3715e977..bac92c2c8 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -140,7 +140,7 @@ void RequestTypeBox::setupControls( _height = y; _submit = [=] { - const auto value = group->hasValue() ? group->value() : -1; + const auto value = group->hasValue() ? group->current() : -1; if (value >= 0) { submit(value); } diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index f93646606..aae5868df 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -503,7 +503,7 @@ QPointer<Ui::RpWidget> Business::createPinnedToBottom( std::move(buttonText), std::nullopt, [=, options = session->api().premium().subscriptionOptions()] { - const auto value = _radioGroup->value(); + const auto value = _radioGroup->current(); return (value < options.size() && value >= 0) ? options[value].botUrl : QString(); diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 0b6a70674..de7c34e3e 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -607,7 +607,7 @@ void ChooseMediaDeviceBox( button->finishAnimating(); button->clicks( ) | rpl::filter([=] { - return (group->value() == index); + return (group->current() == index); }) | rpl::start_with_next([=] { choose(id); }, button->lifetime()); diff --git a/Telegram/SourceFiles/settings/settings_global_ttl.cpp b/Telegram/SourceFiles/settings/settings_global_ttl.cpp index 8e2024622..99dea5b2d 100644 --- a/Telegram/SourceFiles/settings/settings_global_ttl.cpp +++ b/Telegram/SourceFiles/settings/settings_global_ttl.cpp @@ -294,7 +294,7 @@ void GlobalTTL::rebuildButtons(TimeId currentTTL) const { rpl::single(ttlText)), st::settingsButtonNoIcon)); button->setClickedCallback([=] { - if (_group->value() == ttl) { + if (_group->current() == ttl) { return; } if (!ttl) { @@ -357,7 +357,7 @@ void GlobalTTL::setupContent() { show->showBox(Box(TTLMenu::TTLBox, TTLMenu::Args{ .show = show, - .startTtl = _group->value(), + .startTtl = _group->current(), .callback = [=](TimeId ttl, Fn<void()>) { showSure(ttl, true); }, .hideDisable = true, })); diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index f9d627d3b..e56122c94 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1190,7 +1190,7 @@ QPointer<Ui::RpWidget> Premium::createPinnedToBottom( std::move(buttonText), std::nullopt, [=, options = session->api().premium().subscriptionOptions()] { - const auto value = _radioGroup->value(); + const auto value = _radioGroup->current(); return (value < options.size() && value >= 0) ? options[value].botUrl : QString(); diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 5828b2e0e..5f7cb56b1 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -598,7 +598,7 @@ object_ptr<Ui::RpWidget> PhoneNumberPrivacyController::setupMiddleWidget( _saveAdditional = [=] { controller->session().api().userPrivacy().save( Api::UserPrivacy::Key::AddedByPhone, - Api::UserPrivacy::Rule{ .option = group->value() }); + Api::UserPrivacy::Rule{ .option = group->current() }); }; return widget; diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index e46760ee1..3d3cb00ef 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -1063,7 +1063,7 @@ void AddAccountsRow( }); const auto index = int(state->accounts.size()) - 1; state->accounts[index].checkbox.setChecked( - index == group->value(), + index == group->current(), anim::type::instant); widget->paintRequest( @@ -1303,7 +1303,7 @@ void AddGiftOptions( int nowIndex = 0; Ui::Animations::Simple animation; }; - const auto wasGroupValue = group->value(); + const auto wasGroupValue = group->current(); const auto animation = parent->lifetime().make_state<Animation>(); animation->nowIndex = wasGroupValue; @@ -1324,7 +1324,7 @@ void AddGiftOptions( const auto &stCheckbox = st::defaultBoxCheckbox; auto radioView = std::make_unique<GradientRadioView>( st::defaultRadio, - (group->hasValue() && group->value() == index)); + (group->hasValue() && group->current() == index)); const auto radioViewRaw = radioView.get(); const auto radio = Ui::CreateChild<Ui::Radiobutton>( row, @@ -1468,7 +1468,7 @@ void AddGiftOptions( row->setClickedCallback([=, duration = st::defaultCheck.duration] { group->setValue(index); - animation->nowIndex = group->value(); + animation->nowIndex = group->current(); animation->animation.stop(); animation->animation.start( [=] { parent->update(); }, diff --git a/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp b/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp index 1e71863af..4001edae4 100644 --- a/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp +++ b/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp @@ -498,7 +498,7 @@ bool CloudList::insertTillLimit( void CloudList::insert(int index, const Data::CloudTheme &theme) { const auto id = theme.id; const auto value = groupValueForId(id); - const auto checked = _group->hasValue() && (_group->value() == value); + const auto checked = _group->hasValue() && (_group->current() == value); auto check = std::make_unique<CloudListCheck>(checked); const auto raw = check.get(); auto button = std::make_unique<Ui::Radiobutton>( From e6b9ac22675df39bf2e0139986ca5c9b6f5d23c3 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 13:29:29 +0400 Subject: [PATCH 049/108] Support edit / save of away message settings. --- .../data/business/data_business_common.h | 30 +++ .../data/business/data_business_info.cpp | 85 +++++++- .../data/business/data_business_info.h | 11 +- Telegram/SourceFiles/data/data_user.cpp | 65 +++++- .../business/settings_away_message.cpp | 198 +++++++++++++++++- Telegram/SourceFiles/settings/settings.style | 2 + 6 files changed, 378 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index c480a8579..a74b5df48 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -172,4 +172,34 @@ struct BusinessDetails { const BusinessDetails &b) = default; }; +enum class AwayScheduleType : uchar { + Never = 0, + Always = 1, + OutsideWorkingHours = 2, + Custom = 3, +}; + +struct AwaySchedule { + AwayScheduleType type = AwayScheduleType::Always; + WorkingInterval customInterval; + + friend inline bool operator==( + const AwaySchedule &a, + const AwaySchedule &b) = default; +}; + +struct AwaySettings { + BusinessRecipients recipients; + AwaySchedule schedule; + int shortcutId = 0; + + explicit operator bool() const { + return schedule.type != AwayScheduleType::Never; + } + + friend inline bool operator==( + const AwaySettings &a, + const AwaySettings &b) = default; +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 804b23deb..d054d3a56 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -29,6 +29,51 @@ namespace { MTP_vector_from_range(list | ranges::views::transform(proj))); } +template <typename Flag> +[[nodiscard]] auto RecipientsFlags( + const BusinessRecipients &data, + Flag) { + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + return Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) + | (chats.list.empty() ? Flag() : Flag::f_users) + | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); +} + +[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP( + const AwaySchedule &data) { + Expects(data.type != AwayScheduleType::Never); + + return (data.type == AwayScheduleType::Always) + ? MTP_businessAwayMessageScheduleAlways() + : (data.type == AwayScheduleType::OutsideWorkingHours) + ? MTP_businessAwayMessageScheduleOutsideWorkHours() + : MTP_businessAwayMessageScheduleCustom( + MTP_int(data.customInterval.start), + MTP_int(data.customInterval.end)); +} + +[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) { + using Flag = MTPDinputBusinessAwayMessage::Flag; + return MTP_inputBusinessAwayMessage( + MTP_flags(RecipientsFlags(data.recipients, Flag())), + MTP_int(data.shortcutId), + ToMTP(data.schedule), + MTP_vector_from_range( + (data.recipients.allButExcluded + ? data.recipients.excluded + : data.recipients.included).list + | ranges::views::transform(&UserData::inputUser))); +} + } // namespace BusinessInfo::BusinessInfo(not_null<Session*> owner) @@ -42,17 +87,51 @@ void BusinessInfo::saveWorkingHours(WorkingHours data) { if (details.hours == data) { return; } - details.hours = std::move(data); using Flag = MTPaccount_UpdateBusinessWorkHours::Flag; _owner->session().api().request(MTPaccount_UpdateBusinessWorkHours( - MTP_flags(details.hours ? Flag::f_business_work_hours : Flag()), - ToMTP(details.hours) + MTP_flags(data ? Flag::f_business_work_hours : Flag()), + ToMTP(data) )).send(); + details.hours = std::move(data); _owner->session().user()->setBusinessDetails(std::move(details)); } +void BusinessInfo::applyAwaySettings(AwaySettings data) { + if (_awaySettings == data) { + return; + } + _awaySettings = data; + _awaySettingsChanged.fire({}); +} + +void BusinessInfo::saveAwaySettings(AwaySettings data) { + if (_awaySettings == data) { + return; + } + using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; + _owner->session().api().request(MTPaccount_UpdateBusinessAwayMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessAwayMessage() + )).send(); + + _awaySettings = std::move(data); + _awaySettingsChanged.fire({}); +} + +bool BusinessInfo::awaySettingsLoaded() const { + return _awaySettings.has_value(); +} + +AwaySettings BusinessInfo::awaySettings() const { + return _awaySettings.value_or(AwaySettings()); +} + +rpl::producer<> BusinessInfo::awaySettingsChanged() const { + return _awaySettingsChanged.events(); +} + void BusinessInfo::preload() { preloadTimezones(); } diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index ee4a2e043..9cd7f9681 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -18,9 +18,16 @@ public: explicit BusinessInfo(not_null<Session*> owner); ~BusinessInfo(); + void preload(); + void saveWorkingHours(WorkingHours data); - void preload(); + void saveAwaySettings(AwaySettings data); + void applyAwaySettings(AwaySettings data); + [[nodiscard]] AwaySettings awaySettings() const; + [[nodiscard]] bool awaySettingsLoaded() const; + [[nodiscard]] rpl::producer<> awaySettingsChanged() const; + void preloadTimezones(); [[nodiscard]] rpl::producer<Timezones> timezonesValue() const; @@ -28,6 +35,8 @@ private: const not_null<Session*> _owner; rpl::variable<Timezones> _timezones; + std::optional<AwaySettings> _awaySettings; + rpl::event_stream<> _awaySettingsChanged; mtpRequestId _timezonesRequestId = 0; int32 _timezonesHash = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 45f4f9e28..93346ec90 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_user_photos.h" #include "main/main_session.h" #include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -51,14 +52,66 @@ using UpdateFlag = Data::PeerUpdate::Flag; if (location) { const auto &data = location->data(); result.location.address = qs(data.vaddress()); - data.vgeo_point().match([&](const MTPDgeoPoint &data) { - result.location.point = Data::LocationPoint(data); - }, [&](const MTPDgeoPointEmpty &) { - }); + if (const auto point = data.vgeo_point()) { + point->match([&](const MTPDgeoPoint &data) { + result.location.point = Data::LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } } return result; } +template <typename T> +Data::BusinessRecipients RecipientsFromMTP( + not_null<Data::Session*> owner, + const T &data) { + using Type = Data::BusinessChatType; + auto result = Data::BusinessRecipients{ + .allButExcluded = data.is_exclude_selected(), + }; + auto &chats = result.allButExcluded + ? result.excluded + : result.included; + chats.types = Type() + | (data.is_new_chats() ? Type::NewChats : Type()) + | (data.is_existing_chats() ? Type::ExistingChats : Type()) + | (data.is_contacts() ? Type::Contacts : Type()) + | (data.is_non_contacts() ? Type::NonContacts : Type()); + if (const auto users = data.vusers()) { + for (const auto &userId : users->v) { + chats.list.push_back(owner->user(UserId(userId.v))); + } + } + return result; +} + +[[nodiscard]] Data::AwaySettings FromMTP( + not_null<Data::Session*> owner, + const tl::conditional<MTPBusinessAwayMessage> &message) { + if (!message) { + return Data::AwaySettings(); + } + const auto &data = message->data(); + auto result = Data::AwaySettings{ + .recipients = RecipientsFromMTP(owner, data), + .shortcutId = data.vshortcut_id().v, + }; + data.vschedule().match([&]( + const MTPDbusinessAwayMessageScheduleAlways &) { + result.schedule.type = Data::AwayScheduleType::Always; + }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { + result.schedule.type = Data::AwayScheduleType::OutsideWorkingHours; + }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { + result.schedule.type = Data::AwayScheduleType::Custom; + result.schedule.customInterval = Data::WorkingInterval{ + data.vstart_date().v, + data.vend_date().v, + }; + }); + return result; +} + } // namespace BotInfo::BotInfo() = default; @@ -622,6 +675,10 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) { user->setBusinessDetails(FromMTP( update.vbusiness_work_hours(), update.vbusiness_location())); + if (user->isSelf()) { + user->owner().businessInfo().applyAwaySettings( + FromMTP(&user->owner(), update.vbusiness_away_message())); + } user->owner().stories().apply(user, update.vstories()); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 0e22a4c2f..b38e4e184 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -7,17 +7,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_away_message.h" +#include "base/unixtime.h" #include "core/application.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/boxes/choose_date_time.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { @@ -37,9 +42,144 @@ private: void save(); rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<Data::AwaySchedule> _schedule; + rpl::variable<bool> _enabled; }; +[[nodiscard]] TimeId StartTimeMin() { + // Telegram was launched in August 2013 :) + return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1))); +} + +[[nodiscard]] TimeId EndTimeMin() { + return StartTimeMin() + 3600; +} + +[[nodiscard]] bool BadCustomInterval(const Data::WorkingInterval &interval) { + return !interval + || (interval.start < StartTimeMin()) + || (interval.end < EndTimeMin()); +} + +struct AwayScheduleSelectorDescriptor { + not_null<Window::SessionController*> controller; + not_null<rpl::variable<Data::AwaySchedule>*> data; +}; +void AddAwayScheduleSelector( + not_null<Ui::VerticalLayout*> container, + AwayScheduleSelectorDescriptor &&descriptor) { + using Type = Data::AwayScheduleType; + using namespace rpl::mappers; + + const auto controller = descriptor.controller; + const auto data = descriptor.data; + + Ui::AddSubsectionTitle(container, tr::lng_away_schedule()); + const auto group = std::make_shared<Ui::RadioenumGroup<Type>>( + data->current().type); + + const auto add = [&](Type type, const QString &label) { + container->add( + object_ptr<Ui::Radioenum<Type>>( + container, + group, + type, + label), + st::boxRowPadding + st::settingsAwaySchedulePadding); + }; + add(Type::Always, tr::lng_away_schedule_always(tr::now)); + add(Type::OutsideWorkingHours, tr::lng_away_schedule_outside(tr::now)); + add(Type::Custom, tr::lng_away_schedule_custom(tr::now)); + + const auto customWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto customInner = customWrap->entity(); + customWrap->toggleOn(group->value() | rpl::map(_1 == Type::Custom)); + + group->changes() | rpl::start_with_next([=](Type value) { + auto copy = data->current(); + copy.type = value; + *data = copy; + }, customWrap->lifetime()); + + const auto chooseDate = [=]( + rpl::producer<QString> title, + TimeId now, + Fn<TimeId()> min, + Fn<TimeId()> max, + Fn<void(TimeId)> done) { + using namespace Ui; + const auto box = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto save = [=](TimeId time) { + done(time); + if (const auto strong = box->data()) { + strong->closeBox(); + } + }; + *box = controller->show(Box(ChooseDateTimeBox, ChooseDateTimeBoxArgs{ + .title = std::move(title), + .submit = tr::lng_settings_save(), + .done = save, + .min = min, + .time = now, + .max = max, + })); + }; + + Ui::AddSkip(customInner); + Ui::AddDivider(customInner); + Ui::AddSkip(customInner); + + auto startLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.start)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_start(), + std::move(startLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_start(), + data->current().customInterval.start, + StartTimeMin, + [=] { return data->current().customInterval.end - 1; }, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.start = time; + *data = copy; + }); + }); + + auto endLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.end)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_end(), + std::move(endLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_end(), + data->current().customInterval.end, + [=] { return data->current().customInterval.start + 1; }, + nullptr, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.end = time; + *data = copy; + }); + }); +} + AwayMessage::AwayMessage( QWidget *parent, not_null<Window::SessionController*> controller) @@ -59,12 +199,27 @@ rpl::producer<QString> AwayMessage::title() { void AwayMessage::setupContent( not_null<Window::SessionController*> controller) { + using namespace Data; using namespace rpl::mappers; const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - //const auto current = controller->session().data().chatbots().current(); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->awaySettings(); + const auto disabled = (current.schedule.type == AwayScheduleType::Never); + + _recipients = current.recipients; + auto initialSchedule = disabled ? AwaySchedule{ + .type = AwayScheduleType::Always, + } : current.schedule; + if (BadCustomInterval(initialSchedule.customInterval)) { + const auto now = base::unixtime::now(); + initialSchedule.customInterval = WorkingInterval{ + .start = now, + .end = now + 24 * 60 * 60, + }; + } + _schedule = initialSchedule; - //_recipients = current.recipients; AddDividerTextWithLottie(content, { .lottie = u"sleep"_q, .lottieSize = st::settingsCloudPasswordIconSize, @@ -79,7 +234,8 @@ void AwayMessage::setupContent( content, tr::lng_away_enable(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(false)); + ))->toggleOn(rpl::single(!disabled)); + _enabled = enabled->toggledValue(); const auto wrap = content->add( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( @@ -90,8 +246,32 @@ void AwayMessage::setupContent( Ui::AddSkip(inner); Ui::AddDivider(inner); - wrap->toggleOn(enabled->toggledValue()); - wrap->finishAnimating(); + const auto createWrap = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = createInner->add(object_ptr<Ui::SettingsButton>( + createInner, + tr::lng_away_create(), + st::settingsButtonLightNoIcon + )); + create->setClickedCallback([=] { + + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); + AddAwayScheduleSelector(inner, { + .controller = controller, + .data = &_schedule, + }); + Ui::AddSkip(inner); + Ui::AddDivider(inner); AddBusinessRecipientsSelector(inner, { .controller = controller, @@ -101,10 +281,18 @@ void AwayMessage::setupContent( Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + Ui::ResizeFitChild(this, content); } void AwayMessage::save() { + controller()->session().data().businessInfo().saveAwaySettings( + _enabled.current() ? Data::AwaySettings{ + .recipients = _recipients.current(), + .schedule = _schedule.current(), + } : Data::AwaySettings()); } } // namespace diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 3e57d64cc..41fb908e0 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -614,3 +614,5 @@ settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) { settingsWorkingHoursDetails: settingsNotificationTypeDetails; settingsWorkingHoursPicker: 200px; settingsWorkingHoursPickerItemHeight: 40px; + +settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px); From dd7ccada2f2ff97582271a6c4668f5324c2394e5 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 13:56:18 +0400 Subject: [PATCH 050/108] Support edit / save of greeting message settings. --- .../data/business/data_business_common.h | 14 ++ .../data/business/data_business_info.cpp | 48 +++++ .../data/business/data_business_info.h | 10 + Telegram/SourceFiles/data/data_user.cpp | 16 ++ .../settings/business/settings_greeting.cpp | 189 +++++++++++++++++- 5 files changed, 266 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index a74b5df48..914d62503 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -202,4 +202,18 @@ struct AwaySettings { const AwaySettings &b) = default; }; +struct GreetingSettings { + BusinessRecipients recipients; + int noActivityDays = 0; + int shortcutId = 0; + + explicit operator bool() const { + return noActivityDays > 0; + } + + friend inline bool operator==( + const GreetingSettings &a, + const GreetingSettings &b) = default; +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index d054d3a56..1a1d30555 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -74,6 +74,20 @@ template <typename Flag> | ranges::views::transform(&UserData::inputUser))); } +[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP( + const GreetingSettings &data) { + using Flag = MTPDinputBusinessGreetingMessage::Flag; + return MTP_inputBusinessGreetingMessage( + MTP_flags(RecipientsFlags(data.recipients, Flag())), + MTP_int(data.shortcutId), + MTP_vector_from_range( + (data.recipients.allButExcluded + ? data.recipients.excluded + : data.recipients.included).list + | ranges::views::transform(&UserData::inputUser)), + MTP_int(data.noActivityDays)); +} + } // namespace BusinessInfo::BusinessInfo(not_null<Session*> owner) @@ -132,6 +146,40 @@ rpl::producer<> BusinessInfo::awaySettingsChanged() const { return _awaySettingsChanged.events(); } +void BusinessInfo::applyGreetingSettings(GreetingSettings data) { + if (_greetingSettings == data) { + return; + } + _greetingSettings = data; + _greetingSettingsChanged.fire({}); +} + +void BusinessInfo::saveGreetingSettings(GreetingSettings data) { + if (_greetingSettings == data) { + return; + } + using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag; + _owner->session().api().request(MTPaccount_UpdateBusinessGreetingMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessGreetingMessage() + )).send(); + + _greetingSettings = std::move(data); + _greetingSettingsChanged.fire({}); +} + +bool BusinessInfo::greetingSettingsLoaded() const { + return _greetingSettings.has_value(); +} + +GreetingSettings BusinessInfo::greetingSettings() const { + return _greetingSettings.value_or(GreetingSettings()); +} + +rpl::producer<> BusinessInfo::greetingSettingsChanged() const { + return _greetingSettingsChanged.events(); +} + void BusinessInfo::preload() { preloadTimezones(); } diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index 9cd7f9681..e572d2757 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -28,6 +28,12 @@ public: [[nodiscard]] bool awaySettingsLoaded() const; [[nodiscard]] rpl::producer<> awaySettingsChanged() const; + void saveGreetingSettings(GreetingSettings data); + void applyGreetingSettings(GreetingSettings data); + [[nodiscard]] GreetingSettings greetingSettings() const; + [[nodiscard]] bool greetingSettingsLoaded() const; + [[nodiscard]] rpl::producer<> greetingSettingsChanged() const; + void preloadTimezones(); [[nodiscard]] rpl::producer<Timezones> timezonesValue() const; @@ -35,9 +41,13 @@ private: const not_null<Session*> _owner; rpl::variable<Timezones> _timezones; + std::optional<AwaySettings> _awaySettings; rpl::event_stream<> _awaySettingsChanged; + std::optional<GreetingSettings> _greetingSettings; + rpl::event_stream<> _greetingSettingsChanged; + mtpRequestId _timezonesRequestId = 0; int32 _timezonesHash = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 93346ec90..35ad558c7 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -112,6 +112,20 @@ Data::BusinessRecipients RecipientsFromMTP( return result; } +[[nodiscard]] Data::GreetingSettings FromMTP( + not_null<Data::Session*> owner, + const tl::conditional<MTPBusinessGreetingMessage> &message) { + if (!message) { + return Data::GreetingSettings(); + } + const auto &data = message->data(); + return Data::GreetingSettings{ + .recipients = RecipientsFromMTP(owner, data), + .noActivityDays = data.vno_activity_days().v, + .shortcutId = data.vshortcut_id().v, + }; +} + } // namespace BotInfo::BotInfo() = default; @@ -678,6 +692,8 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) { if (user->isSelf()) { user->owner().businessInfo().applyAwaySettings( FromMTP(&user->owner(), update.vbusiness_away_message())); + user->owner().businessInfo().applyGreetingSettings( + FromMTP(&user->owner(), update.vbusiness_greeting_message())); } user->owner().stories().apply(user, update.vstories()); diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 599b25b2c..39520846f 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -7,22 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_greeting.h" +#include "base/event_filter.h" #include "core/application.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/box_content_divider.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/vertical_drum_picker.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { namespace { +constexpr auto kDefaultNoActivityDays = 7; + class Greeting : public BusinessSection<Greeting> { public: Greeting( @@ -32,21 +40,129 @@ public: [[nodiscard]] rpl::producer<QString> title() override; + const Ui::RoundRect *bottomSkipRounding() const { + return &_bottomSkipRounding; + } + private: void setupContent(not_null<Window::SessionController*> controller); void save(); + Ui::RoundRect _bottomSkipRounding; + rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<int> _noActivityDays; + rpl::variable<bool> _enabled; }; Greeting::Greeting( QWidget *parent, not_null<Window::SessionController*> controller) -: BusinessSection(parent, controller) { +: BusinessSection(parent, controller) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } +void EditPeriodBox( + not_null<Ui::GenericBox*> box, + int days, + Fn<void(int)> save) { + auto values = base::flat_set<int>{ 7, 14, 21, 28 }; + if (!values.contains(days)) { + values.emplace(days); + } + const auto startIndex = int(values.find(days) - begin(values)); + + const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>( + box, + st::settingsWorkingHoursPicker)); + + const auto font = st::boxTextFont; + const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; + auto paintCallback = [=]( + QPainter &p, + int index, + float64 y, + float64 distanceFromCenter, + int outerWidth) { + const auto r = QRectF(0, y, outerWidth, itemHeight); + const auto progress = std::abs(distanceFromCenter); + const auto revProgress = 1. - progress; + p.save(); + p.translate(r.center()); + constexpr auto kMinYScale = 0.2; + const auto yScale = kMinYScale + + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); + p.scale(1., yScale); + p.translate(-r.center()); + p.setOpacity(revProgress); + p.setFont(font); + p.setPen(st::defaultFlatLabel.textFg); + p.drawText( + r, + tr::lng_days(tr::now, lt_count, *(values.begin() + index)), + style::al_center); + p.restore(); + }; + + const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>( + content, + std::move(paintCallback), + int(values.size()), + itemHeight, + startIndex); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + picker->resize(s.width(), s.height()); + picker->moveToLeft((s.width() - picker->width()) / 2, 0); + }, content->lifetime()); + + content->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(content); + + p.fillRect(r, Qt::transparent); + + const auto lineRect = QRect( + 0, + content->height() / 2, + content->width(), + st::defaultInputField.borderActive); + p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); + p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); + }, content->lifetime()); + + base::install_event_filter(content, [=](not_null<QEvent*> e) { + if ((e->type() == QEvent::MouseButtonPress) + || (e->type() == QEvent::MouseButtonRelease) + || (e->type() == QEvent::MouseMove)) { + picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get())); + } else if (e->type() == QEvent::Wheel) { + picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get())); + } + return base::EventFilterResult::Continue; + }); + base::install_event_filter(box, [=](not_null<QEvent*> e) { + if (e->type() == QEvent::KeyPress) { + picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); + } + return base::EventFilterResult::Continue; + }); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(*(begin(values) + picker->index())); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + Greeting::~Greeting() { if (!Core::Quitting()) { save(); @@ -62,9 +178,14 @@ void Greeting::setupContent( using namespace rpl::mappers; const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - //const auto current = controller->session().data().chatbots().current(); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->greetingSettings(); + const auto disabled = !current.noActivityDays; - //_recipients = current.recipients; + _recipients = current.recipients; + _noActivityDays = disabled + ? kDefaultNoActivityDays + : current.noActivityDays; AddDividerTextWithLottie(content, { .lottie = u"greeting"_q, @@ -80,7 +201,27 @@ void Greeting::setupContent( content, tr::lng_greeting_enable(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(false)); + ))->toggleOn(rpl::single(!disabled)); + + _enabled = enabled->toggledValue(); + + Ui::AddSkip(content); + + content->add( + object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>( + content, + object_ptr<Ui::BoxContentDivider>( + content, + st::boxDividerHeight, + st::boxDividerBg, + RectPart::Top)) + )->setDuration(0)->toggleOn(enabled->toggledValue() | rpl::map(!_1)); + content->add( + object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>( + content, + object_ptr<Ui::BoxContentDivider>( + content)) + )->setDuration(0)->toggleOn(enabled->toggledValue()); const auto wrap = content->add( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( @@ -88,24 +229,50 @@ void Greeting::setupContent( object_ptr<Ui::VerticalLayout>(content))); const auto inner = wrap->entity(); - Ui::AddSkip(inner); - Ui::AddDivider(inner); - - wrap->toggleOn(enabled->toggledValue()); - wrap->finishAnimating(); - AddBusinessRecipientsSelector(inner, { .controller = controller, .title = tr::lng_greeting_recipients(), .data = &_recipients, }); - Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + AddButtonWithLabel( + inner, + tr::lng_greeting_period_title(), + _noActivityDays.value( + ) | rpl::map( + [](int days) { return tr::lng_days(tr::now, lt_count, days); } + ), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + controller->show(Box( + EditPeriodBox, + _noActivityDays.current(), + [=](int days) { _noActivityDays = days; })); + }); + + Ui::AddSkip(inner); + Ui::AddDividerText( + inner, + tr::lng_greeting_period_about(), + st::settingsChatbotsBottomTextMargin, + RectPart::Top); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); Ui::ResizeFitChild(this, content); } void Greeting::save() { + controller()->session().data().businessInfo().saveGreetingSettings( + _enabled.current() ? Data::GreetingSettings{ + .recipients = _recipients.current(), + .noActivityDays = _noActivityDays.current(), + } : Data::GreetingSettings()); } } // namespace From d05c4e099085c29df9e8d66519e772236ae2b8e3 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 Feb 2024 21:23:15 +0400 Subject: [PATCH 051/108] Start shortcut messages sending. --- Telegram/CMakeLists.txt | 4 + Telegram/SourceFiles/api/api_common.h | 1 + Telegram/SourceFiles/api/api_polls.cpp | 6 +- Telegram/SourceFiles/api/api_sending.cpp | 42 +- Telegram/SourceFiles/apiwrap.cpp | 54 +- Telegram/SourceFiles/boxes/share_box.cpp | 9 +- .../data/business/data_business_common.h | 4 +- .../data/business/data_shortcut_messages.cpp | 539 ++++++++ .../data/business/data_shortcut_messages.h | 125 ++ Telegram/SourceFiles/data/data_msg_id.h | 4 +- .../data/data_scheduled_messages.cpp | 3 +- Telegram/SourceFiles/data/data_session.cpp | 9 +- Telegram/SourceFiles/data/data_session.h | 11 +- Telegram/SourceFiles/data/data_sparse_ids.h | 3 +- Telegram/SourceFiles/data/data_types.cpp | 9 + Telegram/SourceFiles/data/data_types.h | 11 +- Telegram/SourceFiles/history/history_item.h | 7 + .../SourceFiles/info/info_content_widget.cpp | 11 + .../SourceFiles/info/info_content_widget.h | 8 +- .../SourceFiles/info/info_layer_widget.cpp | 7 +- .../SourceFiles/info/info_section_widget.cpp | 6 +- .../SourceFiles/info/info_wrap_widget.cpp | 6 +- Telegram/SourceFiles/info/info_wrap_widget.h | 4 +- .../info/settings/info_settings_widget.cpp | 11 +- .../info/settings/info_settings_widget.h | 2 + Telegram/SourceFiles/main/main_session.cpp | 4 +- Telegram/SourceFiles/main/main_session.h | 2 +- .../media/stories/media_stories_share.cpp | 10 +- .../business/settings_away_message.cpp | 13 +- .../settings/business/settings_chatbots.cpp | 2 +- .../settings/business/settings_greeting.cpp | 25 + .../business/settings_shortcut_messages.cpp | 1183 +++++++++++++++++ .../business/settings_shortcut_messages.h | 16 + .../settings/settings_business.cpp | 5 +- .../SourceFiles/settings/settings_common.h | 7 + .../settings/settings_common_session.h | 10 +- .../settings/settings_notifications_type.cpp | 3 +- .../SourceFiles/settings/settings_premium.cpp | 3 +- 38 files changed, 2109 insertions(+), 70 deletions(-) create mode 100644 Telegram/SourceFiles/data/business/data_shortcut_messages.cpp create mode 100644 Telegram/SourceFiles/data/business/data_shortcut_messages.h create mode 100644 Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_shortcut_messages.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6010bf833..76e06c8e1 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -454,6 +454,8 @@ PRIVATE data/business/data_business_common.h data/business/data_business_info.cpp data/business/data_business_info.h + data/business/data_shortcut_messages.cpp + data/business/data_shortcut_messages.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -1287,6 +1289,8 @@ PRIVATE profile/profile_cover_drop_area.h settings/business/settings_away_message.cpp settings/business/settings_away_message.h + settings/business/settings_shortcut_messages.cpp + settings/business/settings_shortcut_messages.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h settings/business/settings_greeting.cpp diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 155666a5d..fe0d489b0 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -22,6 +22,7 @@ inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); struct SendOptions { PeerData *sendAs = nullptr; TimeId scheduled = 0; + BusinessShortcutId shortcutId = 0; bool silent = false; bool handleSupportSwitch = false; bool hideViaBot = false; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index f465d7f37..3532e2c79 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "base/random.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" #include "data/data_poll.h" @@ -64,6 +65,9 @@ void Polls::create( if (action.options.scheduled) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; @@ -85,7 +89,7 @@ void Polls::create( MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index babce9329..96bdf4a4a 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "base/random.h" #include "base/unixtime.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_document.h" #include "data/data_photo.h" #include "data/data_channel.h" // ChannelData::addsSignature. @@ -128,6 +129,9 @@ void SendExistingMedia( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (message.action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); @@ -146,10 +150,12 @@ void SendExistingMedia( const auto performRequest = [=](const auto &repeatRequest) -> void { auto &histories = history->owner().histories(); + const auto session = &history->session(); + const auto &action = message.action; const auto usedFileReference = media->fileReference(); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage<MTPmessages_SendMedia>( MTP_flags(sendFlags), @@ -160,9 +166,9 @@ void SendExistingMedia( MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -260,7 +266,10 @@ bool SendDice(MessageToSend &message) { message.textWithTags = TextWithTags(); message.action.clearDraft = false; message.action.generateLocal = true; - api->sendAction(message.action); + + + const auto &action = message.action; + api->sendAction(action); const auto newId = FullMsgId( peer->id, @@ -270,17 +279,17 @@ bool SendDice(MessageToSend &message) { auto &histories = history->owner().histories(); auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); - if (message.action.replyTo) { + if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); - const auto silentPost = ShouldSendSilent(peer, message.action.options); - InnerFillMessagePostFlags(message.action.options, peer, flags); + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - const auto sendAs = message.action.options.sendAs; + const auto sendAs = action.options.sendAs; const auto messageFromId = sendAs ? sendAs->id : anonymousPost @@ -293,10 +302,13 @@ bool SendDice(MessageToSend &message) { ? session->user()->name() : QString(); - if (message.action.options.scheduled) { + if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); @@ -305,8 +317,8 @@ bool SendDice(MessageToSend &message) { newId.msg, flags, viaBotId, - message.action.replyTo, - HistoryItem::NewMessageDate(message.action.options.scheduled), + action.replyTo, + HistoryItem::NewMessageDate(action.options.scheduled), messageFromId, messagePostAuthor, TextWithEntities(), @@ -314,7 +326,7 @@ bool SendDice(MessageToSend &message) { HistoryMessageMarkupData()); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage<MTPmessages_SendMedia>( MTP_flags(sendFlags), @@ -325,14 +337,14 @@ bool SendDice(MessageToSend &message) { MTP_long(randomId), MTPReplyMarkup(), MTP_vector<MTPMessageEntity>(), - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); }); - api->finishForwarding(message.action); + api->finishForwarding(action); return true; } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 0d603a044..bca32c3da 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_user_names.h" #include "api/api_websites.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_web_page.h" @@ -3140,7 +3141,9 @@ void ApiWrap::sharedMediaDone( } void ApiWrap::sendAction(const SendAction &action) { - if (!action.options.scheduled && !action.replaceMediaOf) { + if (!action.options.scheduled + && !action.options.shortcutId + && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) @@ -3175,11 +3178,13 @@ void ApiWrap::finishForwarding(const SendAction &action) { } _session->data().sendHistoryChangeNotifications(); - _session->changes().historyUpdated( - history, - (action.options.scheduled - ? Data::HistoryUpdate::Flag::ScheduledSent - : Data::HistoryUpdate::Flag::MessageSent)); + if (!action.options.shortcutId) { + _session->changes().historyUpdated( + history, + (action.options.scheduled + ? Data::HistoryUpdate::Flag::ScheduledSent + : Data::HistoryUpdate::Flag::MessageSent)); + } } void ApiWrap::forwardMessages( @@ -3208,7 +3213,7 @@ void ApiWrap::forwardMessages( const auto history = action.history; const auto peer = history->peer; - if (!action.options.scheduled) { + if (!action.options.scheduled && !action.options.shortcutId) { histories.readInbox(history); } const auto anonymousPost = peer->amAnonymous(); @@ -3226,6 +3231,9 @@ void ApiWrap::forwardMessages( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (draft.options != Data::ForwardOptions::PreserveInfo) { sendFlags |= SendFlag::f_drop_author; } @@ -3265,7 +3273,7 @@ void ApiWrap::forwardMessages( MTP_int(topMsgId), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) )).done([=](const MTPUpdates &result) { applyUpdates(result); if (shared && !--shared->requestsLeft) { @@ -3728,6 +3736,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date; mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; + mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto viaBotId = UserId(); lastMessage = history->addNewLocalMessage( newId.msg, @@ -3763,6 +3775,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { UnixtimeFromMsgId(response.outerMsgId)); } }; + const auto mtpShortcut = Data::ShortcutIdToMTP( + _session, + action.options.shortcutId); if (exactWebPage && !ignoreWebPage && (manualWebPage || sending.empty())) { @@ -3779,9 +3794,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + mtpShortcut ), done, fail); } else { histories.sendPreparedMessage( @@ -3798,7 +3813,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + mtpShortcut ), done, fail); } isFirst = false; @@ -3887,6 +3902,9 @@ void ApiWrap::sendInlineResult( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (action.options.hideViaBot) { sendFlags |= SendFlag::f_hide_via; } @@ -3932,7 +3950,7 @@ void ApiWrap::sendInlineResult( MTP_string(data->getId()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4061,7 +4079,8 @@ void ApiWrap::sendMediaWithRandomId( : Flag(0)) | (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0)) | (options.scheduled ? Flag::f_schedule_date : Flag(0)) - | (options.sendAs ? Flag::f_send_as : Flag(0)); + | (options.sendAs ? Flag::f_send_as : Flag(0)) + | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; @@ -4081,7 +4100,7 @@ void ApiWrap::sendMediaWithRandomId( sentEntities, MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4166,7 +4185,10 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) { ? Flag::f_silent : Flag(0)) | (album->options.scheduled ? Flag::f_schedule_date : Flag(0)) - | (sendAs ? Flag::f_send_as : Flag(0)); + | (sendAs ? Flag::f_send_as : Flag(0)) + | (album->options.shortcutId + ? Flag::f_quick_reply_shortcut + : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; histories.sendPreparedMessage( @@ -4180,7 +4202,7 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) { MTP_vector<MTPInputSingleMedia>(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, album->options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); }, [=](const MTP::Error &error, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 8577aad62..4581fac40 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/share_message_phrase_factory.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_channel.h" #include "data/data_game.h" #include "data/data_histories.h" @@ -1543,11 +1544,15 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( const auto threadHistory = thread->owningHistory(); histories.sendRequest(threadHistory, requestType, [=]( Fn<void()> finish) { - auto &api = threadHistory->session().api(); + const auto session = &threadHistory->session(); + auto &api = session->api(); const auto sendFlags = commonSendFlags | (topMsgId ? Flag::f_top_msg_id : Flag(0)) | (ShouldSendSilent(peer, options) ? Flag::f_silent + : Flag(0)) + | (options.shortcutId + ? Flag::f_quick_reply_shortcut : Flag(0)); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( @@ -1559,7 +1564,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(topMsgId), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, options.shortcutId) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 914d62503..6f3b716dc 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -191,7 +191,7 @@ struct AwaySchedule { struct AwaySettings { BusinessRecipients recipients; AwaySchedule schedule; - int shortcutId = 0; + BusinessShortcutId shortcutId = 0; explicit operator bool() const { return schedule.type != AwayScheduleType::Never; @@ -205,7 +205,7 @@ struct AwaySettings { struct GreetingSettings { BusinessRecipients recipients; int noActivityDays = 0; - int shortcutId = 0; + BusinessShortcutId shortcutId = 0; explicit operator bool() const { return noActivityDays > 0; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp new file mode 100644 index 000000000..1013c05bc --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -0,0 +1,539 @@ +/* +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 "data/business/data_shortcut_messages.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "api/api_text_entities.h" +#include "main/main_session.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/history_item_helpers.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRequestTimeLimit = 60 * crl::time(1000); + +[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) { + Expects(IsServerMsgId(id)); + + return ServerMaxMsgId + id + 1; +} + +[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) { + Expects(IsShortcutMsgId(id)); + + return (id - ServerMaxMsgId - 1); +} + +[[nodiscard]] bool TooEarlyForRequest(crl::time received) { + return (received > 0) && (received + kRequestTimeLimit > crl::now()); +} + +[[nodiscard]] MTPMessage PrepareMessage( + BusinessShortcutId shortcutId, + const MTPMessage &message) { + return message.match([&](const MTPDmessageEmpty &data) { + return MTP_messageEmpty( + data.vflags(), + data.vid(), + data.vpeer_id() ? *data.vpeer_id() : MTPPeer()); + }, [&](const MTPDmessageService &data) { + return MTP_messageService( + data.vflags(), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + data.vpeer_id(), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vaction(), + MTP_int(data.vttl_period().value_or_empty())); + }, [&](const MTPDmessage &data) { + return MTP_message( + MTP_flags(data.vflags().v | MTPDmessage::Flag::f_quick_reply_shortcut_id), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + MTPint(), // from_boosts_applied + data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), + data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), + MTP_long(data.vvia_bot_id().value_or_empty()), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vmessage(), + data.vmedia() ? *data.vmedia() : MTPMessageMedia(), + data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(), + (data.ventities() + ? *data.ventities() + : MTPVector<MTPMessageEntity>()), + MTP_int(data.vviews().value_or_empty()), + MTP_int(data.vforwards().value_or_empty()), + data.vreplies() ? *data.vreplies() : MTPMessageReplies(), + MTP_int(data.vedit_date().value_or_empty()), + MTP_bytes(data.vpost_author().value_or_empty()), + MTP_long(data.vgrouped_id().value_or_empty()), + MTPMessageReactions(), + MTPVector<MTPRestrictionReason>(), + MTP_int(data.vttl_period().value_or_empty()), + MTP_int(shortcutId)); + }); +} + +} // namespace + +bool IsShortcutMsgId(MsgId id) { + return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId); +} + +ShortcutMessages::ShortcutMessages(not_null<Session*> owner) +: _session(&owner->session()) +, _history(owner->history(_session->userPeerId())) +, _clearTimer([=] { clearOldRequests(); }) { + owner->itemRemoved( + ) | rpl::filter([](not_null<const HistoryItem*> item) { + return item->isBusinessShortcut(); + }) | rpl::start_with_next([=](not_null<const HistoryItem*> item) { + remove(item); + }, _lifetime); +} + +ShortcutMessages::~ShortcutMessages() { + for (const auto &request : _requests) { + _session->api().request(request.second.requestId).cancel(); + } +} + +void ShortcutMessages::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; + } + _requests.erase(i); + } +} + +MsgId ShortcutMessages::localMessageId(MsgId remoteId) const { + return RemoteToLocalMsgId(remoteId); +} + +MsgId ShortcutMessages::lookupId(not_null<const HistoryItem*> item) const { + Expects(item->isBusinessShortcut()); + Expects(!item->isSending()); + Expects(!item->hasFailed()); + + return LocalToRemoteMsgId(item->id); +} + +int ShortcutMessages::count(BusinessShortcutId shortcutId) const { + const auto i = _data.find(shortcutId); + return (i != end(_data)) ? i->second.items.size() : 0; +} + +void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { + const auto &message = update.vmessage(); + const auto shortcutId = BusinessShortcutIdFromMessage(message); + if (!shortcutId) { + return; + } + auto &list = _data[shortcutId]; + append(shortcutId, list, message); + sort(list); + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply( + const MTPDupdateDeleteQuickReplyMessages &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + for (const auto &id : update.vmessages().v) { + const auto &list = i->second; + const auto j = list.itemById.find(id.v); + if (j != end(list.itemById)) { + j->second->destroy(); + i = _data.find(shortcutId); + if (i == end(_data)) { + break; + } + } + } + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + while (i != end(_data)) { + Assert(!i->second.itemById.empty()); + i->second.itemById.back().second->destroy(); + i = _data.find(shortcutId); + } + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply( + const MTPDupdateMessageID &update, + not_null<HistoryItem*> local) { + const auto id = update.vid().v; + const auto i = _data.find(local->shortcutId()); + Assert(i != end(_data)); + auto &list = i->second; + const auto j = list.itemById.find(id); + if (j != end(list.itemById) || !IsServerMsgId(id)) { + local->destroy(); + } else { + Assert(!list.itemById.contains(local->id)); + local->setRealId(localMessageId(id)); + list.itemById.emplace(id, local); + } +} + +void ShortcutMessages::appendSending(not_null<HistoryItem*> item) { + Expects(item->isSending()); + Expects(item->isBusinessShortcut()); + + const auto shortcutId = item->shortcutId(); + auto &list = _data[shortcutId]; + list.items.emplace_back(item); + sort(list); + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::removeSending(not_null<HistoryItem*> item) { + Expects(item->isSending() || item->hasFailed()); + Expects(item->isBusinessShortcut()); + + item->destroy(); +} + +rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) { + request(shortcutId); + + return _updates.events( + ) | rpl::filter([=](BusinessShortcutId value) { + return (value == shortcutId); + }) | rpl::to_empty; +} + +Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) { + auto result = Data::MessagesSlice(); + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + const auto i = _requests.find(shortcutId); + if (i == end(_requests)) { + return result; + } + result.fullCount = result.skippedAfter = result.skippedBefore = 0; + return result; + } + const auto &list = i->second.items; + result.skippedAfter = result.skippedBefore = 0; + result.fullCount = int(list.size()); + result.ids = ranges::views::all( + list + ) | ranges::views::transform( + &HistoryItem::fullId + ) | ranges::to_vector; + return result; +} + +void ShortcutMessages::preloadShortcuts() { + if (_shortcutsLoaded || _shortcutsRequestId) { + return; + } + const auto owner = &_session->data(); + _shortcutsRequestId = owner->session().api().request( + MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash)) + ).done([=](const MTPmessages_QuickReplies &result) { + result.match([&](const MTPDmessages_quickReplies &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + owner->processMessages( + data.vmessages(), + NewMessageType::Existing); + auto shortcuts = Shortcuts(); + const auto messages = &owner->shortcutMessages(); + for (const auto &reply : data.vquick_replies().v) { + const auto &data = reply.data(); + const auto id = BusinessShortcutId(data.vshortcut_id().v); + shortcuts.list.emplace(id, Shortcut{ + .name = qs(data.vshortcut()), + .topMessageId = messages->localMessageId( + data.vtop_message().v), + .count = data.vcount().v, + }); + } + for (auto &[id, shortcut] : _shortcuts.list) { + if (id < 0) { + shortcuts.list.emplace(id, shortcut); + } + } + const auto changed = !_shortcutsLoaded + || (shortcuts != _shortcuts); + if (changed) { + _shortcuts = std::move(shortcuts); + _shortcutsLoaded = true; + _shortcutsChanged.fire({}); + } + }, [&](const MTPDmessages_quickRepliesNotModified &) { + if (!_shortcutsLoaded) { + _shortcutsLoaded = true; + _shortcutsChanged.fire({}); + } + }); + }).send(); +} + +const Shortcuts &ShortcutMessages::shortcuts() const { + return _shortcuts; +} + +bool ShortcutMessages::shortcutsLoaded() const { + return _shortcutsLoaded; +} + +rpl::producer<> ShortcutMessages::shortcutsChanged() const { + return _shortcutsChanged.events(); +} + +BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { + Expects(_shortcutsLoaded); + + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcut.name == name) { + return id; + } + } + const auto result = --_localShortcutId; + _shortcuts.list.emplace(result, Shortcut{ name }); + return result; +} + +Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { + const auto i = _shortcuts.list.find(id); + + Ensures(i != end(_shortcuts.list)); + return i->second; +} + +void ShortcutMessages::request(BusinessShortcutId shortcutId) { + auto &request = _requests[shortcutId]; + if (request.requestId || TooEarlyForRequest(request.lastReceived)) { + return; + } + const auto i = _data.find(shortcutId); + const auto hash = (i != end(_data)) + ? countListHash(i->second) + : uint64(0); + request.requestId = _session->api().request( + MTPmessages_GetQuickReplyMessages( + MTP_flags(0), + MTP_int(shortcutId), + MTPVector<MTPint>(), + MTP_long(hash)) + ).done([=](const MTPmessages_Messages &result) { + parse(shortcutId, result); + }).fail([=] { + _requests.remove(shortcutId); + }).send(); +} + +void ShortcutMessages::parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list) { + auto &request = _requests[shortcutId]; + request.lastReceived = crl::now(); + request.requestId = 0; + if (!_clearTimer.isActive()) { + _clearTimer.callOnce(kRequestTimeLimit * 2); + } + + list.match([&](const MTPDmessages_messagesNotModified &data) { + }, [&](const auto &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + + const auto &messages = data.vmessages().v; + if (messages.isEmpty()) { + clearNotSending(shortcutId); + return; + } + auto received = base::flat_set<not_null<HistoryItem*>>(); + auto clear = base::flat_set<not_null<HistoryItem*>>(); + auto &list = _data.emplace(shortcutId, List()).first->second; + for (const auto &message : messages) { + if (const auto item = append(shortcutId, list, message)) { + received.emplace(item); + } + } + for (const auto &owned : list.items) { + const auto item = owned.get(); + if (!item->isSending() && !received.contains(item)) { + clear.emplace(item); + } + } + updated(shortcutId, received, clear); + }); +} + +HistoryItem *ShortcutMessages::append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message) { + const auto id = message.match([&](const auto &data) { + return data.vid().v; + }); + const auto i = list.itemById.find(id); + if (i != end(list.itemById)) { + const auto existing = i->second; + message.match([&](const MTPDmessage &data) { + if (data.is_edit_hide()) { + existing->applyEdition(HistoryMessageEdition(_session, data)); + } else { + existing->updateSentContent({ + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()) + }, data.vmedia()); + existing->updateReplyMarkup( + HistoryMessageMarkupData(data.vreply_markup())); + existing->updateForwardedInfo(data.vfwd_from()); + } + existing->updateDate(data.vdate().v); + _history->owner().requestItemTextRefresh(existing); + }, [&](const auto &data) {}); + return existing; + } + + if (!IsServerMsgId(id)) { + LOG(("API Error: Bad id in quick reply messages: %1.").arg(id)); + return nullptr; + } + const auto item = _session->data().addNewMessage( + localMessageId(id), + PrepareMessage(shortcutId, message), + MessageFlags(), // localFlags + NewMessageType::Existing); + if (!item + || item->history() != _history + || item->shortcutId() != shortcutId) { + LOG(("API Error: Bad data received in quick reply messages.")); + return nullptr; + } + list.items.emplace_back(item); + list.itemById.emplace(id, item); + return item; +} + +void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + auto clear = base::flat_set<not_null<HistoryItem*>>(); + for (const auto &owned : i->second.items) { + if (!owned->isSending() && !owned->hasFailed()) { + clear.emplace(owned.get()); + } + } + updated(shortcutId, {}, clear); +} + +void ShortcutMessages::updated( + BusinessShortcutId shortcutId, + const base::flat_set<not_null<HistoryItem*>> &added, + const base::flat_set<not_null<HistoryItem*>> &clear) { + if (!clear.empty()) { + for (const auto &item : clear) { + item->destroy(); + } + } + const auto i = _data.find(shortcutId); + if (i != end(_data)) { + sort(i->second); + } + if (!added.empty() || !clear.empty()) { + _updates.fire_copy(shortcutId); + } +} + +void ShortcutMessages::sort(List &list) { + ranges::sort(list.items, ranges::less(), &HistoryItem::position); +} + +void ShortcutMessages::remove(not_null<const HistoryItem*> item) { + const auto shortcutId = item->shortcutId(); + const auto i = _data.find(shortcutId); + Assert(i != end(_data)); + auto &list = i->second; + + if (!item->isSending() && !item->hasFailed()) { + list.itemById.remove(lookupId(item)); + } + const auto k = ranges::find(list.items, item, &OwnedItem::get); + Assert(k != list.items.end()); + k->release(); + list.items.erase(k); + + if (list.items.empty()) { + _data.erase(i); + } + _updates.fire_copy(shortcutId); +} + +uint64 ShortcutMessages::countListHash(const List &list) const { + using namespace Api; + + auto hash = HashInit(); + auto &&serverside = ranges::views::all( + list.items + ) | ranges::views::filter([](const OwnedItem &item) { + return !item->isSending() && !item->hasFailed(); + }) | ranges::views::reverse; + for (const auto &item : serverside) { + HashUpdate(hash, lookupId(item.get()).bare); + if (const auto edited = item->Get<HistoryMessageEdited>()) { + HashUpdate(hash, edited->date); + } else { + HashUpdate(hash, TimeId(0)); + } + } + return HashFinalize(hash); +} + +MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null<Main::Session*> session, + BusinessShortcutId id) { + if (id >= 0) { + return MTP_inputQuickReplyShortcutId(MTP_int(id)); + } + return MTP_inputQuickReplyShortcut(MTP_string( + session->data().shortcutMessages().lookupShortcut(id).name)); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h new file mode 100644 index 000000000..2028f2ece --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -0,0 +1,125 @@ +/* +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 "history/history_item.h" +#include "base/timer.h" + +class History; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +struct MessagesSlice; + +struct Shortcut { + QString name; + MsgId topMessageId = 0; + int count = 0; + + friend inline bool operator==( + const Shortcut &a, + const Shortcut &b) = default; +}; + +struct Shortcuts { + base::flat_map<BusinessShortcutId, Shortcut> list; + + friend inline bool operator==( + const Shortcuts &a, + const Shortcuts &b) = default; +}; + +[[nodiscard]] bool IsShortcutMsgId(MsgId id); + +class ShortcutMessages final { +public: + explicit ShortcutMessages(not_null<Session*> owner); + ~ShortcutMessages(); + + [[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const; + [[nodiscard]] int count(BusinessShortcutId shortcutId) const; + [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; + + void apply(const MTPDupdateQuickReplyMessage &update); + void apply(const MTPDupdateDeleteQuickReplyMessages &update); + void apply(const MTPDupdateDeleteQuickReply &update); + void apply( + const MTPDupdateMessageID &update, + not_null<HistoryItem*> local); + + void appendSending(not_null<HistoryItem*> item); + void removeSending(not_null<HistoryItem*> item); + + [[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId); + [[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId); + + void preloadShortcuts(); + [[nodiscard]] const Shortcuts &shortcuts() const; + [[nodiscard]] bool shortcutsLoaded() const; + [[nodiscard]] rpl::producer<> shortcutsChanged() const; + [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); + [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; + +private: + using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>; + struct List { + std::vector<OwnedItem> items; + base::flat_map<MsgId, not_null<HistoryItem*>> itemById; + }; + struct Request { + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; + + void request(BusinessShortcutId shortcutId); + void parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list); + HistoryItem *append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message); + void clearNotSending(BusinessShortcutId shortcutId); + void updated( + BusinessShortcutId shortcutId, + const base::flat_set<not_null<HistoryItem*>> &added, + const base::flat_set<not_null<HistoryItem*>> &clear); + void sort(List &list); + void remove(not_null<const HistoryItem*> item); + [[nodiscard]] uint64 countListHash(const List &list) const; + void clearOldRequests(); + + const not_null<Main::Session*> _session; + const not_null<History*> _history; + + base::Timer _clearTimer; + base::flat_map<BusinessShortcutId, List> _data; + base::flat_map<BusinessShortcutId, Request> _requests; + rpl::event_stream<BusinessShortcutId> _updates; + + Shortcuts _shortcuts; + rpl::event_stream<> _shortcutsChanged; + BusinessShortcutId _localShortcutId = 0; + uint64 _shortcutsHash = 0; + mtpRequestId _shortcutsRequestId = 0; + bool _shortcutsLoaded = false; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null<Main::Session*> session, + BusinessShortcutId id); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index d2790a288..b3ecb27b0 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -54,6 +54,7 @@ Q_DECLARE_METATYPE(MsgId); } using StoryId = int32; +using BusinessShortcutId = int32; struct FullStoryId { PeerId peer = 0; @@ -77,7 +78,8 @@ constexpr auto ServerMaxStoryId = StoryId(1 << 30); constexpr auto StoryMsgIds = int64(ServerMaxStoryId); constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); -constexpr auto ScheduledMsgIdsRange = (1LL << 32); +constexpr auto ScheduledMaxMsgId = MsgId(ServerMaxMsgId + (1LL << 32)); +constexpr auto ShortcutMaxMsgId = MsgId(ScheduledMaxMsgId + (1LL << 32)); constexpr auto ShowAtUnreadMsgId = MsgId(0); constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 41f3cd22f..3fe58f530 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -96,8 +96,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); } // namespace bool IsScheduledMsgId(MsgId id) { - return (id > ServerMaxMsgId) - && (id < ServerMaxMsgId + ScheduledMsgIdsRange); + return (id > ServerMaxMsgId) && (id < ScheduledMaxMsgId); } ScheduledMessages::ScheduledMessages(not_null<Session*> owner) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 37fb68e86..24ea2cd46 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name #include "data/business/data_business_chatbots.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -256,14 +257,12 @@ Session::Session(not_null<Main::Session*> session) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) , _chatsFilters(std::make_unique<ChatFilters>(this)) -, _scheduledMessages(std::make_unique<ScheduledMessages>(this)) , _cloudThemes(std::make_unique<CloudThemes>(session)) , _sendActionManager(std::make_unique<SendActionManager>()) , _streaming(std::make_unique<Streaming>(this)) , _mediaRotation(std::make_unique<MediaRotation>()) , _histories(std::make_unique<Histories>(this)) , _stickers(std::make_unique<Stickers>(this)) -, _sponsoredMessages(std::make_unique<SponsoredMessages>(this)) , _reactions(std::make_unique<Reactions>(this)) , _emojiStatuses(std::make_unique<EmojiStatuses>(this)) , _forumIcons(std::make_unique<ForumIcons>(this)) @@ -272,7 +271,10 @@ Session::Session(not_null<Main::Session*> session) , _stories(std::make_unique<Stories>(this)) , _savedMessages(std::make_unique<SavedMessages>(this)) , _chatbots(std::make_unique<Chatbots>(this)) -, _businessInfo(std::make_unique<BusinessInfo>(this)) { +, _businessInfo(std::make_unique<BusinessInfo>(this)) +, _scheduledMessages(std::make_unique<ScheduledMessages>(this)) +, _shortcutMessages(std::make_unique<ShortcutMessages>(this)) +, _sponsoredMessages(std::make_unique<SponsoredMessages>(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -396,6 +398,7 @@ void Session::clear() { _histories->unloadAll(); _scheduledMessages = nullptr; + _shortcutMessages = nullptr; _sponsoredMessages = nullptr; _dependentMessages.clear(); base::take(_messages); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index aab939cb4..74204af26 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -44,6 +44,7 @@ class Folder; class LocationPoint; class WallPaper; class ScheduledMessages; +class ShortcutMessages; class SendActionManager; class SponsoredMessages; class Reactions; @@ -102,6 +103,9 @@ public: [[nodiscard]] ScheduledMessages &scheduledMessages() const { return *_scheduledMessages; } + [[nodiscard]] ShortcutMessages &shortcutMessages() const { + return *_shortcutMessages; + } [[nodiscard]] SendActionManager &sendActionManager() const { return *_sendActionManager; } @@ -1058,14 +1062,12 @@ private: Groups _groups; const std::unique_ptr<ChatFilters> _chatsFilters; - std::unique_ptr<ScheduledMessages> _scheduledMessages; const std::unique_ptr<CloudThemes> _cloudThemes; const std::unique_ptr<SendActionManager> _sendActionManager; const std::unique_ptr<Streaming> _streaming; const std::unique_ptr<MediaRotation> _mediaRotation; const std::unique_ptr<Histories> _histories; const std::unique_ptr<Stickers> _stickers; - std::unique_ptr<SponsoredMessages> _sponsoredMessages; const std::unique_ptr<Reactions> _reactions; const std::unique_ptr<EmojiStatuses> _emojiStatuses; const std::unique_ptr<ForumIcons> _forumIcons; @@ -1075,8 +1077,11 @@ private: const std::unique_ptr<SavedMessages> _savedMessages; const std::unique_ptr<Chatbots> _chatbots; const std::unique_ptr<BusinessInfo> _businessInfo; + std::unique_ptr<ScheduledMessages> _scheduledMessages; + std::unique_ptr<ShortcutMessages> _shortcutMessages; + std::unique_ptr<SponsoredMessages> _sponsoredMessages; - MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; + MsgId _nonHistoryEntryId = ShortcutMaxMsgId; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 8d5489d70..6b762e2db 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -27,8 +27,7 @@ using SparseUnsortedIdsSlice = AbstractSparseIds<std::vector<MsgId>>; class SparseIdsMergedSlice { public: using UniversalMsgId = MsgId; - static constexpr MsgId kScheduledTopicId - = ServerMaxMsgId + ScheduledMsgIdsRange; + static constexpr MsgId kScheduledTopicId = ScheduledMaxMsgId; struct Key { Key( diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 997d4282e..ab792cd30 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -145,6 +145,15 @@ TimeId DateFromMessage(const MTPmessage &message) { }); } +BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message) { + return message.match([](const MTPDmessage &data) { + return data.vquick_reply_shortcut_id().value_or_empty(); + }, [](const auto &) { + return BusinessShortcutId(); + }); +} + bool GoodStickerDimensions(int width, int height) { // Show all .webp (except very large ones) as stickers, // allow to open them in media viewer to see details. diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 3b511cba0..176a13f74 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -109,10 +109,13 @@ using FilterId = int32; using MessageIdsList = std::vector<FullMsgId>; -PeerId PeerFromMessage(const MTPmessage &message); -MTPDmessage::Flags FlagsFromMessage(const MTPmessage &message); -MsgId IdFromMessage(const MTPmessage &message); -TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] PeerId PeerFromMessage(const MTPmessage &message); +[[nodiscard]] MTPDmessage::Flags FlagsFromMessage( + const MTPmessage &message); +[[nodiscard]] MsgId IdFromMessage(const MTPmessage &message); +[[nodiscard]] TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message); [[nodiscard]] inline MTPint MTP_int(MsgId id) noexcept { return MTP_int(id.bare); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 0525e5c12..febc8d67c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -210,6 +210,12 @@ public: [[nodiscard]] bool isSponsored() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; + [[nodiscard]] BusinessShortcutId shortcutId() const { + return _shortcutId; + } + [[nodiscard]] bool isBusinessShortcut() const { + return _shortcutId != 0; + } void addLogEntryOriginal( WebPageId localId, @@ -662,6 +668,7 @@ private: TimeId _date = 0; TimeId _ttlDestroyAt = 0; int _boostsApplied = 0; + BusinessShortcutId _shortcutId = 0; HistoryView::Element *_mainView = nullptr; MessageGroupId _groupId = MessageGroupId(); diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 8cf7a38d5..ab5a0ef5e 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -203,6 +203,13 @@ void ContentWidget::applyAdditionalScroll(int additionalScroll) { } } +void ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) { + if (_maxVisibleHeight != maxVisibleHeight) { + _maxVisibleHeight = maxVisibleHeight; + update(); + } +} + rpl::producer<int> ContentWidget::desiredHeightValue() const { using namespace rpl::mappers; return rpl::combine( @@ -328,6 +335,10 @@ rpl::producer<bool> ContentWidget::desiredBottomShadowVisibility() const { }); } +not_null<Ui::ScrollArea*> ContentWidget::scroll() const { + return _scroll.data(); +} + Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index a7782d4f9..b8d1ebe4b 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -81,6 +81,7 @@ public: const QRect &newGeometry, int topDelta); void applyAdditionalScroll(int additionalScroll); + void applyMaxVisibleHeight(int maxVisibleHeight); int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const; [[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const { @@ -115,9 +116,13 @@ protected: doSetInnerWidget(std::move(inner))); } - not_null<Controller*> controller() const { + [[nodiscard]] not_null<Controller*> controller() const { return _controller; } + [[nodiscard]] not_null<Ui::ScrollArea*> scroll() const; + [[nodiscard]] int maxVisibleHeight() const { + return _maxVisibleHeight; + } void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; @@ -151,6 +156,7 @@ private: base::unique_qptr<Ui::RpWidget> _searchWrap = nullptr; QPointer<Ui::InputField> _searchField; int _innerDesiredHeight = 0; + int _maxVisibleHeight = 0; bool _isStackBottom = false; // Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent(). diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index cbf5117c3..b4fdd2414 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -291,9 +291,10 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto newBottom = newTop; const auto bottomRadius = st::boxRadius; + const auto maxVisibleHeight = windowHeight - newTop; // Top rounding is included in _contentHeight. auto desiredHeight = _contentHeight + bottomRadius; - accumulate_min(desiredHeight, windowHeight - newTop - newBottom); + accumulate_min(desiredHeight, maxVisibleHeight - newBottom); // First resize content to new width and get the new desired height. const auto contentLeft = 0; @@ -308,7 +309,7 @@ QRect LayerWidget::countGeometry(int newWidth) { desiredHeight += additionalScroll; contentHeight += additionalScroll; - _tillBottom = (newTop + desiredHeight >= windowHeight); + _tillBottom = (desiredHeight >= maxVisibleHeight); if (_tillBottom) { additionalScroll += contentBottom; } @@ -321,7 +322,7 @@ QRect LayerWidget::countGeometry(int newWidth) { contentTop, contentWidth, contentHeight, - }, expanding, additionalScroll); + }, expanding, additionalScroll, maxVisibleHeight); return QRect(newLeft, newTop, newWidth, desiredHeight); } diff --git a/Telegram/SourceFiles/info/info_section_widget.cpp b/Telegram/SourceFiles/info/info_section_widget.cpp index 084b2f223..a70131428 100644 --- a/Telegram/SourceFiles/info/info_section_widget.cpp +++ b/Telegram/SourceFiles/info/info_section_widget.cpp @@ -54,7 +54,11 @@ void SectionWidget::init() { const auto full = !_content->scrollBottomSkip(); const auto height = size.height() - (full ? 0 : st::boxRadius); const auto wrapGeometry = QRect{ 0, 0, size.width(), height }; - _content->updateGeometry(wrapGeometry, expanding, additionalScroll); + _content->updateGeometry( + wrapGeometry, + expanding, + additionalScroll, + size.height()); }, lifetime()); _connecting = std::make_unique<Window::ConnectionState>( diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 32bd69c7a..bf03f7131 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -934,13 +934,17 @@ object_ptr<Ui::RpWidget> WrapWidget::createTopBarSurrogate( void WrapWidget::updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll) { + int additionalScroll, + int maxVisibleHeight) { auto scrollChanged = (_additionalScroll != additionalScroll); auto geometryChanged = (geometry() != newGeometry); auto shrinkingContent = (additionalScroll < _additionalScroll); _additionalScroll = additionalScroll; + _maxVisibleHeight = maxVisibleHeight; _expanding = expanding; + _content->applyMaxVisibleHeight(maxVisibleHeight); + if (geometryChanged) { if (shrinkingContent) { setGeometry(newGeometry); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index d16108114..f102cc834 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -124,7 +124,8 @@ public: void updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll); + int additionalScroll, + int maxVisibleHeight); [[nodiscard]] int scrollBottomSkip() const; [[nodiscard]] int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const; @@ -207,6 +208,7 @@ private: std::unique_ptr<Controller> _controller; object_ptr<ContentWidget> _content = { nullptr }; int _additionalScroll = 0; + int _maxVisibleHeight = 0; bool _expanding = false; rpl::variable<bool> _grabbingForExpanding = false; object_ptr<TopBar> _topBar = { nullptr }; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index cd6d6bd03..2cddcf63e 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -44,7 +44,10 @@ Widget::Widget( , _self(controller->key().settingsSelf()) , _type(controller->section().settingsType()) , _inner([&] { - auto inner = _type->create(this, controller->parentController()); + auto inner = _type->create( + this, + controller->parentController(), + scroll()); if (inner->hasFlexibleTopBar()) { auto filler = setInnerWidget(object_ptr<Ui::RpWidget>(this)); filler->resize(1, 1); @@ -229,6 +232,12 @@ rpl::producer<QString> Widget::title() { return _inner->title(); } +void Widget::paintEvent(QPaintEvent *e) { + if (!_inner->paintOuter(this, maxVisibleHeight(), e->rect())) { + ContentWidget::paintEvent(e); + } +} + std::shared_ptr<ContentMemento> Widget::doCreateMemento() { auto result = std::make_shared<Memento>(self(), _type); saveState(result.get()); diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index e6715d68c..d2eb63615 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -84,6 +84,8 @@ private: void saveState(not_null<Memento*> memento); void restoreState(not_null<Memento*> memento); + void paintEvent(QPaintEvent *e) override; + std::shared_ptr<ContentMemento> doCreateMemento() override; not_null<UserData*> _self; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 9a3722eae..2ba11542e 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -79,7 +79,8 @@ Session::Session( not_null<Account*> account, const MTPUser &user, std::unique_ptr<SessionSettings> settings) -: _account(account) +: _userId(user.c_user().vid()) +, _account(account) , _settings(std::move(settings)) , _changes(std::make_unique<Data::Changes>(this)) , _api(std::make_unique<ApiWrap>(this)) @@ -89,7 +90,6 @@ Session::Session( , _uploader(std::make_unique<Storage::Uploader>(_api.get())) , _storage(std::make_unique<Storage::Facade>()) , _data(std::make_unique<Data::Session>(this)) -, _userId(user.c_user().vid()) , _user(_data->processUser(user)) , _emojiStickersPack(std::make_unique<Stickers::EmojiPack>(this)) , _diceStickersPacks(std::make_unique<Stickers::DicePacks>(this)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index cca061d08..635f453d5 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -199,6 +199,7 @@ private: void parseColorIndices(const MTPDhelp_peerColors &data); + const UserId _userId; const not_null<Account*> _account; const std::unique_ptr<SessionSettings> _settings; @@ -212,7 +213,6 @@ private: // _data depends on _downloader / _uploader. const std::unique_ptr<Data::Session> _data; - const UserId _userId; const not_null<UserData*> _user; // _emojiStickersPack depends on _data. diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 05fceb77b..76e353c00 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_chat_participant_status.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" @@ -119,6 +120,7 @@ namespace Media::Stories { message.action.clearDraft = false; api->sendMessage(std::move(message)); } + const auto session = &thread->session(); const auto threadPeer = thread->peer(); const auto threadHistory = thread->owningHistory(); const auto randomId = base::RandomValue<uint64>(); @@ -132,6 +134,12 @@ namespace Media::Stories { if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } + if (options.scheduled) { + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + if (options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto done = [=] { if (!--state->requests) { if (show->valid()) { @@ -155,7 +163,7 @@ namespace Media::Stories { MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), MTP_inputPeerEmpty(), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index b38e4e184..6c820afa9 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "core/application.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" #include "ui/boxes/choose_date_time.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -37,10 +39,13 @@ public: [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] rpl::producer<Type> sectionShowOther() override; + private: void setupContent(not_null<Window::SessionController*> controller); void save(); + rpl::event_stream<Type> _showOther; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<Data::AwaySchedule> _schedule; rpl::variable<bool> _enabled; @@ -197,6 +202,10 @@ rpl::producer<QString> AwayMessage::title() { return tr::lng_away_title(); } +rpl::producer<Type> AwayMessage::sectionShowOther() { + return _showOther.events(); +} + void AwayMessage::setupContent( not_null<Window::SessionController*> controller) { using namespace Data; @@ -258,7 +267,9 @@ void AwayMessage::setupContent( st::settingsButtonLightNoIcon )); create->setClickedCallback([=] { - + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("away"); + _showOther.fire(ShortcutMessagesId(id)); }); Ui::AddSkip(createInner); Ui::AddDivider(createInner); diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 5500ca539..18d9c9545 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -6,7 +6,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" -// + #include "core/application.h" #include "data/business/data_business_chatbots.h" #include "data/data_session.h" diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 39520846f..7e5ad6b1e 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -10,9 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "core/application.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/business/settings_shortcut_messages.h" #include "settings/business/settings_recipients_helper.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" @@ -50,6 +52,7 @@ private: Ui::RoundRect _bottomSkipRounding; + rpl::event_stream<Type> _showOther; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<int> _noActivityDays; rpl::variable<bool> _enabled; @@ -229,6 +232,28 @@ void Greeting::setupContent( object_ptr<Ui::VerticalLayout>(content))); const auto inner = wrap->entity(); + const auto createWrap = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = createInner->add(object_ptr<Ui::SettingsButton>( + createInner, + tr::lng_greeting_create(), + st::settingsButtonLightNoIcon + )); + create->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("hello"); + _showOther.fire(ShortcutMessagesId(id)); + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); AddBusinessRecipientsSelector(inner, { .controller = controller, .title = tr::lng_greeting_recipients(), diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp new file mode 100644 index 000000000..290aa222c --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -0,0 +1,1183 @@ +/* +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 "settings/business/settings_shortcut_messages.h" + +#include "api/api_editing.h" +#include "api/api_sending.h" +#include "apiwrap.h" +#include "base/call_delayed.h" +#include "boxes/delete_messages_box.h" +#include "boxes/premium_limits_box.h" +#include "boxes/send_files_box.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "data/data_message_reaction_id.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/controls/compose_controls_common.h" +#include "history/view/controls/history_view_compose_controls.h" +#include "history/view/history_view_corner_buttons.h" +#include "history/view/history_view_empty_list_bubble.h" +#include "history/view/history_view_list_widget.h" +#include "history/view/history_view_sticker_toast.h" +#include "history/history.h" +#include "history/history_item.h" +#include "inline_bots/inline_bot_result.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "menu/menu_send.h" +#include "settings/business/settings_recipients_helper.h" +#include "storage/localimageloader.h" +#include "storage/storage_account.h" +#include "storage/storage_media_prepare.h" +#include "ui/chat/attach/attach_send_files_way.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/controls/jump_down_button.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/scroll_area.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_chat.h" + +namespace Settings { +namespace { + +using namespace HistoryView; + +class ShortcutMessages + : public AbstractSection + , private ListDelegate + , private CornerButtonsDelegate { +public: + ShortcutMessages( + QWidget *parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + BusinessShortcutId shortcutId); + ~ShortcutMessages(); + + [[nodiscard]] static Type Id(BusinessShortcutId shortcutId); + + [[nodiscard]] Type id() const final override { + return Id(_shortcutId); + } + + [[nodiscard]] rpl::producer<QString> title() override; + + bool paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) override; + +private: + // ListDelegate interface. + Context listContext() override; + bool listScrollTo(int top, bool syntetic = true) override; + void listCancelRequest() override; + void listDeleteRequest() override; + void listTryProcessKeyInput(not_null<QKeyEvent*> e) override; + rpl::producer<Data::MessagesSlice> listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsItemGoodForSelection(not_null<HistoryItem*> item) override; + bool listIsLessInOrder( + not_null<HistoryItem*> first, + not_null<HistoryItem*> second) override; + void listSelectionChanged(SelectedItems &&items) override; + void listMarkReadTill(not_null<HistoryItem*> item) override; + void listMarkContentsRead( + const base::flat_set<not_null<HistoryItem*>> &items) override; + MessagesBarData listMessagesBar( + const std::vector<not_null<Element*>> &elements) override; + void listContentRefreshed() override; + void listUpdateDateLink( + ClickHandlerPtr &link, + not_null<Element*> view) override; + bool listElementHideReply(not_null<const Element*> view) override; + bool listElementShownUnread(not_null<const Element*> view) override; + bool listIsGoodForAroundPosition( + not_null<const Element *> view) override; + void listSendBotCommand( + const QString &command, + const FullMsgId &context) override; + void listSearch( + const QString &query, + const FullMsgId &context) override; + void listHandleViaClick(not_null<UserData*> bot) override; + not_null<Ui::ChatTheme*> listChatTheme() override; + CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; + CopyRestrictionType listCopyMediaRestrictionType( + not_null<HistoryItem*> item) override; + CopyRestrictionType listSelectRestrictionType() override; + auto listAllowedReactionsValue() + -> rpl::producer<Data::AllowedReactions> override; + void listShowPremiumToast(not_null<DocumentData*> document) override; + void listOpenPhoto( + not_null<PhotoData*> photo, + FullMsgId context) override; + void listOpenDocument( + not_null<DocumentData*> document, + FullMsgId context, + bool showInMediaView) override; + void listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) override; + QString listElementAuthorRank(not_null<const Element*> view) override; + History *listTranslateHistory() override; + void listAddTranslatedItems( + not_null<TranslateTracker*> tracker) override; + + // CornerButtonsDelegate delegate. + void cornerButtonsShowAtPosition( + Data::MessagePosition position) override; + Data::Thread *cornerButtonsThread() override; + FullMsgId cornerButtonsCurrentId() override; + bool cornerButtonsIgnoreVisibility() override; + std::optional<bool> cornerButtonsDownShown() override; + bool cornerButtonsUnreadMayBeShown() override; + bool cornerButtonsHas(CornerButtonType type) override; + + QPointer<Ui::RpWidget> createPinnedToBottom( + not_null<Ui::RpWidget*> parent) override; + void setupComposeControls(); + + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional<bool> overrideSendImagesAsPhotos = std::nullopt, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + const QStringList &files, + const QString &insertTextOnCancel); + bool confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + not_null<const QMimeData*> data, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Ui::PreparedList &list) const; + bool showSendingFilesError( + const Ui::PreparedList &list, + std::optional<bool> compress) const; + void sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter); + + void sendExistingDocument(not_null<DocumentData*> document); + bool sendExistingDocument( + not_null<DocumentData*> document, + Api::SendOptions options, + std::optional<MsgId> localId); + void sendExistingPhoto(not_null<PhotoData*> photo); + bool sendExistingPhoto( + not_null<PhotoData*> photo, + Api::SendOptions options); + void sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot); + void sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot, + Api::SendOptions options, + std::optional<MsgId> localMessageId); + + [[nodiscard]] Api::SendAction prepareSendAction( + Api::SendOptions options) const; + void send(); + void send(Api::SendOptions options); + void sendVoice(Controls::VoiceToSend &&data); + void edit( + not_null<HistoryItem*> item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId); + void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos); + [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] FullReplyTo replyTo() const; + void doSetInnerFocus(); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId = {}); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms); + void showAtEnd(); + void finishSending(); + + const not_null<Window::SessionController*> _controller; + const not_null<Main::Session*> _session; + const not_null<Ui::ScrollArea*> _scroll; + const BusinessShortcutId _shortcutId; + const not_null<History*> _history; + std::shared_ptr<Ui::ChatStyle> _style; + std::shared_ptr<Ui::ChatTheme> _theme; + QPointer<ListWidget> _inner; + std::unique_ptr<Ui::RpWidget> _controlsWrap; + std::unique_ptr<ComposeControls> _composeControls; + bool _skipScrollEvent = false; + + std::unique_ptr<StickerToast> _stickerToast; + + FullMsgId _lastShownAt; + CornerButtons _cornerButtons; + + Data::MessagesSlice _lastSlice; + bool _choosingAttach = false; + +}; + +struct Factory : AbstractSectionFactory { + explicit Factory(BusinessShortcutId shortcutId) + : shortcutId(shortcutId) { + } + + object_ptr<AbstractSection> create( + not_null<QWidget*> parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll + ) const final override { + return object_ptr<ShortcutMessages>( + parent, + controller, + scroll, + shortcutId); + } + + const BusinessShortcutId shortcutId = {}; +}; + +ShortcutMessages::ShortcutMessages( + QWidget *parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + BusinessShortcutId shortcutId) +: AbstractSection(parent) +, _controller(controller) +, _session(&controller->session()) +, _scroll(scroll) +, _shortcutId(shortcutId) +, _history(_session->data().history(_session->user()->id)) +, _cornerButtons( + _scroll, + controller->chatStyle(), + static_cast<HistoryView::CornerButtonsDelegate*>(this)) { + controller->chatStyle()->paletteChanged( + ) | rpl::start_with_next([=] { + _scroll->updateBars(); + }, _scroll->lifetime()); + + _style = std::make_shared<Ui::ChatStyle>(_session->colorIndicesValue()); + _theme = std::shared_ptr<Ui::ChatTheme>( + Window::Theme::DefaultChatThemeOn(lifetime())); + + _inner = Ui::CreateChild<ListWidget>( + this, + controller, + static_cast<ListDelegate*>(this)); + //_scroll->scrolls( + //) | rpl::start_with_next([=] { + // onScroll(); + //}, lifetime()); + + _inner->editMessageRequested( + ) | rpl::start_with_next([=](auto fullId) { + if (const auto item = _session->data().message(fullId)) { + const auto media = item->media(); + if (!media || media->webpage() || media->allowsEditCaption()) { + _composeControls->editMessage(fullId); + } + } + }, _inner->lifetime()); + + { + auto emptyInfo = base::make_unique_q<EmptyListBubbleWidget>( + _inner, + controller->chatStyle(), + st::msgServicePadding); + const auto emptyText = Ui::Text::Semibold( + tr::lng_scheduled_messages_empty(tr::now)); + emptyInfo->setText(emptyText); + _inner->setEmptyInfoWidget(std::move(emptyInfo)); + } + + widthValue() | rpl::start_with_next([=](int width) { + resize(width, width); + }, lifetime()); +} + +ShortcutMessages::~ShortcutMessages() = default; + +Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { + return std::make_shared<Factory>(shortcutId); +} + +rpl::producer<QString> ShortcutMessages::title() { + return rpl::single(u"Editing messages list"_q); +} + +bool ShortcutMessages::paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) { + const auto window = outer->window()->height(); + Window::SectionWidget::PaintBackground( + _theme.get(), + outer, + std::max(outer->height(), maxVisibleHeight), + 0, + clip); + return true; +} + +void ShortcutMessages::setupComposeControls() { + _composeControls->setHistory({ + .history = _history.get(), + .writeRestriction = rpl::single(Controls::WriteRestriction()), + }); + + _composeControls->height( + ) | rpl::start_with_next([=](int height) { + const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); + _controlsWrap->resize(width(), height); + if (wasMax) { + listScrollTo(_scroll->scrollTopMax()); + } + }, lifetime()); + + _composeControls->cancelRequests( + ) | rpl::start_with_next([=] { + listCancelRequest(); + }, lifetime()); + + _composeControls->sendRequests( + ) | rpl::start_with_next([=] { + send(); + }, lifetime()); + + _composeControls->sendVoiceRequests( + ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { + sendVoice(std::move(data)); + }, lifetime()); + + _composeControls->sendCommandRequests( + ) | rpl::start_with_next([=](const QString &command) { + listSendBotCommand(command, FullMsgId()); + }, lifetime()); + + const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0); + _composeControls->editRequests( + ) | rpl::start_with_next([=](auto data) { + if (const auto item = _session->data().message(data.fullId)) { + if (item->isBusinessShortcut()) { + edit(item, data.options, saveEditMsgRequestId); + } + } + }, lifetime()); + + _composeControls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=](std::optional<bool> overrideCompress) { + _choosingAttach = true; + base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] { + _choosingAttach = false; + chooseAttach(overrideCompress); + }); + }, lifetime()); + + _composeControls->fileChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + _controller->hideLayer(anim::type::normal); + sendExistingDocument(data.document); + }, lifetime()); + + _composeControls->photoChosen( + ) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) { + sendExistingPhoto(chosen.photo); + }, lifetime()); + + _composeControls->inlineResultChosen( + ) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) { + sendInlineResult(chosen.result, chosen.bot); + }, lifetime()); + + _composeControls->jumpToItemRequests( + ) | rpl::start_with_next([=](FullReplyTo to) { + if (const auto item = _session->data().message(to.messageId)) { + showAtPosition(item->position()); + } + }, lifetime()); + + _composeControls->scrollKeyEvents( + ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) { + _scroll->keyPressEvent(e); + }, lifetime()); + + _composeControls->editLastMessageRequests( + ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) { + if (!_inner->lastMessageEditRequestNotify()) { + _scroll->keyPressEvent(e); + } + }, lifetime()); + + _composeControls->setMimeDataHook([=]( + not_null<const QMimeData*> data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return Core::CanSendFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + //return confirmSendingFiles( + // data, + // std::nullopt, + // Core::ReadMimeText(data));#TODO + } + Unexpected("action in MimeData hook."); + }); + + _composeControls->lockShowStarts( + ) | rpl::start_with_next([=] { + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); + }, lifetime()); + + _composeControls->viewportEvents( + ) | rpl::start_with_next([=](not_null<QEvent*> e) { + _scroll->viewportEvent(e); + }, lifetime()); + + _controlsWrap->widthValue() | rpl::start_with_next([=](int width) { + _composeControls->resizeToWidth(width); + }, _controlsWrap->lifetime()); + _composeControls->height() | rpl::start_with_next([=](int height) { + _controlsWrap->resize(_controlsWrap->width(), height); + }, _controlsWrap->lifetime()); +} + +QPointer<Ui::RpWidget> ShortcutMessages::createPinnedToBottom( + not_null<Ui::RpWidget*> parent) { + _controlsWrap = std::make_unique<Ui::RpWidget>(parent); + _composeControls = std::make_unique<ComposeControls>( + _controlsWrap.get(), + _controller, + [=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); }, + ComposeControls::Mode::Scheduled, + SendMenu::Type::Disabled); + + setupComposeControls(); + + return _controlsWrap.get(); +} + +Context ShortcutMessages::listContext() { + return Context::History; +} + +bool ShortcutMessages::listScrollTo(int top, bool syntetic) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { + //updateInnerVisibleArea(); + return false; + } + _scroll->scrollToY(top); + return true; +} + +void ShortcutMessages::listCancelRequest() { + if (_inner && !_inner->getSelectedItems().empty()) { + //clearSelected(); + return; + } else if (_composeControls->handleCancelRequest()) { + return; + } + _controller->showBackFromStack(); +} + +void ShortcutMessages::listDeleteRequest() { + //confirmDeleteSelected(); +} + +void ShortcutMessages::listTryProcessKeyInput(not_null<QKeyEvent*> e) { + _composeControls->tryProcessKeyInput(e); +} + +rpl::producer<Data::MessagesSlice> ShortcutMessages::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto data = &_controller->session().data(); + //return rpl::single(rpl::empty) | rpl::then( + // data->scheduledMessages().updates(_history) + //) | rpl::map([=] { + // return data->scheduledMessages().list(_history); + //}) | rpl::after_next([=](const Data::MessagesSlice &slice) { + // highlightSingleNewMessage(slice); + //}); + return rpl::never<Data::MessagesSlice>(); +} + +bool ShortcutMessages::listAllowsMultiSelect() { + return true; +} + +bool ShortcutMessages::listIsItemGoodForSelection( + not_null<HistoryItem*> item) { + return !item->isSending() && !item->hasFailed(); +} + +bool ShortcutMessages::listIsLessInOrder( + not_null<HistoryItem*> first, + not_null<HistoryItem*> second) { + return first->position() < second->position(); +} + +void ShortcutMessages::listSelectionChanged(SelectedItems &&items) { +} + +void ShortcutMessages::listMarkReadTill(not_null<HistoryItem*> item) { +} + +void ShortcutMessages::listMarkContentsRead( + const base::flat_set<not_null<HistoryItem*>> &items) { +} + +MessagesBarData ShortcutMessages::listMessagesBar( + const std::vector<not_null<Element*>> &elements) { + return {}; +} + +void ShortcutMessages::listContentRefreshed() { +} + +void ShortcutMessages::listUpdateDateLink( + ClickHandlerPtr &link, + not_null<Element*> view) { +} + +bool ShortcutMessages::listElementHideReply(not_null<const Element*> view) { + return false; +} + +bool ShortcutMessages::listElementShownUnread(not_null<const Element*> view) { + return true; +} + +bool ShortcutMessages::listIsGoodForAroundPosition( + not_null<const Element*> view) { + return true; +} + +void ShortcutMessages::listSendBotCommand( + const QString &command, + const FullMsgId &context) { +} + +void ShortcutMessages::listSearch( + const QString &query, + const FullMsgId &context) { + const auto inChat = _history->peer->isUser() + ? Dialogs::Key() + : Dialogs::Key(_history); + _controller->searchMessages(query, inChat); +} + +void ShortcutMessages::listHandleViaClick(not_null<UserData*> bot) { + _composeControls->setText({ '@' + bot->username() + ' ' }); +} + +not_null<Ui::ChatTheme*> ShortcutMessages::listChatTheme() { + return _theme.get(); +} + +CopyRestrictionType ShortcutMessages::listCopyRestrictionType( + HistoryItem *item) { + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listCopyMediaRestrictionType( + not_null<HistoryItem*> item) { + if (const auto media = item->media()) { + if (const auto invoice = media->invoice()) { + if (invoice->extendedMedia) { + return CopyMediaRestrictionTypeFor(_history->peer, item); + } + } + } + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listSelectRestrictionType() { + return CopyRestrictionType::None; +} + +auto ShortcutMessages::listAllowedReactionsValue() +-> rpl::producer<Data::AllowedReactions> { + return rpl::single(Data::AllowedReactions()); +} + +void ShortcutMessages::listShowPremiumToast( + not_null<DocumentData*> document) { + if (!_stickerToast) { + _stickerToast = std::make_unique<HistoryView::StickerToast>( + _controller, + this, + [=] { _stickerToast = nullptr; }); + } + _stickerToast->showFor(document); +} + +void ShortcutMessages::listOpenPhoto( + not_null<PhotoData*> photo, + FullMsgId context) { + _controller->openPhoto(photo, { context }); +} + +void ShortcutMessages::listOpenDocument( + not_null<DocumentData*> document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, showInMediaView, { context }); +} + +void ShortcutMessages::listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) { +} + +QString ShortcutMessages::listElementAuthorRank( + not_null<const Element*> view) { + return {}; +} + +History *ShortcutMessages::listTranslateHistory() { + return nullptr; +} + +void ShortcutMessages::listAddTranslatedItems( + not_null<TranslateTracker*> tracker) { +} + +void ShortcutMessages::cornerButtonsShowAtPosition( + Data::MessagePosition position) { + //showAtPosition(position); +} + +Data::Thread *ShortcutMessages::cornerButtonsThread() { + return _history; +} + +FullMsgId ShortcutMessages::cornerButtonsCurrentId() { + return _lastShownAt; +} + +bool ShortcutMessages::cornerButtonsIgnoreVisibility() { + return false;// animatingShow(); +} + +std::optional<bool> ShortcutMessages::cornerButtonsDownShown() { + if (_composeControls->isLockPresent() + || _composeControls->isTTLButtonShown()) { + return false; + } + //const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + //if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { + // return true; + //} else if (_inner->loadedAtBottomKnown()) { + // return !_inner->loadedAtBottom(); + //} + return std::nullopt; +} + +bool ShortcutMessages::cornerButtonsUnreadMayBeShown() { + return _inner->loadedAtBottomKnown() + && !_composeControls->isLockPresent() + && !_composeControls->isTTLButtonShown(); +} + +bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { + return (type == CornerButtonType::Down); +} + +void ShortcutMessages::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + // #TODO replies schedule + _session->api().sendFile(fileContent, type, prepareSendAction({})); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list) const { + return showSendingFilesError(list, std::nullopt); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list, + std::optional<bool> compress) const { + const auto text = [&] { + using Error = Ui::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return u"(toolarge)"_q; + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } else if (text == u"(toolarge)"_q) { + const auto fileSize = list.files.back().size; + _controller->show( + Box(FileSizeLimitBox, _session, fileSize, nullptr)); + return true; + } + + _controller->showToast(text); + return true; +} + +Api::SendAction ShortcutMessages::prepareSendAction( + Api::SendOptions options) const { + auto result = Api::SendAction(_history, options); + result.replyTo = replyTo(); + result.options.shortcutId = _shortcutId; + result.options.sendAs = _composeControls->sendAsPeer(); + return result; +} + +void ShortcutMessages::send() { + if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { + return; + } + send({}); + // #TODO replies schedule + //const auto callback = [=](Api::SendOptions options) { send(options); }; + //Ui::show( + // PrepareScheduleBox(this, sendMenuType(), callback), + // Ui::LayerOption::KeepOther); +} + +void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { + auto action = prepareSendAction(data.options); + _session->api().sendVoiceMessage( + data.bytes, + data.waveform, + data.duration, + std::move(action)); + + _composeControls->cancelReplyMessage(); + _composeControls->clearListenState(); + finishSending(); +} + +void ShortcutMessages::send(Api::SendOptions options) { + _cornerButtons.clearReplyReturns(); + + auto message = Api::MessageToSend(prepareSendAction(options)); + message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); + message.webPage = _composeControls->webPageDraft(); + + _session->api().sendMessage(std::move(message)); + + _composeControls->clear(); + + finishSending(); +} + +void ShortcutMessages::edit( + not_null<HistoryItem*> item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId) { + if (*saveEditMsgRequestId) { + return; + } + const auto webpage = _composeControls->webPageDraft(); + auto sending = TextWithEntities(); + auto left = _composeControls->prepareTextForEditMsg(); + + const auto originalLeftSize = left.text.size(); + const auto hasMediaWithCaption = item + && item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(_session).captionLengthCurrent(); + if (!TextUtilities::CutPart(sending, left, maxCaptionSize) + && !hasMediaWithCaption) { + if (item) { + _controller->show(Box<DeleteMessagesBox>(item, false)); + } else { + doSetInnerFocus(); + } + return; + } else if (!left.text.isEmpty()) { + const auto remove = originalLeftSize - maxCaptionSize; + _controller->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } + + lifetime().add([=] { + if (!*saveEditMsgRequestId) { + return; + } + _session->api().request(base::take(*saveEditMsgRequestId)).cancel(); + }); + + const auto done = [=](mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + _composeControls->cancelEditMessage(); + } + }; + + const auto fail = [=](const QString &error, mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + } + + if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { + _controller->showToast(tr::lng_edit_error(tr::now)); + } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { + _composeControls->cancelEditMessage(); + } else if (error == u"MESSAGE_EMPTY"_q) { + doSetInnerFocus(); + } else { + _controller->showToast(tr::lng_edit_error(tr::now)); + } + update(); + return true; + }; + + *saveEditMsgRequestId = Api::EditTextMessage( + item, + sending, + webpage, + options, + crl::guard(this, done), + crl::guard(this, fail)); + + _composeControls->hidePanelsAnimated(); + doSetInnerFocus(); +} + +bool ShortcutMessages::confirmSendingFiles( + not_null<const QMimeData*> data, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + const auto hasImage = data->hasImage(); + const auto premium = _controller->session().user()->isPremium(); + + if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { + auto list = Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize, + premium); + if (list.error != Ui::PreparedList::Error::NonLocalUrl) { + if (list.error == Ui::PreparedList::Error::None + || !hasImage) { + const auto emptyTextOnCancel = QString(); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list), emptyTextOnCancel); + return true; + } + } + } + + if (auto read = Core::ReadMimeImage(data)) { + confirmSendingFiles( + std::move(read.image), + std::move(read.content), + overrideSendImagesAsPhotos, + insertTextOnCancel); + return true; + } + return false; +} + +bool ShortcutMessages::confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { + if (_composeControls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { + return false; + } + + auto box = Box<SendFilesBox>( + _controller, + std::move(list), + _composeControls->getTextWithAppliedMarkdown(), + _history->peer, + Api::SendType::Normal, + SendMenu::Type::SilentOnly); // #TODO replies schedule + + box->setConfirmedCallback(crl::guard(this, [=]( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + sendingFilesConfirmed( + std::move(list), + way, + std::move(caption), + options, + ctrlShiftEnter); + })); + box->setCancelledCallback(_composeControls->restoreTextCallback( + insertTextOnCancel)); + + //ActivateWindow(_controller); + _controller->show(std::move(box)); + + return true; +} + +bool ShortcutMessages::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + return confirmSendingFiles(std::move(list), insertTextOnCancel); +} + +void ShortcutMessages::sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + Expects(list.filesToProcess.empty()); + + if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + return; + } + auto groups = DivideByGroups( + std::move(list), + way, + _history->peer->slowmodeApplied()); + const auto type = way.sendImagesAsPhotos() + ? SendMediaType::Photo + : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + if ((groups.size() != 1 || !groups.front().sentWithCaption()) + && !caption.text.isEmpty()) { + auto message = Api::MessageToSend(action); + message.textWithTags = base::take(caption); + _session->api().sendMessage(std::move(message)); + } + for (auto &group : groups) { + const auto album = (group.type != Ui::AlbumType::None) + ? std::make_shared<SendingAlbum>() + : nullptr; + _session->api().sendFiles( + std::move(group.list), + type, + base::take(caption), + album, + action); + } + if (_composeControls->replyingToMessage() == action.replyTo) { + _composeControls->cancelReplyMessage(); + } + finishSending(); +} + +void ShortcutMessages::chooseAttach( + std::optional<bool> overrideSendImagesAsPhotos) { + _choosingAttach = false; + + const auto filter = (overrideSendImagesAsPhotos == true) + ? FileDialog::ImagesOrAllFilter() + : FileDialog::AllOrImagesFilter(); + FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto read = Images::Read({ + .content = result.remoteContent, + }); + if (!read.image.isNull() && !read.animated) { + confirmSendingFiles( + std::move(read.image), + std::move(result.remoteContent), + overrideSendImagesAsPhotos); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + const auto premium = _controller->session().user()->isPremium(); + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize, + premium); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list)); + } + }), nullptr); +} + +void ShortcutMessages::finishSending() { + _composeControls->hidePanelsAnimated(); + //if (_previewData && _previewData->pendingTill) previewCancel(); + doSetInnerFocus(); + showAtEnd(); +} + +void ShortcutMessages::showAtEnd() { + showAtPosition(Data::MaxMessagePosition); +} + +void ShortcutMessages::doSetInnerFocus() { + if (!_inner->getSelectedText().rich.text.isEmpty() + || !_inner->getSelectedItems().empty() + || !_composeControls->focus()) { + _inner->setFocus(); + } +} + +void ShortcutMessages::sendExistingDocument( + not_null<DocumentData*> document) { + sendExistingDocument(document, {}, std::nullopt); +} + +bool ShortcutMessages::sendExistingDocument( + not_null<DocumentData*> document, + Api::SendOptions options, + std::optional<MsgId> localId) { + Api::SendExistingDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendExistingPhoto(not_null<PhotoData*> photo) { + sendExistingPhoto(photo, {}); +} + +bool ShortcutMessages::sendExistingPhoto( + not_null<PhotoData*> photo, + Api::SendOptions options) { + Api::SendExistingPhoto( + Api::MessageToSend(prepareSendAction(options)), + photo); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot) { + const auto errorText = result->getErrorOnSend(_history); + if (!errorText.isEmpty()) { + _controller->showToast(errorText); + return; + } + sendInlineResult(result, bot, {}, std::nullopt); + //const auto callback = [=](Api::SendOptions options) { + // sendInlineResult(result, bot, options); + //}; + //Ui::show( + // PrepareScheduleBox(this, sendMenuType(), callback), + // Ui::LayerOption::KeepOther); +} + +void ShortcutMessages::sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot, + Api::SendOptions options, + std::optional<MsgId> localMessageId) { + auto action = prepareSendAction(options); + action.generateLocal = true; + _session->api().sendInlineResult(bot, result, action, localMessageId); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + bot->session().local().writeRecentHashtagsAndBots(); + } + finishSending(); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId) { + showAtPosition(position, originItemId, {}); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms) { + _lastShownAt = position.fullId; + _inner->showAtPosition( + position, + params, + _cornerButtons.doneJumpFrom(position.fullId, originItemId, true)); +} + +FullReplyTo ShortcutMessages::replyTo() const { + return _composeControls->replyingToMessage(); +} + +} // namespace + +Type ShortcutMessagesId(int shortcutId) { + return ShortcutMessages::Id(shortcutId); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h new file mode 100644 index 000000000..325b12602 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ShortcutMessagesId(int shortcutId); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index aae5868df..e3aa43129 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" // AmPremiumValue. #include "data/data_session.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -359,6 +360,7 @@ void Business::setupContent() { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); _controller->session().data().businessInfo().preloadTimezones(); + _controller->session().data().shortcutMessages().preloadShortcuts(); Ui::AddSkip(content, st::settingsFromFileTop); @@ -566,7 +568,8 @@ template <> struct SectionFactory<Business> : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll ) const final override { return object_ptr<Business>(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index ea80b4573..02beb1003 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -90,6 +90,13 @@ public: } virtual void setStepDataReference(std::any &data) { } + + virtual bool paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) { + return false; + } }; enum class IconType { diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 911e22897..16a03b9e4 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "settings/settings_type.h" +namespace Ui { +class ScrollArea; +} // namespace Ui + namespace Ui::Menu { struct MenuCallback; } // namespace Ui::Menu @@ -27,7 +31,8 @@ class AbstractSection; struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller) const = 0; + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll) const = 0; [[nodiscard]] virtual bool hasCustomTopBar() const { return false; } @@ -39,7 +44,8 @@ template <typename SectionType> struct SectionFactory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll ) const final override { return object_ptr<SectionType>(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 52114830f..717627573 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -43,7 +43,8 @@ struct Factory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll ) const final override { return object_ptr<NotificationsType>(parent, controller, type); } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index e56122c94..ec17c8aac 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1267,7 +1267,8 @@ template <> struct SectionFactory<Premium> : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll ) const final override { return object_ptr<Premium>(parent, controller); } From 5c11fa4f6321671545ec9553a255d8b4984d8ac6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 26 Feb 2024 22:01:23 +0400 Subject: [PATCH 052/108] Update API scheme on layer 176. --- .../data/business/data_business_info.cpp | 48 +++++++++++++------ Telegram/SourceFiles/data/data_user.cpp | 11 +++-- Telegram/SourceFiles/mtproto/scheme/api.tl | 12 +++-- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 1a1d30555..5fa0844ee 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -29,6 +29,38 @@ namespace { MTP_vector_from_range(list | ranges::views::transform(proj))); } +[[nodiscard]] MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data) { + //MTP_flags(RecipientsFlags(data.recipients, Flag())), + // MTP_vector_from_range( + // (data.recipients.allButExcluded + // ? data.recipients.excluded + // : data.recipients.included).list + // | ranges::views::transform(&UserData::inputUser)), + + using Flag = MTPDinputBusinessRecipients::Flag; + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + const auto flags = Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) + | (chats.list.empty() ? Flag() : Flag::f_users) + | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); + const auto &users = data.allButExcluded + ? data.excluded + : data.included; + return MTP_inputBusinessRecipients( + MTP_flags(flags), + MTP_vector_from_range(users.list + | ranges::views::transform(&UserData::inputUser))); +} + template <typename Flag> [[nodiscard]] auto RecipientsFlags( const BusinessRecipients &data, @@ -62,29 +94,17 @@ template <typename Flag> } [[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) { - using Flag = MTPDinputBusinessAwayMessage::Flag; return MTP_inputBusinessAwayMessage( - MTP_flags(RecipientsFlags(data.recipients, Flag())), MTP_int(data.shortcutId), ToMTP(data.schedule), - MTP_vector_from_range( - (data.recipients.allButExcluded - ? data.recipients.excluded - : data.recipients.included).list - | ranges::views::transform(&UserData::inputUser))); + ToMTP(data.recipients)); } [[nodiscard]] MTPInputBusinessGreetingMessage ToMTP( const GreetingSettings &data) { - using Flag = MTPDinputBusinessGreetingMessage::Flag; return MTP_inputBusinessGreetingMessage( - MTP_flags(RecipientsFlags(data.recipients, Flag())), MTP_int(data.shortcutId), - MTP_vector_from_range( - (data.recipients.allButExcluded - ? data.recipients.excluded - : data.recipients.included).list - | ranges::views::transform(&UserData::inputUser)), + ToMTP(data.recipients), MTP_int(data.noActivityDays)); } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 35ad558c7..a351ce678 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -62,11 +62,12 @@ using UpdateFlag = Data::PeerUpdate::Flag; return result; } -template <typename T> -Data::BusinessRecipients RecipientsFromMTP( +Data::BusinessRecipients FromMTP( not_null<Data::Session*> owner, - const T &data) { + const MTPBusinessRecipients &recipients) { using Type = Data::BusinessChatType; + + const auto &data = recipients.data(); auto result = Data::BusinessRecipients{ .allButExcluded = data.is_exclude_selected(), }; @@ -94,7 +95,7 @@ Data::BusinessRecipients RecipientsFromMTP( } const auto &data = message->data(); auto result = Data::AwaySettings{ - .recipients = RecipientsFromMTP(owner, data), + .recipients = FromMTP(owner, data.vrecipients()), .shortcutId = data.vshortcut_id().v, }; data.vschedule().match([&]( @@ -120,7 +121,7 @@ Data::BusinessRecipients RecipientsFromMTP( } const auto &data = message->data(); return Data::GreetingSettings{ - .recipients = RecipientsFromMTP(owner, data), + .recipients = FromMTP(owner, data.vrecipients()), .noActivityDays = data.vno_activity_days().v, .shortcutId = data.vshortcut_id().v, }; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 9fdf2c153..cd07a4e19 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1671,17 +1671,21 @@ businessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string week businessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation; +inputBusinessRecipients#6f8b32aa flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<InputUser> = InputBusinessRecipients; + +businessRecipients#21108ff7 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<long> = BusinessRecipients; + businessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule; businessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule; businessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule; -inputBusinessGreetingMessage#7d4a3609 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int users:flags.4?Vector<InputUser> no_activity_days:int = InputBusinessGreetingMessage; +inputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRecipients no_activity_days:int = InputBusinessGreetingMessage; -businessGreetingMessage#a098d54c flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int users:flags.4?Vector<long> no_activity_days:int = BusinessGreetingMessage; +businessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage; -inputBusinessAwayMessage#ce6fda48 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int schedule:BusinessAwayMessageSchedule users:flags.4?Vector<InputUser> = InputBusinessAwayMessage; +inputBusinessAwayMessage#edac03f4 shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage; -businessAwayMessage#9acd7a15 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true shortcut_id:int schedule:BusinessAwayMessageSchedule users:flags.4?Vector<long> = BusinessAwayMessage; +businessAwayMessage#1bd9bebc shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage; timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; From 7f3ebde252efc86c71e6ad414962b9c563cf93e7 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 26 Feb 2024 22:24:00 +0400 Subject: [PATCH 053/108] Implement sending of shortcutted messages. --- Telegram/SourceFiles/api/api_editing.cpp | 3 + Telegram/SourceFiles/api/api_sending.cpp | 95 ++++--- Telegram/SourceFiles/api/api_updates.cpp | 26 ++ Telegram/SourceFiles/apiwrap.cpp | 104 ++++--- .../boxes/background_preview_box.cpp | 38 +-- .../boxes/peers/edit_peer_color_box.cpp | 59 ++-- .../boxes/reactions_settings_box.cpp | 19 +- .../data/business/data_shortcut_messages.cpp | 12 +- .../data/business/data_shortcut_messages.h | 2 + .../data/data_download_manager.cpp | 27 +- Telegram/SourceFiles/data/data_histories.cpp | 21 ++ .../SourceFiles/data/data_replies_list.cpp | 10 +- .../data/data_sponsored_messages.cpp | 2 +- Telegram/SourceFiles/data/data_stories.cpp | 2 +- Telegram/SourceFiles/data/data_types.h | 2 + .../admin_log/history_admin_log_item.cpp | 96 +++---- Telegram/SourceFiles/history/history.cpp | 137 +++------ Telegram/SourceFiles/history/history.h | 75 ++--- Telegram/SourceFiles/history/history_item.cpp | 263 +++++++----------- Telegram/SourceFiles/history/history_item.h | 104 +++---- .../history/history_item_components.cpp | 1 + .../history/history_item_helpers.cpp | 14 +- .../controls/history_view_draft_options.cpp | 52 ++-- .../history/view/history_view_about_view.cpp | 57 ++-- .../history/view/history_view_element.h | 1 + .../history/view/history_view_list_widget.cpp | 8 +- .../history/view/history_view_list_widget.h | 2 +- .../history/view/history_view_message.cpp | 5 + .../inline_bots/inline_bot_result.cpp | 32 +-- .../inline_bots/inline_bot_result.h | 9 +- .../inline_bots/inline_bot_send_data.cpp | 88 +----- .../inline_bots/inline_bot_send_data.h | 47 +--- .../media/stories/media_stories_reactions.cpp | 23 +- .../business/settings_shortcut_messages.cpp | 149 +++++++--- .../support/support_autocomplete.cpp | 58 ++-- 35 files changed, 709 insertions(+), 934 deletions(-) diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 126223bee..a1e168adf 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_media.h" #include "api/api_text_entities.h" #include "ui/boxes/confirm_box.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_histories.h" #include "data/data_scheduled_messages.h" #include "data/data_session.h" @@ -92,6 +93,8 @@ mtpRequestId EditMessage( const auto id = item->isScheduled() ? session->data().scheduledMessages().lookupId(item) + : item->isBusinessShortcut() + ? session->data().shortcutMessages().lookupId(item) : item->id; return api->request(MTPmessages_EditMessage( MTP_flags(flags), diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 96bdf4a4a..982992ce3 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -85,20 +85,21 @@ void SendExistingMedia( ? (*localMessageId) : session->data().nextLocalMessageId()); const auto randomId = base::RandomValue<uint64>(); + const auto &action = message.action; auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); - if (message.action.replyTo) { + if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); - const auto silentPost = ShouldSendSilent(peer, message.action.options); - InnerFillMessagePostFlags(message.action.options, peer, flags); + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - const auto sendAs = message.action.options.sendAs; + const auto sendAs = action.options.sendAs; const auto messageFromId = sendAs ? sendAs->id : anonymousPost @@ -125,33 +126,30 @@ void SendExistingMedia( } const auto captionText = caption.text; - if (message.action.options.scheduled) { + if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } - if (message.action.options.shortcutId) { + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } session->data().registerMessageRandomId(randomId, newId); - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - message.action.replyTo, - HistoryItem::NewMessageDate(message.action.options.scheduled), - messageFromId, - messagePostAuthor, - media, - caption, - HistoryMessageMarkupData()); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, media, caption); const auto performRequest = [=](const auto &repeatRequest) -> void { auto &histories = history->owner().histories(); const auto session = &history->session(); - const auto &action = message.action; const auto usedFileReference = media->fileReference(); histories.sendPreparedMessage( history, @@ -187,7 +185,7 @@ void SendExistingMedia( }; performRequest(performRequest); - api->finishForwarding(message.action); + api->finishForwarding(action); } } // namespace @@ -307,23 +305,23 @@ bool SendDice(MessageToSend &message) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } session->data().registerMessageRandomId(randomId, newId); - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - action.replyTo, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - TextWithEntities(), - MTP_messageMediaDice(MTP_int(0), MTP_string(emoji)), - HistoryMessageMarkupData()); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, TextWithEntities(), MTP_messageMediaDice( + MTP_int(0), + MTP_string(emoji))); histories.sendPreparedMessage( history, action.replyTo, @@ -420,7 +418,13 @@ void SendConfirmedFile( if (file->to.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - // Scheduled messages have no the 'edited' badge. + // Scheduled messages have no 'edited' badge. + flags |= MessageFlag::HideEdited; + } + if (file->to.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + + // Shortcut messages have no 'edited' badge. flags |= MessageFlag::HideEdited; } if (file->type == SendMediaType::Audio) { @@ -429,8 +433,7 @@ void SendConfirmedFile( } } - const auto messageFromId = - file->to.options.sendAs + const auto messageFromId = file->to.options.sendAs ? file->to.options.sendAs->id : anonymousPost ? PeerId() @@ -500,19 +503,15 @@ void SendConfirmedFile( edition.savePreviousMedia = true; itemToEdit->applyEdition(std::move(edition)); } else { - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - file->to.replyTo, - HistoryItem::NewMessageDate(file->to.options.scheduled), - messageFromId, - messagePostAuthor, - caption, - media, - HistoryMessageMarkupData(), - groupId); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = file->to.replyTo, + .date = HistoryItem::NewMessageDate(file->to.options), + .shortcutId = file->to.options.shortcutId, + .postAuthor = messagePostAuthor, + }, caption, media); } if (isEditing) { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 679faa063..57db15d92 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtp_instance.h" #include "mtproto/mtproto_config.h" #include "mtproto/mtproto_dc_options.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_saved_messages.h" @@ -1774,6 +1775,31 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().scheduledMessages().apply(d); } break; + case mtpc_updateQuickReplies: { + const auto &d = update.c_updateQuickReplies(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateNewQuickReply: { + const auto &d = update.c_updateNewQuickReply(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateDeleteQuickReply: { + const auto &d = update.c_updateDeleteQuickReply(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateQuickReplyMessage: { + const auto &d = update.c_updateQuickReplyMessage(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateDeleteQuickReplyMessages: { + const auto &d = update.c_updateDeleteQuickReplyMessages(); + session().data().shortcutMessages().apply(d); + } break; + case mtpc_updateWebPage: { auto &d = update.c_updateWebPage(); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index bca32c3da..4676c5a50 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2449,6 +2449,14 @@ void ApiWrap::refreshFileReference( request(MTPmessages_GetScheduledMessages( item->history()->peer->input, MTP_vector<MTPint>(1, MTP_int(realId)))); + } else if (item->isBusinessShortcut()) { + const auto &shortcuts = _session->data().shortcutMessages(); + const auto realId = shortcuts.lookupId(item); + request(MTPmessages_GetQuickReplyMessages( + MTP_flags(MTPmessages_GetQuickReplyMessages::Flag::f_id), + MTP_int(item->shortcutId()), + MTP_vector<MTPint>(1, MTP_int(realId)), + MTP_long(0))); } else if (const auto channel = item->history()->peer->asChannel()) { request(MTPchannels_GetMessages( channel->inputChannel, @@ -3232,6 +3240,7 @@ void ApiWrap::forwardMessages( sendFlags |= SendFlag::f_schedule_date; } if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; sendFlags |= SendFlag::f_quick_reply_shortcut; } if (draft.options != Data::ForwardOptions::PreserveInfo) { @@ -3317,14 +3326,15 @@ void ApiWrap::forwardMessages( const auto messagePostAuthor = peer->isBroadcast() ? self->name() : QString(); - history->addNewLocalMessage( - newId.msg, - flags, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - item, - topMsgId); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = { .topicRootId = topMsgId }, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, item); _session->data().registerMessageRandomId(randomId, newId); if (!localIds) { localIds = std::make_shared<base::flat_map<uint64, FullMsgId>>(); @@ -3405,6 +3415,9 @@ void ApiWrap::sendSharedContact( if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + } const auto messageFromId = action.options.sendAs ? action.options.sendAs->id : anonymousPost @@ -3413,23 +3426,20 @@ void ApiWrap::sendSharedContact( const auto messagePostAuthor = peer->isBroadcast() ? _session->user()->name() : QString(); - const auto viaBotId = UserId(); - const auto item = history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - action.replyTo, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - TextWithEntities(), - MTP_messageMediaContact( - MTP_string(phone), - MTP_string(firstName), - MTP_string(lastName), - MTP_string(), // vcard - MTP_long(userId.bare)), - HistoryMessageMarkupData()); + const auto item = history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, TextWithEntities(), MTP_messageMediaContact( + MTP_string(phone), + MTP_string(firstName), + MTP_string(lastName), + MTP_string(), // vcard + MTP_long(userId.bare))); const auto media = MTP_inputMediaContact( MTP_string(phone), @@ -3737,21 +3747,19 @@ void ApiWrap::sendMessage(MessageToSend &&message) { mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } - const auto viaBotId = UserId(); - lastMessage = history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - action.replyTo, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - sending, - media, - HistoryMessageMarkupData()); + lastMessage = history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, sending, media); const auto done = [=]( const MTPUpdates &result, const MTP::Response &response) { @@ -3903,6 +3911,7 @@ void ApiWrap::sendInlineResult( sendFlags |= SendFlag::f_schedule_date; } if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; sendFlags |= SendFlag::f_quick_reply_shortcut; } if (action.options.hideViaBot) { @@ -3923,15 +3932,18 @@ void ApiWrap::sendInlineResult( _session->data().registerMessageRandomId(randomId, newId); - data->addToHistory( - history, - flags, - newId.msg, - messageFromId, - HistoryItem::NewMessageDate(action.options.scheduled), - (bot && !action.options.hideViaBot) ? peerToUser(bot->id) : 0, - action.replyTo, - messagePostAuthor); + data->addToHistory(history, { + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .viaBotId = ((bot && !action.options.hideViaBot) + ? peerToUser(bot->id) + : UserId()), + .postAuthor = messagePostAuthor, + }); history->clearCloudDraft(topicRootId); history->startSavingCloudDraft(topicRootId); diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 8406657e6..048362638 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -81,11 +81,11 @@ constexpr auto kMaxWallPaperSlugLength = 255; const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto item = history->makeMessage( - history->owner().nextLocalMessageId(), - flags, - base::unixtime::now(), - PreparedServiceText{ { text } }); + const auto item = history->makeMessage({ + .id = history->owner().nextLocalMessageId(), + .flags = flags, + .date = base::unixtime::now(), + }, PreparedServiceText{ { text } }); return AdminLog::OwnedItem(delegate, item); } @@ -96,24 +96,16 @@ constexpr auto kMaxWallPaperSlugLength = 255; bool out) { Expects(history->peer->isUser()); - const auto flags = MessageFlag::FakeHistoryItem - | MessageFlag::HasFromId - | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - base::unixtime::now(), - out ? history->session().userId() : peerToUser(history->peer->id), - QString(), - TextWithEntities{ text }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | (out ? MessageFlag::Outgoing : MessageFlag(0))), + .from = (out + ? history->session().userId() + : peerToUser(history->peer->id)), + .date = base::unixtime::now(), + }, TextWithEntities{ text }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 3f01fe269..2e24d2804 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -319,47 +319,36 @@ PreviewWrap::PreviewWrap( , _delegate(std::make_unique<PreviewDelegate>(box, _style.get(), [=] { update(); })) -, _replyToItem(_history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem +, _replyToItem(_history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::Post), - UserId(), // via - FullReplyTo(), - base::unixtime::now(), // date - _fake->id, - QString(), // postAuthor - TextWithEntities{ _peer->isSelf() - ? tr::lng_settings_color_reply(tr::now) - : tr::lng_settings_color_reply_channel(tr::now), - }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - uint64(0))) -, _replyItem(_history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + .from = _fake->id, + .date = base::unixtime::now(), +}, TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_reply(tr::now) + : tr::lng_settings_color_reply_channel(tr::now), +}, MTP_messageMediaEmpty())) +, _replyItem(_history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::HasReplyInfo | MessageFlag::Post), - UserId(), // via - FullReplyTo{ .messageId = _replyToItem->fullId() }, - base::unixtime::now(), // date - _fake->id, - QString(), // postAuthor - TextWithEntities{ _peer->isSelf() - ? tr::lng_settings_color_text(tr::now) - : tr::lng_settings_color_text_channel(tr::now), - }, - MTP_messageMediaWebPage( + .from = _fake->id, + .replyTo = FullReplyTo{.messageId = _replyToItem->fullId() }, + .date = base::unixtime::now(), +}, TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_text(tr::now) + : tr::lng_settings_color_text_channel(tr::now), +}, MTP_messageMediaWebPage( + MTP_flags(0), + MTP_webPagePending( MTP_flags(0), - MTP_webPagePending( - MTP_flags(0), - MTP_long(_webpage->id), - MTPstring(), - MTP_int(0))), - HistoryMessageMarkupData(), - uint64(0))) + MTP_long(_webpage->id), + MTPstring(), + MTP_int(0))))) , _element(_replyItem->createView(_delegate.get())) , _position(0, st::msgMargin.bottom()) { _style->apply(_theme.get()); diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 4de09e882..e33624403 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -77,20 +77,15 @@ AdminLog::OwnedItem GenerateItem( const QString &text) { Expects(history->peer->isUser()); - const auto item = history->addNewLocalMessage( - history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + const auto item = history->addNewLocalMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::HasReplyInfo), - UserId(), // via - FullReplyTo{ .messageId = replyTo }, - base::unixtime::now(), // date - from, - QString(), // postAuthor - TextWithEntities{ .text = text }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - uint64(0)); // groupedId + .from = from, + .replyTo = FullReplyTo{ .messageId = replyTo }, + .date = base::unixtime::now(), + }, TextWithEntities{ .text = text }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 1013c05bc..026e6a99e 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -27,13 +27,13 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); [[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) { Expects(IsServerMsgId(id)); - return ServerMaxMsgId + id + 1; + return ScheduledMaxMsgId + id + 1; } [[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) { Expects(IsShortcutMsgId(id)); - return (id - ServerMaxMsgId - 1); + return (id - ScheduledMaxMsgId - 1); } [[nodiscard]] bool TooEarlyForRequest(crl::time received) { @@ -145,6 +145,14 @@ int ShortcutMessages::count(BusinessShortcutId shortcutId) const { return (i != end(_data)) ? i->second.items.size() : 0; } +void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) { + +} + +void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) { + +} + void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { const auto &message = update.vmessage(); const auto shortcutId = BusinessShortcutIdFromMessage(message); diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h index 2028f2ece..57997e7e0 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.h +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -50,6 +50,8 @@ public: [[nodiscard]] int count(BusinessShortcutId shortcutId) const; [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; + void apply(const MTPDupdateQuickReplies &update); + void apply(const MTPDupdateNewQuickReply &update); void apply(const MTPDupdateQuickReplyMessage &update); void apply(const MTPDupdateDeleteQuickReplyMessages &update); void apply(const MTPDupdateDeleteQuickReply &update); diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index cd736e9e7..95c78092d 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -879,29 +879,20 @@ not_null<HistoryItem*> DownloadManager::generateItem( const auto session = document ? &document->session() : &photo->session(); - const auto fromId = previousItem - ? previousItem->from()->id - : session->userPeerId(); const auto history = previousItem ? previousItem->history() : session->data().history(session->user()); - const auto flags = MessageFlag::FakeHistoryItem; - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto date = base::unixtime::now(); + ; const auto caption = TextWithEntities(); const auto make = [&](const auto media) { - return history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - date, - fromId, - QString(), - media, - caption, - HistoryMessageMarkupData()); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem, + .from = (previousItem + ? previousItem->from()->id + : session->userPeerId()), + .date = base::unixtime::now(), + }, media, caption); }; const auto result = document ? make(document) : make(photo); _generated.emplace(result); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 0dde157ad..a43b2f680 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "api/api_text_entities.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -821,6 +822,7 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { remove.reserve(ids.size()); base::flat_map<not_null<History*>, QVector<MTPint>> idsByPeer; base::flat_map<not_null<PeerData*>, QVector<MTPint>> scheduledIdsByPeer; + base::flat_map<BusinessShortcutId, QVector<MTPint>> quickIdsByShortcut; for (const auto &itemId : ids) { if (const auto item = _owner->message(itemId)) { const auto history = item->history(); @@ -834,6 +836,16 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { _owner->scheduledMessages().removeSending(item); } continue; + } else if (item->isBusinessShortcut()) { + const auto wasOnServer = !item->isSending() + && !item->hasFailed(); + if (wasOnServer) { + quickIdsByShortcut[item->shortcutId()].push_back(MTP_int( + _owner->shortcutMessages().lookupId(item))); + } else { + _owner->shortcutMessages().removeSending(item); + } + continue; } remove.push_back(item); if (item->isRegular()) { @@ -853,6 +865,15 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { peer->session().api().applyUpdates(result); }).send(); } + for (const auto &[shortcutId, ids] : quickIdsByShortcut) { + const auto api = &_owner->session().api(); + api->request(MTPmessages_DeleteQuickReplyMessages( + MTP_int(shortcutId), + MTP_vector<MTPint>(ids) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + } for (const auto item : remove) { const auto history = item->history(); diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 733a0ae05..5c1700093 100644 --- a/Telegram/SourceFiles/data/data_replies_list.cpp +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -34,11 +34,11 @@ constexpr auto kMaxMessagesToDeleteMyTopic = 10; not_null<History*> history, TimeId date, const QString &text) { - return history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::FakeHistoryItem, - date, - PreparedServiceText{ { .text = text } }); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem, + .date = date, + }, PreparedServiceText{ { .text = text } }); } [[nodiscard]] bool IsCreating(not_null<History*> history, MsgId rootId) { diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index 556bee6ce..fb6f03308 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -80,7 +80,7 @@ bool SponsoredMessages::append(not_null<History*> history) { entryIt->itemFullId = FullMsgId( history->peer->id, _session->data().nextLocalMessageId()); - entryIt->item.reset(history->addNewLocalMessage( + entryIt->item.reset(history->addSponsoredMessage( entryIt->itemFullId.msg, entryIt->sponsored.from, entryIt->sponsored.textWithEntities)); diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index a35279be4..0090ba369 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -970,7 +970,7 @@ std::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) { } const auto history = _owner->history(story->peer()); auto result = std::shared_ptr<HistoryItem>( - history->makeMessage(story).get(), + history->makeMessage(StoryIdToMsgId(story->id()), story).get(), HistoryItem::Destroyer()); i->second = result; return result; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 176a13f74..ca11c8969 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -318,6 +318,8 @@ enum class MessageFlag : uint64 { Sponsored = (1ULL << 42), ReactionsAreTags = (1ULL << 43), + + ShortcutMessage = (1ULL << 44), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags<MessageFlag>; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index b71b0cd82..c54093dc3 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -801,13 +801,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); addPart( - history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id), - photo), + history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message), photo), 0, realId); }; @@ -826,23 +825,11 @@ void GenerateItems( }; const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { - const auto bodyFlags = MessageFlag::HasFromId - | MessageFlag::AdminLogEntry; - const auto bodyReplyTo = FullReplyTo(); - const auto bodyViaBotId = UserId(); - const auto bodyGroupedId = uint64(); - return history->makeMessage( - history->nextNonHistoryEntryId(), - bodyFlags, - bodyReplyTo, - bodyViaBotId, - date, - peerToUser(from->id), - QString(), - std::move(text), - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - bodyGroupedId); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, + .from = from->id, + }, std::move(text), MTP_messageMediaEmpty()); }; const auto addSimpleTextMessage = [&](TextWithEntities &&text) { @@ -1145,12 +1132,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(setLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1189,12 +1176,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(setLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1270,12 +1257,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(chatLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1366,12 +1353,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(link); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); }; const auto createParticipantMute = [&](const LogMute &data) { @@ -1441,13 +1428,12 @@ void GenerateItems( if (additional) { message.links.push_back(std::move(additional)); } - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id), - nullptr)); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); }; const auto createParticipantJoinByInvite = [&]( diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index f5a9765c0..499bf7cea 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_translation.h" #include "history/history_unread_things.h" #include "dialogs/ui/dialogs_layout.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -71,6 +72,12 @@ constexpr auto kSkipCloudDraftsFor = TimeId(2); using UpdateFlag = Data::HistoryUpdate::Flag; +[[nodiscard]] HistoryItemCommonFields WithLocalFlag( + HistoryItemCommonFields fields) { + fields.flags |= MessageFlag::Local; + return fields; +} + } // namespace History::History(not_null<Data::Session*> owner, PeerId peerId) @@ -446,17 +453,17 @@ std::vector<not_null<HistoryItem*>> History::createItems( not_null<HistoryItem*> History::addNewMessage( MsgId id, - const MTPMessage &msg, + const MTPMessage &message, MessageFlags localFlags, NewMessageType type) { - const auto detachExistingItem = (type == NewMessageType::Unread); - const auto item = createItem(id, msg, localFlags, detachExistingItem); + const auto detachExisting = (type == NewMessageType::Unread); + const auto item = createItem(id, message, localFlags, detachExisting); if (type == NewMessageType::Existing || item->mainView()) { return item; } const auto unread = (type == NewMessageType::Unread); if (unread && item->isHistoryEntry()) { - applyMessageChanges(item, msg); + applyMessageChanges(item, message); } return addNewItem(item, unread); } @@ -585,6 +592,9 @@ not_null<HistoryItem*> History::addNewItem( if (item->isScheduled()) { owner().scheduledMessages().appendSending(item); return item; + } else if (item->isBusinessShortcut()) { + owner().shortcutMessages().appendSending(item); + return item; } else if (!item->isHistoryEntry()) { return item; } @@ -635,139 +645,54 @@ void History::checkForLoadedAtTop(not_null<HistoryItem*> added) { } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &text, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId) { + const MTPMessageMedia &media) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - text, - media, - std::move(markup), - groupedId), + makeMessage(WithLocalFlag(std::move(fields)), text, media), true); } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> forwardOriginal, - MsgId topicRootId) { + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> forwardOriginal) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - date, - from, - postAuthor, - forwardOriginal, - topicRootId), + makeMessage(WithLocalFlag(std::move(fields)), forwardOriginal), true); } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + const TextWithEntities &caption) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - document, - caption, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), document, caption), true); } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + const TextWithEntities &caption) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - photo, - caption, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), photo, caption), true); } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup) { + HistoryItemCommonFields &&fields, + not_null<GameData*> game) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - game, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), game), true); } -not_null<HistoryItem*> History::addNewLocalMessage( +not_null<HistoryItem*> History::addSponsoredMessage( MsgId id, Data::SponsoredFrom from, const TextWithEntities &textWithEntities) { return addNewItem( - makeMessage( - id, - from, - textWithEntities, - nullptr), + makeMessage(id, from, textWithEntities, nullptr), true); } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index adeb63fc2..29620dc2e 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -20,6 +20,7 @@ class History; class HistoryBlock; class HistoryTranslation; class HistoryItem; +struct HistoryItemCommonFields; struct HistoryMessageMarkupData; class HistoryMainElementDelegateMixin; struct LanguageId; @@ -127,11 +128,23 @@ public: void applyGroupAdminChanges(const base::flat_set<UserId> &changes); template <typename ...Args> - not_null<HistoryItem*> makeMessage(Args &&...args) { + not_null<HistoryItem*> makeMessage(MsgId id, Args &&...args) { return static_cast<HistoryItem*>( insertItem( std::make_unique<HistoryItem>( this, + id, + std::forward<Args>(args)...)).get()); + } + template <typename ...Args> + not_null<HistoryItem*> makeMessage( + HistoryItemCommonFields &&fields, + Args &&...args) { + return static_cast<HistoryItem*>( + insertItem( + std::make_unique<HistoryItem>( + this, + std::move(fields), std::forward<Args>(args)...)).get()); } @@ -143,62 +156,30 @@ public: not_null<HistoryItem*> addNewMessage( MsgId id, - const MTPMessage &msg, + const MTPMessage &message, MessageFlags localFlags, NewMessageType type); + not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &text, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId = 0); + const MTPMessageMedia &media); not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> forwardOriginal, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> forwardOriginal); not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); - not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null<HistoryItem*> addNewLocalMessage( + HistoryItemCommonFields &&fields, + not_null<GameData*> game); + + not_null<HistoryItem*> addSponsoredMessage( MsgId id, Data::SponsoredFrom from, const TextWithEntities &textWithEntities); // sponsored diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index aa815ccc9..1f75a47d8 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -125,6 +125,14 @@ template <typename T> return false; } +[[nodiscard]] HistoryItemCommonFields ForwardedFields( + HistoryItemCommonFields fields, + not_null<History*> history, + not_null<HistoryItem*> original) { + fields.flags |= NewForwardedFlags(history->peer, fields.from, original); + return fields; +} + } // namespace void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) { @@ -347,12 +355,13 @@ HistoryItem::HistoryItem( MsgId id, const MTPDmessage &data, MessageFlags localFlags) -: HistoryItem( - history, - id, - FlagsFromMTP(id, data.vflags().v, localFlags), - data.vdate().v, - data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { +: HistoryItem(history, { + .id = id, + .flags = FlagsFromMTP(id, data.vflags().v, localFlags), + .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), + .date = data.vdate().v, + .shortcutId = data.vquick_reply_shortcut_id().value_or_empty(), +}) { _boostsApplied = data.vfrom_boosts_applied().value_or_empty(); const auto media = data.vmedia(); @@ -406,12 +415,12 @@ HistoryItem::HistoryItem( MsgId id, const MTPDmessageService &data, MessageFlags localFlags) -: HistoryItem( - history, - id, - FlagsFromMTP(id, data.vflags().v, localFlags), - data.vdate().v, - data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { +: HistoryItem(history, { + .id = id, + .flags = FlagsFromMTP(id, data.vflags().v, localFlags), + .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), + .date = data.vdate().v, +}) { if (data.vaction().type() != mtpc_messageActionPhoneCall) { createServiceFromMtp(data); } else { @@ -431,9 +440,7 @@ HistoryItem::HistoryItem( MessageFlags localFlags) : HistoryItem( history, - id, - localFlags, - TimeId(0), + { .id = id, .flags = localFlags }, PreparedServiceText{ tr::lng_message_empty( tr::now, Ui::Text::WithEntities) }) { @@ -441,13 +448,10 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, + HistoryItemCommonFields &&fields, PreparedServiceText &&message, - PeerId from, PhotoData *photo) -: HistoryItem(history, id, flags, date, from) { +: HistoryItem(history, fields) { setServiceText(std::move(message)); if (photo) { _media = std::make_unique<Data::MediaPhoto>( @@ -459,25 +463,16 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> original, - MsgId topicRootId) -: HistoryItem( - history, - id, - (NewForwardedFlags(history->peer, from, original) | flags), - date, - from) { + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> original) +: HistoryItem(history, ForwardedFields(fields, history, original)) { const auto peer = history->peer; auto config = CreateConfig(); const auto originalMedia = original->media(); const auto dropForwardInfo = original->computeDropForwardedInfo(); + const auto topicRootId = fields.replyTo.topicRootId; config.reply.messageId = config.reply.topMessageId = topicRootId; config.reply.topicPost = (topicRootId != 0) ? 1 : 0; if (const auto originalReply = original->Get<HistoryMessageReply>()) { @@ -520,8 +515,8 @@ HistoryItem::HistoryItem( ? original->author()->id : PeerId(); } - if (flags & MessageFlag::HasPostAuthor) { - config.postAuthor = postAuthor; + if (_flags & MessageFlag::HasPostAuthor) { + config.postAuthor = fields.postAuthor; } if (const auto fwdViaBot = original->viaBot()) { config.viaBotId = peerToUser(fwdViaBot->id); @@ -571,63 +566,28 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &textWithEntities, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const MTPMessageMedia &media) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); setMedia(media); setText(textWithEntities); - if (groupedId) { + if (fields.groupedId) { setGroupId(MessageGroupId::FromRaw( history->peer->id, - groupedId, - flags & MessageFlag::IsOrWasScheduled)); + fields.groupedId, + _flags & MessageFlag::IsOrWasScheduled)); } } HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const TextWithEntities &caption) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); const auto skipPremiumEffect = !history->session().premium(); const auto spoiler = false; @@ -642,28 +602,11 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const TextWithEntities &caption) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); const auto spoiler = false; _media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler); @@ -672,27 +615,10 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + HistoryItemCommonFields &&fields, + not_null<GameData*> game) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); _media = std::make_unique<Data::MediaGame>(this, game); setTextValue({}); @@ -704,18 +630,15 @@ HistoryItem::HistoryItem( Data::SponsoredFrom from, const TextWithEntities &textWithEntities, HistoryItem *injectedAfter) -: HistoryItem( - history, - id, - ((history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0)) - //| (from.peer ? MessageFlag::HasFromId : MessageFlag(0)) - | MessageFlag::Local), - HistoryItem::NewMessageDate(injectedAfter - ? injectedAfter->date() - : 0), - /*from.peer ? from.peer->id : */PeerId(0)) { - _flags |= MessageFlag::Sponsored; - +: HistoryItem(history, { + .id = id, + .flags = (MessageFlag::Local + | MessageFlag::Sponsored + | (history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0))), + .date = HistoryItem::NewMessageDate(injectedAfter + ? injectedAfter->date() + : 0), +}) { const auto webPageType = !from.externalLink.isEmpty() ? WebPageType::None : from.isExactPost @@ -758,15 +681,15 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from) -: id(id) + const HistoryItemCommonFields &fields) +: id(fields.id) , _history(history) -, _from(from ? history->owner().peer(from) : history->peer) -, _flags(FinalizeMessageFlags(history, flags)) -, _date(date) { +, _from((fields.flags & MessageFlag::HasFromId && fields.from) + ? history->owner().peer(fields.from) + : history->peer) +, _flags(FinalizeMessageFlags(history, fields.flags)) +, _date(fields.date) +, _shortcutId(fields.shortcutId) { if (isHistoryEntry() && IsClientMsgId(id)) { _history->registerClientSideMessage(this); } @@ -774,15 +697,18 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, + MsgId id, not_null<Data::Story*> story) -: id(StoryIdToMsgId(story->id())) -, _history(history) -, _from(history->peer) -, _flags(MessageFlag::Local - | MessageFlag::Outgoing - | MessageFlag::FakeHistoryItem - | MessageFlag::StoryItem) -, _date(story->date()) { +: HistoryItem(history, { + .id = id, + .flags = (MessageFlag::Local + | MessageFlag::Outgoing + | MessageFlag::HasFromId + | MessageFlag::FakeHistoryItem + | MessageFlag::StoryItem), + .from = history->peer->id, + .date = story->date(), +}) { setStoryFields(story); } @@ -807,6 +733,11 @@ TimeId HistoryItem::NewMessageDate(TimeId scheduled) { return scheduled ? scheduled : base::unixtime::now(); } +TimeId HistoryItem::NewMessageDate( + const Api::SendOptions &options) { + return options.shortcutId ? TimeId() : NewMessageDate(options.scheduled); +} + HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { if (const auto pinned = Get<HistoryServicePinned>()) { return pinned; @@ -1603,6 +1534,14 @@ bool HistoryItem::isUserpicSuggestion() const { return (_flags & MessageFlag::IsUserpicSuggestion); } +BusinessShortcutId HistoryItem::shortcutId() const { + return _shortcutId; +} + +bool HistoryItem::isBusinessShortcut() const { + return _shortcutId != 0; +} + void HistoryItem::destroy() { _history->destroyMessage(this); } @@ -3520,15 +3459,11 @@ TextWithEntities HistoryItem::withLocalEntities( return textWithEntities; } -void HistoryItem::createComponentsHelper( - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) { +void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { + const auto &replyTo = fields.replyTo; auto config = CreateConfig(); - config.viaBotId = viaBotId; - if (flags & MessageFlag::HasReplyInfo) { + config.viaBotId = fields.viaBotId; + if (fields.flags & MessageFlag::HasReplyInfo) { config.reply.messageId = replyTo.messageId.msg; config.reply.storyId = replyTo.storyId.story; config.reply.externalPeerId = replyTo.storyId @@ -3567,9 +3502,13 @@ void HistoryItem::createComponentsHelper( config.reply.quoteOffset = replyTo.quoteOffset; config.reply.quote = std::move(replyTo.quote); } - config.markup = std::move(markup); - if (flags & MessageFlag::HasPostAuthor) config.postAuthor = postAuthor; - if (flags & MessageFlag::HasViews) config.viewsCount = 1; + config.markup = std::move(fields.markup); + if (fields.flags & MessageFlag::HasPostAuthor) { + config.postAuthor = fields.postAuthor; + } + if (fields.flags & MessageFlag::HasViews) { + config.viewsCount = 1; + } createComponents(std::move(config)); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index febc8d67c..f287bb039 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HiddenSenderInfo; class History; + struct HistoryMessageReply; struct HistoryMessageViews; struct HistoryMessageMarkupData; @@ -28,6 +29,10 @@ struct PreparedServiceText; class ReplyKeyboard; struct LanguageId; +namespace Api { +struct SendOptions; +} // namespace Api + namespace base { template <typename Enum> class enum_mask; @@ -86,6 +91,19 @@ class Service; class ServiceMessagePainter; } // namespace HistoryView +struct HistoryItemCommonFields { + MsgId id = 0; + MessageFlags flags = 0; + PeerId from = 0; + FullReplyTo replyTo; + TimeId date = 0; + BusinessShortcutId shortcutId = 0; + UserId viaBotId = 0; + QString postAuthor; + uint64 groupedId = 0; + HistoryMessageMarkupData markup; +}; + class HistoryItem final : public RuntimeComposer<HistoryItem> { public: [[nodiscard]] static std::unique_ptr<Data::Media> CreateMedia( @@ -114,73 +132,39 @@ public: Data::SponsoredFrom from, const TextWithEntities &textWithEntities, HistoryItem *injectedAfter); + HistoryItem( // Story wrap. + not_null<History*> history, + MsgId id, + not_null<Data::Story*> story); HistoryItem( // Local message. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &textWithEntities, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId); + const MTPMessageMedia &media); HistoryItem( // Local service message. not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, + HistoryItemCommonFields &&fields, PreparedServiceText &&message, - PeerId from = 0, PhotoData *photo = nullptr); HistoryItem( // Local forwarded. not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> original, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> original); HistoryItem( // Local photo. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local document. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local game. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup); - HistoryItem(not_null<History*> history, not_null<Data::Story*> story); + HistoryItemCommonFields &&fields, + not_null<GameData*> game); ~HistoryItem(); struct Destroyer { @@ -210,12 +194,8 @@ public: [[nodiscard]] bool isSponsored() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; - [[nodiscard]] BusinessShortcutId shortcutId() const { - return _shortcutId; - } - [[nodiscard]] bool isBusinessShortcut() const { - return _shortcutId != 0; - } + [[nodiscard]] BusinessShortcutId shortcutId() const; + [[nodiscard]] bool isBusinessShortcut() const; void addLogEntryOriginal( WebPageId localId, @@ -473,6 +453,8 @@ public: [[nodiscard]] TimeId date() const; [[nodiscard]] static TimeId NewMessageDate(TimeId scheduled); + [[nodiscard]] static TimeId NewMessageDate( + const Api::SendOptions &options); [[nodiscard]] Data::Media *media() const { return _media.get(); @@ -554,17 +536,9 @@ private: HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from); + const HistoryItemCommonFields &fields); - void createComponentsHelper( - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - const QString &postAuthor, - HistoryMessageMarkupData &&markup); + void createComponentsHelper(HistoryItemCommonFields &&fields); void createComponents(CreateConfig &&config); void setupForwardedComponent(const CreateConfig &config); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index ae6d3dc3a..ee5d1d21f 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -301,6 +301,7 @@ ReplyFields ReplyFieldsFromMTP( if (const auto id = data.vreply_to_msg_id().value_or_empty()) { result.messageId = data.is_reply_to_scheduled() ? owner->scheduledMessages().localMessageId(id) + AssertIsDebug() : id; result.topMessageId = data.vreply_to_top_id().value_or(id); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 752560a7f..65153bf86 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -386,6 +386,9 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag()) | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag()) | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag()) + | ((flags & MTP::f_quick_reply_shortcut_id) + ? Flag::ShortcutMessage + : Flag()) | ((flags & MTP::f_from_scheduled) ? Flag::IsOrWasScheduled : Flag()) @@ -598,11 +601,11 @@ not_null<HistoryItem*> GenerateJoinedMessage( TimeId inviteDate, not_null<UserData*> inviter, bool viaRequest) { - return history->makeMessage( - history->owner().nextLocalMessageId(), - MessageFlag::Local | MessageFlag::ShowSimilarChannels, - inviteDate, - GenerateJoinedText(history, inviter, viaRequest)); + return history->makeMessage({ + .id = history->owner().nextLocalMessageId(), + .flags = MessageFlag::Local | MessageFlag::ShowSimilarChannels, + .date = inviteDate, + }, GenerateJoinedText(history, inviter, viaRequest)); } std::optional<bool> PeerHasThisCall( @@ -657,6 +660,7 @@ std::optional<bool> PeerHasThisCall( MessageFlags flags) { if (!(flags & MessageFlag::FakeHistoryItem) && !(flags & MessageFlag::IsOrWasScheduled) + && !(flags & MessageFlag::ShortcutMessage) && !(flags & MessageFlag::AdminLogEntry)) { flags |= MessageFlag::HistoryEntry; if (history->peer->isSelf()) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 5ba81cd1e..a0d3d6066 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -257,38 +257,32 @@ rpl::producer<QString> PreviewWrap::showLinkSelector( was->destroy(); } using Flag = MTPDmessageMediaWebPage::Flag; - _draftItem = _history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + _draftItem = _history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::Outgoing | MessageFlag::HasFromId | (webpage.invert ? MessageFlag::InvertMedia : MessageFlag())), - UserId(), // via - FullReplyTo(), - base::unixtime::now(), // date - _history->session().userPeerId(), - QString(), // postAuthor - HighlightParsedLinks({ - message.text, - TextUtilities::ConvertTextTagsToEntities(message.tags), - }, links), - MTP_messageMediaWebPage( - MTP_flags(Flag() - | (webpage.forceLargeMedia - ? Flag::f_force_large_media - : Flag()) - | (webpage.forceSmallMedia - ? Flag::f_force_small_media - : Flag())), - MTP_webPagePending( - MTP_flags(webpage.url.isEmpty() - ? MTPDwebPagePending::Flag() - : MTPDwebPagePending::Flag::f_url), - MTP_long(webpage.id), - MTP_string(webpage.url), - MTP_int(0))), - HistoryMessageMarkupData(), - uint64(0)); // groupedId + .from = _history->session().userPeerId(), + .date = base::unixtime::now(), + }, HighlightParsedLinks({ + message.text, + TextUtilities::ConvertTextTagsToEntities(message.tags), + }, links), MTP_messageMediaWebPage( + MTP_flags(Flag() + | (webpage.forceLargeMedia + ? Flag::f_force_large_media + : Flag()) + | (webpage.forceSmallMedia + ? Flag::f_force_small_media + : Flag())), + MTP_webPagePending( + MTP_flags(webpage.url.isEmpty() + ? MTPDwebPagePending::Flag() + : MTPDwebPagePending::Flag::f_url), + MTP_long(webpage.id), + MTP_string(webpage.url), + MTP_int(0)))); _element = _draftItem->createView(_delegate.get()); _selectType = TextSelectType::Letters; _symbol = _selectionStartSymbol = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 51ae055f3..c017d8842 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -188,54 +188,39 @@ bool AboutView::refresh() { } AdminLog::OwnedItem AboutView::makeAboutBot(not_null<BotInfo*> info) { - const auto flags = MessageFlag::FakeAboutView - | MessageFlag::FakeHistoryItem - | MessageFlag::Local; - const auto postAuthor = QString(); - const auto date = TimeId(0); - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(0); - const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( info->description, Ui::ItemTextBotNoMonoOptions().flags); - const auto make = [&](auto &&a, auto &&b, auto &&...other) { - return _history->makeMessage( - _history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - date, - _history->peer->id, - postAuthor, - std::forward<decltype(a)>(a), - std::forward<decltype(b)>(b), - HistoryMessageMarkupData(), - std::forward<decltype(other)>(other)...); + const auto make = [&](auto &&...args) { + return _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, std::forward<decltype(args)>(args)...); }; const auto item = info->document ? make(info->document, textWithEntities) : info->photo ? make(info->photo, textWithEntities) - : make(textWithEntities, MTP_messageMediaEmpty(), groupedId); + : make(textWithEntities, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(_delegate, item); } AdminLog::OwnedItem AboutView::makePremiumRequired() { - const auto flags = MessageFlag::FakeAboutView - | MessageFlag::FakeHistoryItem - | MessageFlag::Local; - const auto date = TimeId(0); - const auto item = _history->makeMessage( - _history->nextNonHistoryEntryId(), - flags, - date, - PreparedServiceText{ tr::lng_send_non_premium_text( - tr::now, - lt_user, - Ui::Text::Bold(_history->peer->shortName()), - Ui::Text::RichLangValue) }, - peerToUser(_history->peer->id)); + const auto item = _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, PreparedServiceText{ tr::lng_send_non_premium_text( + tr::now, + lt_user, + Ui::Text::Bold(_history->peer->shortName()), + Ui::Text::RichLangValue), + }); auto result = AdminLog::OwnedItem(_delegate, item); result->overrideMedia(std::make_unique<ServiceBox>( result.get(), diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 0f15a0fef..5a6ea07d6 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -59,6 +59,7 @@ enum class Context : char { ContactPreview, SavedSublist, TTLViewer, + ShortcutMessages, }; enum class OnlyEmojiAndSpaces : char { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 393284d32..3c09635a1 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1792,7 +1792,7 @@ void ListWidget::updateItemsGeometry() { if (view->isHidden()) { view->setDisplayDate(false); } else { - view->setDisplayDate(true); + view->setDisplayDate(_context != Context::ShortcutMessages); view->setAttachToPrevious(false); return i; } @@ -2048,7 +2048,8 @@ void ListWidget::checkActivation() { } void ListWidget::paintEvent(QPaintEvent *e) { - if (_controller->contentOverlapped(this, e)) { + if ((_context != Context::ShortcutMessages) + && _controller->contentOverlapped(this, e)) { return; } @@ -3737,7 +3738,8 @@ void ListWidget::refreshAttachmentsFromTill(int from, int till) { } else { const auto viewDate = view->dateTime(); const auto nextDate = next->dateTime(); - next->setDisplayDate(nextDate.date() != viewDate.date()); + next->setDisplayDate(_context != Context::ShortcutMessages + && nextDate.date() != viewDate.date()); auto attached = next->computeIsAttachToPrevious(view); next->setAttachToPrevious(attached, view); view->setAttachToNext(attached, next); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index ebba2f578..2e2fdbae7 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -631,11 +631,11 @@ private: const not_null<ListDelegate*> _delegate; const not_null<Window::SessionController*> _controller; const std::unique_ptr<EmojiInteractions> _emojiInteractions; + const Context _context; Data::MessagePosition _aroundPosition; Data::MessagePosition _shownAtPosition; Data::MessagePosition _initialAroundPosition; - Context _context; int _aroundIndex = -1; int _idsLimit = kMinimalIdsLimit; Data::MessagesSlice _slice; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 4eafd53e5..f08ed8414 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2061,6 +2061,7 @@ bool Message::hasFromPhoto() const { return !item->out() && !item->history()->peer->isUser(); } break; case Context::ContactPreview: + case Context::ShortcutMessages: return false; } Unexpected("Context in Message::hasFromPhoto."); @@ -3268,6 +3269,7 @@ bool Message::hasFromName() const { return false; } break; case Context::ContactPreview: + case Context::ShortcutMessages: return false; } Unexpected("Context in Message::hasFromName."); @@ -3306,6 +3308,9 @@ bool Message::hasOutLayout() const { const auto item = data(); if (item->history()->peer->isSelf()) { if (const auto forwarded = item->Get<HistoryMessageForwarded>()) { + if (context() == Context::ShortcutMessages) { + return true; + } return (context() == Context::SavedSublist) && (!forwarded->forwardOfForward() ? (forwarded->originalSender diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 339b40eaf..5931c22c4 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_document_media.h" #include "history/history.h" +#include "history/history_item.h" #include "history/history_item_reply_markup.h" #include "inline_bots/inline_bot_layout_item.h" #include "inline_bots/inline_bot_send_data.h" @@ -376,30 +377,15 @@ bool Result::hasThumbDisplay() const { void Result::addToHistory( not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor) const { - flags |= MessageFlag::FromInlineBot; - - auto markup = _replyMarkup ? *_replyMarkup : HistoryMessageMarkupData(); - if (!markup.isNull()) { - flags |= MessageFlag::HasReplyMarkup; + HistoryItemCommonFields &&fields) const { + fields.flags |= MessageFlag::FromInlineBot; + if (_replyMarkup) { + fields.markup = *_replyMarkup; + if (!fields.markup.isNull()) { + fields.flags |= MessageFlag::HasReplyMarkup; + } } - sendData->addToHistory( - this, - history, - flags, - msgId, - fromId, - date, - viaBotId, - replyTo, - postAuthor, - std::move(markup)); + sendData->addToHistory(this, history, std::move(fields)); } QString Result::getErrorOnSend(not_null<History*> history) const { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index 296deb613..cc7e090ee 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -16,6 +16,7 @@ class FileLoader; class History; class UserData; struct HistoryMessageMarkupData; +struct HistoryItemCommonFields; namespace Data { class LocationPoint; @@ -64,13 +65,7 @@ public: void addToHistory( not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor) const; + HistoryItemCommonFields &&fields) const; QString getErrorOnSend(not_null<History*> history) const; // interface for Layout:: usage diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index a8135979b..0b8e9d8b7 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -31,29 +31,15 @@ QString SendData::getLayoutDescription(const Result *owner) const { void SendDataCommon::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { - auto fields = getSentMessageFields(); - if (replyTo) { - flags |= MessageFlag::HasReplyInfo; + HistoryItemCommonFields &&fields) const { + auto distinct = getSentMessageFields(); + if (fields.replyTo) { + fields.flags |= MessageFlag::HasReplyInfo; } history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, - std::move(fields.text), - std::move(fields.media), - std::move(markup)); + std::move(fields), + std::move(distinct.text), + std::move(distinct.media)); } QString SendDataCommon::getErrorOnSend( @@ -113,25 +99,11 @@ QString SendContact::getLayoutDescription(const Result *owner) const { void SendPhoto::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { + HistoryItemCommonFields &&fields) const { history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, + std::move(fields), _photo, - { _message, _entities }, - std::move(markup)); + { _message, _entities }); } QString SendPhoto::getErrorOnSend( @@ -144,25 +116,11 @@ QString SendPhoto::getErrorOnSend( void SendFile::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { + HistoryItemCommonFields &&fields) const { history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, + std::move(fields), _document, - { _message, _entities }, - std::move(markup)); + { _message, _entities }); } QString SendFile::getErrorOnSend( @@ -175,24 +133,8 @@ QString SendFile::getErrorOnSend( void SendGame::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { - history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, - _game, - std::move(markup)); + HistoryItemCommonFields &&fields) const { + history->addNewLocalMessage(std::move(fields), _game); } QString SendGame::getErrorOnSend( diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index 84c25624f..502cc006e 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_location_manager.h" -struct HistoryMessageMarkupData; +struct HistoryItemCommonFields; namespace Main { class Session; @@ -43,14 +43,7 @@ public: virtual void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const = 0; + HistoryItemCommonFields &&fields) const = 0; virtual QString getErrorOnSend( const Result *owner, not_null<History*> history) const = 0; @@ -85,14 +78,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -253,14 +239,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -294,14 +273,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -329,14 +301,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index f401f6b37..da07a1516 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -131,23 +131,12 @@ private: not_null<History*> history) { Expects(history->peer->isUser()); - const auto flags = MessageFlag::FakeHistoryItem - | MessageFlag::HasFromId; - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - base::unixtime::now(), - peerToUser(history->peer->id), - QString(), - TextWithEntities(), - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId, + .from = history->peer->id, + .date = base::unixtime::now(), + }, TextWithEntities(), MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 290aa222c..8a1053da5 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_selector.h" #include "core/file_utilities.h" #include "core/mime_type.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_message_reaction_id.h" #include "data/data_premium_limits.h" #include "data/data_session.h" @@ -75,6 +76,8 @@ public: } [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] rpl::producer<> sectionShowBack() override; + void setInnerFocus() override; bool paintOuter( not_null<QWidget*> outer, @@ -82,6 +85,8 @@ public: QRect clip) override; private: + void outerResized(QSize outer); + // ListDelegate interface. Context listContext() override; bool listScrollTo(int top, bool syntetic = true) override; @@ -154,7 +159,11 @@ private: QPointer<Ui::RpWidget> createPinnedToBottom( not_null<Ui::RpWidget*> parent) override; void setupComposeControls(); + void processScroll(); + void updateInnerVisibleArea(); + void pushReplyReturn(not_null<HistoryItem*> item); + void checkReplyReturns(); void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( @@ -234,6 +243,7 @@ private: QPointer<ListWidget> _inner; std::unique_ptr<Ui::RpWidget> _controlsWrap; std::unique_ptr<ComposeControls> _composeControls; + rpl::event_stream<> _showBackRequests; bool _skipScrollEvent = false; std::unique_ptr<StickerToast> _stickerToast; @@ -294,10 +304,17 @@ ShortcutMessages::ShortcutMessages( this, controller, static_cast<ListDelegate*>(this)); - //_scroll->scrolls( - //) | rpl::start_with_next([=] { - // onScroll(); - //}, lifetime()); + + _scroll->sizeValue() | rpl::filter([](QSize size) { + return !size.isEmpty(); + }) | rpl::start_with_next([=](QSize size) { + outerResized(size); + }, lifetime()); + + _scroll->scrolls( + ) | rpl::start_with_next([=] { + processScroll(); + }, lifetime()); _inner->editMessageRequested( ) | rpl::start_with_next([=](auto fullId) { @@ -312,10 +329,10 @@ ShortcutMessages::ShortcutMessages( { auto emptyInfo = base::make_unique_q<EmptyListBubbleWidget>( _inner, - controller->chatStyle(), + _style.get(), st::msgServicePadding); const auto emptyText = Ui::Text::Semibold( - tr::lng_scheduled_messages_empty(tr::now)); + u"give me your money.."_q); emptyInfo->setText(emptyText); _inner->setEmptyInfoWidget(std::move(emptyInfo)); } @@ -335,6 +352,31 @@ rpl::producer<QString> ShortcutMessages::title() { return rpl::single(u"Editing messages list"_q); } +void ShortcutMessages::processScroll() { + if (_skipScrollEvent) { + return; + } + updateInnerVisibleArea(); +} + +void ShortcutMessages::updateInnerVisibleArea() { + if (!_inner->animatedScrolling()) { + checkReplyReturns(); + } + const auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); +} + +rpl::producer<> ShortcutMessages::sectionShowBack() { + return _showBackRequests.events(); +} + +void ShortcutMessages::setInnerFocus() { + _composeControls->focus(); +} + bool ShortcutMessages::paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, @@ -349,6 +391,29 @@ bool ShortcutMessages::paintOuter( return true; } +void ShortcutMessages::outerResized(QSize outer) { + const auto contentWidth = outer.width(); + + const auto newScrollTop = _scroll->isHidden() + ? std::nullopt + : _scroll->scrollTop() + ? base::make_optional(_scroll->scrollTop()) + : 0; + _skipScrollEvent = true; + _inner->resizeToWidth(contentWidth, _scroll->height()); + resize(width(), _inner->height()); + _skipScrollEvent = false; + + if (!_scroll->isHidden()) { + if (newScrollTop) { + _scroll->scrollToY(*newScrollTop); + } + updateInnerVisibleArea(); + } + _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); + _cornerButtons.updatePositions(); +} + void ShortcutMessages::setupComposeControls() { _composeControls->setHistory({ .history = _history.get(), @@ -446,10 +511,10 @@ void ShortcutMessages::setupComposeControls() { if (action == Ui::InputField::MimeAction::Check) { return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { - //return confirmSendingFiles( - // data, - // std::nullopt, - // Core::ReadMimeText(data));#TODO + return confirmSendingFiles( + data, + std::nullopt, + Core::ReadMimeText(data)); } Unexpected("action in MimeData hook."); }); @@ -480,22 +545,24 @@ QPointer<Ui::RpWidget> ShortcutMessages::createPinnedToBottom( _controlsWrap.get(), _controller, [=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); }, - ComposeControls::Mode::Scheduled, + ComposeControls::Mode::Normal, SendMenu::Type::Disabled); setupComposeControls(); + showAtEnd(); + return _controlsWrap.get(); } Context ShortcutMessages::listContext() { - return Context::History; + return Context::ShortcutMessages; } bool ShortcutMessages::listScrollTo(int top, bool syntetic) { top = std::clamp(top, 0, _scroll->scrollTopMax()); if (_scroll->scrollTop() == top) { - //updateInnerVisibleArea(); + updateInnerVisibleArea(); return false; } _scroll->scrollToY(top); @@ -509,7 +576,7 @@ void ShortcutMessages::listCancelRequest() { } else if (_composeControls->handleCancelRequest()) { return; } - _controller->showBackFromStack(); + _showBackRequests.fire({}); } void ShortcutMessages::listDeleteRequest() { @@ -525,13 +592,11 @@ rpl::producer<Data::MessagesSlice> ShortcutMessages::listSource( int limitBefore, int limitAfter) { const auto data = &_controller->session().data(); - //return rpl::single(rpl::empty) | rpl::then( - // data->scheduledMessages().updates(_history) - //) | rpl::map([=] { - // return data->scheduledMessages().list(_history); - //}) | rpl::after_next([=](const Data::MessagesSlice &slice) { - // highlightSingleNewMessage(slice); - //}); + return rpl::single(rpl::empty) | rpl::then( + data->shortcutMessages().updates(_shortcutId) + ) | rpl::map([=] { + return data->shortcutMessages().list(_shortcutId); + }); return rpl::never<Data::MessagesSlice>(); } @@ -698,12 +763,12 @@ std::optional<bool> ShortcutMessages::cornerButtonsDownShown() { || _composeControls->isTTLButtonShown()) { return false; } - //const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; - //if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { - // return true; - //} else if (_inner->loadedAtBottomKnown()) { - // return !_inner->loadedAtBottom(); - //} + const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { + return true; + } else if (_inner->loadedAtBottomKnown()) { + return !_inner->loadedAtBottom(); + } return std::nullopt; } @@ -717,10 +782,31 @@ bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { return (type == CornerButtonType::Down); } +void ShortcutMessages::pushReplyReturn(not_null<HistoryItem*> item) { + if (item->shortcutId() == _shortcutId) { + _cornerButtons.pushReplyReturn(item); + } +} + +void ShortcutMessages::checkReplyReturns() { + const auto currentTop = _scroll->scrollTop(); + while (const auto replyReturn = _cornerButtons.replyReturn()) { + const auto position = replyReturn->position(); + const auto scrollTop = _inner->scrollTopForPosition(position); + const auto below = scrollTop + ? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax())) + : _inner->isBelowPosition(position); + if (below) { + _cornerButtons.calculateNextReplyReturn(); + } else { + break; + } + } +} + void ShortcutMessages::uploadFile( const QByteArray &fileContent, SendMediaType type) { - // #TODO replies schedule _session->api().sendFile(fileContent, type, prepareSendAction({})); } @@ -773,11 +859,6 @@ void ShortcutMessages::send() { return; } send({}); - // #TODO replies schedule - //const auto callback = [=](Api::SendOptions options) { send(options); }; - //Ui::show( - // PrepareScheduleBox(this, sendMenuType(), callback), - // Ui::LayerOption::KeepOther); } void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { @@ -933,7 +1014,7 @@ bool ShortcutMessages::confirmSendingFiles( _composeControls->getTextWithAppliedMarkdown(), _history->peer, Api::SendType::Normal, - SendMenu::Type::SilentOnly); // #TODO replies schedule + SendMenu::Type::Disabled); box->setConfirmedCallback(crl::guard(this, [=]( Ui::PreparedList &&list, diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 75d42760d..45ad83edd 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -273,24 +273,14 @@ AdminLog::OwnedItem GenerateCommentItem( if (data.comment.isEmpty()) { return nullptr; } - const auto flags = MessageFlag::HasFromId - | MessageFlag::Outgoing - | MessageFlag::FakeHistoryItem; - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - base::unixtime::now(), - history->session().userId(), - QString(), - TextWithEntities{ data.comment }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::HasFromId + | MessageFlag::Outgoing + | MessageFlag::FakeHistoryItem), + .from = history->session().userPeerId(), + .date = base::unixtime::now(), + }, TextWithEntities{ data.comment }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } @@ -298,29 +288,19 @@ AdminLog::OwnedItem GenerateContactItem( not_null<HistoryView::ElementDelegate*> delegate, not_null<History*> history, const Contact &data) { - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto postAuthor = QString(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - (MessageFlag::HasFromId + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::HasFromId | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem), - replyTo, - viaBotId, - base::unixtime::now(), - history->session().userPeerId(), - postAuthor, - TextWithEntities(), - MTP_messageMediaContact( - MTP_string(data.phone), - MTP_string(data.firstName), - MTP_string(data.lastName), - MTP_string(), // vcard - MTP_long(0)), // user_id - HistoryMessageMarkupData(), - groupedId); + .from = history->session().userPeerId(), + .date = base::unixtime::now(), + }, TextWithEntities(), MTP_messageMediaContact( + MTP_string(data.phone), + MTP_string(data.firstName), + MTP_string(data.lastName), + MTP_string(), // vcard + MTP_long(0))); // user_id return AdminLog::OwnedItem(delegate, item); } From 6e08b00dba33d55c8069177f18058f6b90e9eeb4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 Feb 2024 13:11:16 +0400 Subject: [PATCH 054/108] Fix sending .tgs files as stickers. Regression was introduced in 3467fe226f. --- Telegram/SourceFiles/storage/localimageloader.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 8044e266c..532a1d35e 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -901,11 +901,11 @@ void FileLoadTask::process(Args &&args) { attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h))); if (ValidateThumbDimensions(w, h)) { - isSticker = (_type == SendMediaType::File) - && Core::IsMimeSticker(filemime) + isSticker = Core::IsMimeSticker(filemime) && (filesize < Storage::kMaxStickerBytesSize) && (Core::IsMimeStickerAnimated(filemime) - || GoodStickerDimensions(w, h)); + || (_type == SendMediaType::File + && GoodStickerDimensions(w, h))); if (isSticker) { attributes.push_back(MTP_documentAttributeSticker( MTP_flags(0), From fb539b0f70046e6fcf1f71212ef56cbfc8196348 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 Feb 2024 13:58:46 +0400 Subject: [PATCH 055/108] Improve shortcut messages geometry. --- Telegram/SourceFiles/history/history_item.cpp | 2 +- Telegram/SourceFiles/history/history_item_components.cpp | 6 ++++-- Telegram/SourceFiles/history/view/history_view_element.cpp | 2 +- .../settings/business/settings_shortcut_messages.cpp | 7 +++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 1f75a47d8..38f236c4b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -735,7 +735,7 @@ TimeId HistoryItem::NewMessageDate(TimeId scheduled) { TimeId HistoryItem::NewMessageDate( const Api::SendOptions &options) { - return options.shortcutId ? TimeId() : NewMessageDate(options.scheduled); + return options.shortcutId ? 1 : NewMessageDate(options.scheduled); } HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index ee5d1d21f..f2ebc57c5 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" +#include "data/business/data_shortcut_messages.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_media_types.h" @@ -301,10 +302,11 @@ ReplyFields ReplyFieldsFromMTP( if (const auto id = data.vreply_to_msg_id().value_or_empty()) { result.messageId = data.is_reply_to_scheduled() ? owner->scheduledMessages().localMessageId(id) - AssertIsDebug() + : item->shortcutId() + ? owner->shortcutMessages().localMessageId(id) : id; result.topMessageId - = data.vreply_to_top_id().value_or(id); + = data.vreply_to_top_id().value_or(result.messageId.bare); result.topicPost = data.is_forum_topic() ? 1 : 0; } if (const auto header = data.vreply_from()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index bac85515a..3e142d0c9 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -462,7 +462,7 @@ Element::Element( Flag serviceFlag) : _delegate(delegate) , _data(data) -, _dateTime(IsItemScheduledUntilOnline(data) +, _dateTime((IsItemScheduledUntilOnline(data) || data->shortcutId()) ? QDateTime() : ItemDateTime(data)) , _text(st::msgMinWidth) diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 8a1053da5..06ea5ac7e 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -337,8 +337,8 @@ ShortcutMessages::ShortcutMessages( _inner->setEmptyInfoWidget(std::move(emptyInfo)); } - widthValue() | rpl::start_with_next([=](int width) { - resize(width, width); + _inner->heightValue() | rpl::start_with_next([=](int height) { + resize(width(), height); }, lifetime()); } @@ -401,7 +401,6 @@ void ShortcutMessages::outerResized(QSize outer) { : 0; _skipScrollEvent = true; _inner->resizeToWidth(contentWidth, _scroll->height()); - resize(width(), _inner->height()); _skipScrollEvent = false; if (!_scroll->isHidden()) { @@ -743,7 +742,7 @@ void ShortcutMessages::listAddTranslatedItems( void ShortcutMessages::cornerButtonsShowAtPosition( Data::MessagePosition position) { - //showAtPosition(position); + showAtPosition(position); } Data::Thread *ShortcutMessages::cornerButtonsThread() { From 23e22de6ec7d3478d23d729f3c26210ef5d0821c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 Feb 2024 14:20:30 +0400 Subject: [PATCH 056/108] Fix deleting shortcut items. --- Telegram/SourceFiles/history/history_item.cpp | 7 ++++++- .../settings/business/settings_shortcut_messages.cpp | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 38f236c4b..84986c28a 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2037,6 +2037,9 @@ void HistoryItem::setRealId(MsgId newId) { const auto oldId = std::exchange(id, newId); _flags &= ~(MessageFlag::BeingSent | MessageFlag::Local); + if (isBusinessShortcut()) { + _date = 0; + } if (isRegular()) { _history->unregisterClientSideMessage(this); } @@ -2154,7 +2157,9 @@ bool HistoryItem::canDelete() const { return false; } else if (topicRootId() == id) { return false; - } else if (!isHistoryEntry() && !isScheduled()) { + } else if (!isHistoryEntry() + && !isScheduled() + && !isBusinessShortcut()) { return false; } auto channel = _history->peer->asChannel(); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 06ea5ac7e..df5f778ba 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -164,6 +164,8 @@ private: void pushReplyReturn(not_null<HistoryItem*> item); void checkReplyReturns(); + void confirmDeleteSelected(); + void clearSelected(); void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( @@ -579,7 +581,7 @@ void ShortcutMessages::listCancelRequest() { } void ShortcutMessages::listDeleteRequest() { - //confirmDeleteSelected(); + confirmDeleteSelected(); } void ShortcutMessages::listTryProcessKeyInput(not_null<QKeyEvent*> e) { @@ -803,6 +805,14 @@ void ShortcutMessages::checkReplyReturns() { } } +void ShortcutMessages::confirmDeleteSelected() { + ConfirmDeleteSelectedItems(_inner); +} + +void ShortcutMessages::clearSelected() { + _inner->cancelSelection(); +} + void ShortcutMessages::uploadFile( const QByteArray &fileContent, SendMediaType type) { From f086203d258450592bdf713321b4e86a26df690e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 28 Feb 2024 08:58:37 +0400 Subject: [PATCH 057/108] Implement proper shortcut management. --- Telegram/SourceFiles/api/api_updates.cpp | 2 + .../data/business/data_shortcut_messages.cpp | 222 +++++++++++++++--- .../data/business/data_shortcut_messages.h | 21 +- Telegram/SourceFiles/history/history_item.cpp | 4 + Telegram/SourceFiles/history/history_item.h | 1 + .../history/view/history_view_bottom_info.cpp | 11 +- .../history/view/history_view_bottom_info.h | 1 + .../history/view/history_view_list_widget.cpp | 6 + Telegram/SourceFiles/info/info_top_bar.cpp | 21 +- .../SourceFiles/info/info_wrap_widget.cpp | 32 ++- Telegram/SourceFiles/info/info_wrap_widget.h | 8 +- .../info/settings/info_settings_widget.cpp | 8 + .../info/settings/info_settings_widget.h | 3 + .../business/settings_away_message.cpp | 1 - .../settings/business/settings_greeting.cpp | 5 + .../business/settings_quick_replies.cpp | 89 ++++++- .../business/settings_shortcut_messages.cpp | 98 ++++++-- Telegram/SourceFiles/settings/settings.style | 16 ++ .../SourceFiles/settings/settings_common.h | 12 + 19 files changed, 474 insertions(+), 87 deletions(-) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 57db15d92..ab4b5f30b 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1560,6 +1560,8 @@ void Updates::feedUpdate(const MTPUpdate &update) { if (const auto local = owner.message(id)) { if (local->isScheduled()) { session().data().scheduledMessages().apply(d, local); + } else if (local->isBusinessShortcut()) { + session().data().shortcutMessages().apply(d, local); } else { const auto existing = session().data().message( id.peer, diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 026e6a99e..8b59309bf 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -128,6 +128,94 @@ void ShortcutMessages::clearOldRequests() { } } +void ShortcutMessages::updateShortcuts(const QVector<MTPQuickReply> &list) { + auto shortcuts = parseShortcuts(list); + auto changes = std::vector<ShortcutIdChange>(); + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcuts.list.contains(id)) { + continue; + } + auto foundId = BusinessShortcutId(); + for (auto &[realId, real] : shortcuts.list) { + if (real.name == shortcut.name) { + foundId = realId; + break; + } + } + if (foundId) { + mergeMessagesFromTo(id, foundId); + changes.push_back({ .oldId = id, .newId = foundId }); + } else { + shortcuts.list.emplace(id, shortcut); + } + } + const auto changed = !_shortcutsLoaded + || (shortcuts != _shortcuts); + if (changed) { + _shortcuts = std::move(shortcuts); + _shortcutsLoaded = true; + for (const auto &change : changes) { + _shortcutIdChanges.fire_copy(change); + } + _shortcutsChanged.fire({}); + } else { + Assert(changes.empty()); + } +} + +void ShortcutMessages::mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId) { + auto &to = _data[toId]; + const auto i = _data.find(fromId); + if (i == end(_data)) { + return; + } + + auto &from = i->second; + auto destroy = base::flat_set<not_null<HistoryItem*>>(); + for (auto &item : from.items) { + if (item->isSending() || item->hasFailed()) { + item->setRealShortcutId(toId); + to.items.push_back(std::move(item)); + } else { + destroy.emplace(item.get()); + } + } + for (const auto &item : destroy) { + item->destroy(); + } + _data.remove(fromId); + + cancelRequest(fromId); + + _updates.fire_copy(toId); + if (!destroy.empty()) { + cancelRequest(toId); + request(toId); + } +} + +Shortcuts ShortcutMessages::parseShortcuts( + const QVector<MTPQuickReply> &list) const { + auto result = Shortcuts(); + for (const auto &reply : list) { + const auto shortcut = parseShortcut(reply); + result.list.emplace(shortcut.id, shortcut); + } + return result; +} + +Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const { + const auto &data = reply.data(); + return Shortcut{ + .id = BusinessShortcutId(data.vshortcut_id().v), + .count = data.vcount().v, + .name = qs(data.vshortcut()), + .topMessageId = localMessageId(data.vtop_message().v), + }; +} + MsgId ShortcutMessages::localMessageId(MsgId remoteId) const { return RemoteToLocalMsgId(remoteId); } @@ -146,11 +234,60 @@ int ShortcutMessages::count(BusinessShortcutId shortcutId) const { } void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) { + updateShortcuts(update.vquick_replies().v); + scheduleShortcutsReload(); +} +void ShortcutMessages::scheduleShortcutsReload() { + const auto hasUnknownMessages = [&] { + const auto selfId = _session->userPeerId(); + for (const auto &[id, shortcut] : _shortcuts.list) { + if (!_session->data().message({ selfId, shortcut.topMessageId })) { + return true; + } + } + return false; + }; + if (hasUnknownMessages()) { + _shortcutsLoaded = false; + const auto cancelledId = base::take(_shortcutsRequestId); + _session->api().request(cancelledId).cancel(); + crl::on_main(_session, [=] { + if (cancelledId || hasUnknownMessages()) { + preloadShortcuts(); + } + }); + } } void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) { - + const auto selfId = _session->userPeerId(); + const auto &reply = update.vquick_reply(); + auto foundId = BusinessShortcutId(); + const auto shortcut = parseShortcut(reply); + for (auto &[id, existing] : _shortcuts.list) { + if (id == shortcut.id) { + foundId = id; + break; + } else if (existing.name == shortcut.name) { + foundId = id; + break; + } + } + if (foundId == shortcut.id) { + auto &already = _shortcuts.list[shortcut.id]; + if (already != shortcut) { + already = shortcut; + _shortcutsChanged.fire({}); + } + return; + } else if (foundId) { + _shortcuts.list.emplace(shortcut.id, shortcut); + mergeMessagesFromTo(foundId, shortcut.id); + _shortcuts.list.remove(foundId); + _shortcutIdChanges.fire({ foundId, shortcut.id }); + _shortcutsChanged.fire({}); + } } void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { @@ -159,10 +296,30 @@ void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { if (!shortcutId) { return; } + const auto loaded = _data.contains(shortcutId); auto &list = _data[shortcutId]; append(shortcutId, list, message); sort(list); _updates.fire_copy(shortcutId); + updateCount(shortcutId); + if (!loaded) { + request(shortcutId); + } +} + +void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + const auto j = _shortcuts.list.find(shortcutId); + if (j == end(_shortcuts.list)) { + return; + } + const auto count = (i != end(_data)) + ? int(i->second.itemById.size()) + : 0; + if (j->second.count != count) { + _shortcuts.list[shortcutId].count = count; + _shortcutsChanged.fire({}); + } } void ShortcutMessages::apply( @@ -187,6 +344,10 @@ void ShortcutMessages::apply( } } _updates.fire_copy(shortcutId); + updateCount(shortcutId); + + cancelRequest(shortcutId); + request(shortcutId); } void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { @@ -195,12 +356,17 @@ void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { return; } auto i = _data.find(shortcutId); - while (i != end(_data)) { - Assert(!i->second.itemById.empty()); + while (i != end(_data) && !i->second.itemById.empty()) { i->second.itemById.back().second->destroy(); i = _data.find(shortcutId); } _updates.fire_copy(shortcutId); + if (_data.contains(shortcutId)) { + updateCount(shortcutId); + } else { + _shortcuts.list.remove(shortcutId); + _shortcutIdChanges.fire({ shortcutId, 0 }); + } } void ShortcutMessages::apply( @@ -283,30 +449,7 @@ void ShortcutMessages::preloadShortcuts() { owner->processMessages( data.vmessages(), NewMessageType::Existing); - auto shortcuts = Shortcuts(); - const auto messages = &owner->shortcutMessages(); - for (const auto &reply : data.vquick_replies().v) { - const auto &data = reply.data(); - const auto id = BusinessShortcutId(data.vshortcut_id().v); - shortcuts.list.emplace(id, Shortcut{ - .name = qs(data.vshortcut()), - .topMessageId = messages->localMessageId( - data.vtop_message().v), - .count = data.vcount().v, - }); - } - for (auto &[id, shortcut] : _shortcuts.list) { - if (id < 0) { - shortcuts.list.emplace(id, shortcut); - } - } - const auto changed = !_shortcutsLoaded - || (shortcuts != _shortcuts); - if (changed) { - _shortcuts = std::move(shortcuts); - _shortcutsLoaded = true; - _shortcutsChanged.fire({}); - } + updateShortcuts(data.vquick_replies().v); }, [&](const MTPDmessages_quickRepliesNotModified &) { if (!_shortcutsLoaded) { _shortcutsLoaded = true; @@ -328,6 +471,11 @@ rpl::producer<> ShortcutMessages::shortcutsChanged() const { return _shortcutsChanged.events(); } +auto ShortcutMessages::shortcutIdChanged() const +-> rpl::producer<ShortcutIdChange> { + return _shortcutIdChanges.events(); +} + BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { Expects(_shortcutsLoaded); @@ -337,7 +485,7 @@ BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { } } const auto result = --_localShortcutId; - _shortcuts.list.emplace(result, Shortcut{ name }); + _shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name }); return result; } @@ -348,6 +496,14 @@ Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { return i->second; } +void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) { + const auto j = _requests.find(shortcutId); + if (j != end(_requests)) { + _session->api().request(j->second.requestId).cancel(); + _requests.erase(j); + } +} + void ShortcutMessages::request(BusinessShortcutId shortcutId) { auto &request = _requests[shortcutId]; if (request.requestId || TooEarlyForRequest(request.lastReceived)) { @@ -512,6 +668,7 @@ void ShortcutMessages::remove(not_null<const HistoryItem*> item) { _data.erase(i); } _updates.fire_copy(shortcutId); + updateCount(shortcutId); } uint64 ShortcutMessages::countListHash(const List &list) const { @@ -537,11 +694,10 @@ uint64 ShortcutMessages::countListHash(const List &list) const { MTPInputQuickReplyShortcut ShortcutIdToMTP( not_null<Main::Session*> session, BusinessShortcutId id) { - if (id >= 0) { - return MTP_inputQuickReplyShortcutId(MTP_int(id)); - } - return MTP_inputQuickReplyShortcut(MTP_string( - session->data().shortcutMessages().lookupShortcut(id).name)); + return id + ? MTP_inputQuickReplyShortcut(MTP_string( + session->data().shortcutMessages().lookupShortcut(id).name)) + : MTPInputQuickReplyShortcut(); } } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h index 57997e7e0..6b7343b57 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.h +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -22,15 +22,21 @@ class Session; struct MessagesSlice; struct Shortcut { + BusinessShortcutId id = 0; + int count = 0; QString name; MsgId topMessageId = 0; - int count = 0; friend inline bool operator==( const Shortcut &a, const Shortcut &b) = default; }; +struct ShortcutIdChange { + BusinessShortcutId oldId = 0; + BusinessShortcutId newId = 0; +}; + struct Shortcuts { base::flat_map<BusinessShortcutId, Shortcut> list; @@ -69,6 +75,7 @@ public: [[nodiscard]] const Shortcuts &shortcuts() const; [[nodiscard]] bool shortcutsLoaded() const; [[nodiscard]] rpl::producer<> shortcutsChanged() const; + [[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const; [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; @@ -100,6 +107,17 @@ private: void remove(not_null<const HistoryItem*> item); [[nodiscard]] uint64 countListHash(const List &list) const; void clearOldRequests(); + void cancelRequest(BusinessShortcutId shortcutId); + void updateCount(BusinessShortcutId shortcutId); + + void scheduleShortcutsReload(); + void mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId); + void updateShortcuts(const QVector<MTPQuickReply> &list); + [[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const; + [[nodiscard]] Shortcuts parseShortcuts( + const QVector<MTPQuickReply> &list) const; const not_null<Main::Session*> _session; const not_null<History*> _history; @@ -111,6 +129,7 @@ private: Shortcuts _shortcuts; rpl::event_stream<> _shortcutsChanged; + rpl::event_stream<ShortcutIdChange> _shortcutIdChanges; BusinessShortcutId _localShortcutId = 0; uint64 _shortcutsHash = 0; mtpRequestId _shortcutsRequestId = 0; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 84986c28a..807563fc0 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1542,6 +1542,10 @@ bool HistoryItem::isBusinessShortcut() const { return _shortcutId != 0; } +void HistoryItem::setRealShortcutId(BusinessShortcutId id) { + _shortcutId = id; +} + void HistoryItem::destroy() { _history->destroyMessage(this); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index f287bb039..d75182680 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -196,6 +196,7 @@ public: [[nodiscard]] bool isUserpicSuggestion() const; [[nodiscard]] BusinessShortcutId shortcutId() const; [[nodiscard]] bool isBusinessShortcut() const; + void setRealShortcutId(BusinessShortcutId id); void addLogEntryOriginal( WebPageId localId, diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index c008fac99..cfb31eabe 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -442,7 +442,7 @@ void BottomInfo::paintReactions( } QSize BottomInfo::countCurrentSize(int newWidth) { - if (newWidth >= maxWidth()) { + if (newWidth >= maxWidth() || (_data.flags & Data::Flag::Shortcut)) { return optimalSize(); } const auto dateHeight = (_data.flags & Data::Flag::Sponsored) @@ -509,7 +509,8 @@ void BottomInfo::layoutRepliesText() { if (!_data.replies || !*_data.replies || (_data.flags & Data::Flag::RepliesContext) - || (_data.flags & Data::Flag::Sending)) { + || (_data.flags & Data::Flag::Sending) + || (_data.flags & Data::Flag::Shortcut)) { _replies.clear(); return; } @@ -549,6 +550,9 @@ void BottomInfo::layoutReactionsText() { } QSize BottomInfo::countOptimalSize() { + if (_data.flags & Data::Flag::Shortcut) { + return { st::historySendStateSpace / 2, st::msgDateFont->height }; + } auto width = 0; if (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { width += st::historySendStateSpace; @@ -654,6 +658,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) { if (item->isPinned() && message->context() != Context::Pinned) { result.flags |= Flag::Pinned; } + if (message->context() == Context::ShortcutMessages) { + result.flags |= Flag::Shortcut; + } if (const auto msgsigned = item->Get<HistoryMessageSigned>()) { if (!msgsigned->isAnonymousRank) { result.author = msgsigned->postAuthor; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index d593063ef..efdab3334 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -44,6 +44,7 @@ public: Sponsored = 0x10, Pinned = 0x20, Imported = 0x40, + Shortcut = 0x80, //Unread, // We don't want to pass and update it in Date for now. }; friend inline constexpr bool is_flag_type(Flag) { return true; }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 3c09635a1..31e6b4e9e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1937,6 +1937,9 @@ int ListWidget::resizeGetHeight(int newWidth) { _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0; + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } return _itemsTop + _itemsHeight + st::historyPaddingBottom; } @@ -3934,6 +3937,9 @@ void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) { _emptyInfo = std::move(w); + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } } ListWidget::~ListWidget() { diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index d9a422adb..1e5f34c2f 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" -#include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" @@ -721,25 +720,7 @@ bool TopBar::computeCanToggleStoryPin() const { } Ui::StringWithNumbers TopBar::generateSelectedText() const { - using Type = Storage::SharedMediaType; - const auto phrase = [&] { - switch (_selectedItems.type) { - case Type::Photo: return tr::lng_media_selected_photo; - case Type::GIF: return tr::lng_media_selected_gif; - case Type::Video: return tr::lng_media_selected_video; - case Type::File: return tr::lng_media_selected_file; - case Type::MusicFile: return tr::lng_media_selected_song; - case Type::Link: return tr::lng_media_selected_link; - case Type::RoundVoiceFile: return tr::lng_media_selected_audio; - case Type::PhotoVideo: return tr::lng_stories_row_count; - } - Unexpected("Type in TopBar::generateSelectedText()"); - }(); - return phrase( - tr::now, - lt_count, - _selectedItems.list.size(), - Ui::StringWithNumbers::FromString); + return _selectedItems.title(_selectedItems.list.size()); } bool TopBar::selectionMode() const { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index bf03f7131..ddab6ef51 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "mainwidget.h" #include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_info.h" #include "styles/style_profile.h" @@ -64,6 +65,26 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { && section.settingsType()->hasCustomTopBar(); } +[[nodiscard]] Fn<Ui::StringWithNumbers(int)> SelectedTitleForMedia( + Section::MediaType type) { + return [type](int count) { + using Type = Storage::SharedMediaType; + return [&] { + switch (type) { + case Type::Photo: return tr::lng_media_selected_photo; + case Type::GIF: return tr::lng_media_selected_gif; + case Type::Video: return tr::lng_media_selected_video; + case Type::File: return tr::lng_media_selected_file; + case Type::MusicFile: return tr::lng_media_selected_song; + case Type::Link: return tr::lng_media_selected_link; + case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + case Type::PhotoVideo: return tr::lng_stories_row_count; + } + Unexpected("Type in TopBar::generateSelectedText()"); + }()(tr::now, lt_count, count, Ui::StringWithNumbers::FromString); + }; +} + } // namespace struct WrapWidget::StackItem { @@ -71,6 +92,10 @@ struct WrapWidget::StackItem { // std::shared_ptr<ContentMemento> anotherTab; }; +SelectedItems::SelectedItems(Section::MediaType mediaType) +: title(SelectedTitleForMedia(mediaType)) { +} + WrapWidget::WrapWidget( QWidget *parent, not_null<Window::SessionController*> window, @@ -609,7 +634,12 @@ void WrapWidget::finishShowContent() { _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); _desiredBottomShadowVisibilities.fire( _content->desiredBottomShadowVisibility()); - _selectedLists.fire(_content->selectedListValue()); + if (auto selection = _content->selectedListValue()) { + _selectedLists.fire(std::move(selection)); + } else { + _selectedLists.fire(rpl::single( + SelectedItems(Storage::SharedMediaType::Photo))); + } _scrollTillBottomChanges.fire(_content->scrollTillBottomChanges()); _topShadow->raise(); _topShadow->finishAnimating(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index f102cc834..0759c0729 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -20,6 +20,7 @@ class PlainShadow; class PopupMenu; class IconButton; class RoundRect; +struct StringWithNumbers; } // namespace Ui namespace Window { @@ -61,11 +62,10 @@ struct SelectedItem { }; struct SelectedItems { - explicit SelectedItems(Storage::SharedMediaType type) - : type(type) { - } + SelectedItems() = default; + explicit SelectedItems(Storage::SharedMediaType type); - Storage::SharedMediaType type; + Fn<Ui::StringWithNumbers(int)> title; std::vector<SelectedItem> list; }; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 2cddcf63e..85878baf1 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -248,6 +248,14 @@ void Widget::enableBackButton() { _flexibleScroll.backButtonEnables.fire({}); } +rpl::producer<SelectedItems> Widget::selectedListValue() const { + return _inner->selectedListValue(); +} + +void Widget::selectionAction(SelectionAction action) { + _inner->selectionAction(action); +} + void Widget::saveState(not_null<Memento*> memento) { memento->setScrollTop(scrollTopSave()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index d2eb63615..7ab7968d5 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -80,6 +80,9 @@ public: void enableBackButton() override; + rpl::producer<SelectedItems> selectedListValue() const override; + void selectionAction(SelectionAction action) override; + private: void saveState(not_null<Memento*> memento); void restoreState(not_null<Memento*> memento); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 6c820afa9..180145716 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -38,7 +38,6 @@ public: ~AwayMessage(); [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 7e5ad6b1e..81580801c 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -41,6 +41,7 @@ public: ~Greeting(); [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] rpl::producer<Type> sectionShowOther() override; const Ui::RoundRect *bottomSkipRounding() const { return &_bottomSkipRounding; @@ -176,6 +177,10 @@ rpl::producer<QString> Greeting::title() { return tr::lng_greeting_title(); } +rpl::producer<Type> Greeting::sectionShowOther() { + return _showOther.events(); +} + void Greeting::setupContent( not_null<Window::SessionController*> controller) { using namespace rpl::mappers; diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index dc8927bc2..e13f822a2 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -8,16 +8,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_quick_replies.h" #include "core/application.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { @@ -31,12 +37,13 @@ public: ~QuickReplies(); [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: void setupContent(not_null<Window::SessionController*> controller); void save(); - rpl::variable<Data::BusinessRecipients> _recipients; + rpl::event_stream<Type> _showOther; }; @@ -57,6 +64,10 @@ rpl::producer<QString> QuickReplies::title() { return tr::lng_replies_title(); } +rpl::producer<Type> QuickReplies::sectionShowOther() { + return _showOther.events(); +} + void QuickReplies::setupContent( not_null<Window::SessionController*> controller) { using namespace rpl::mappers; @@ -73,24 +84,82 @@ void QuickReplies::setupContent( }); Ui::AddSkip(content); - const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + const auto add = content->add(object_ptr<Ui::SettingsButton>( content, tr::lng_replies_add(), st::settingsButtonNoIcon )); - enabled->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto messages = &owner->shortcutMessages(); + add->setClickedCallback([=] { + controller->show(Box([=](not_null<Ui::GenericBox*> box) { + box->setTitle(tr::lng_replies_add_title()); + box->addRow(object_ptr<Ui::FlatLabel>( + box, + tr::lng_replies_add_shortcut(), + st::settingsAddReplyLabel)); + const auto field = box->addRow(object_ptr<Ui::InputField>( + box, + st::settingsAddReplyField, + tr::lng_replies_add_placeholder(), + QString())); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto submit = [=] { + const auto weak = Ui::MakeWeak(box); + const auto name = field->getLastText().trimmed(); + if (name.isEmpty()) { + field->showError(); + } else { + const auto id = messages->emplaceShortcut(name); + _showOther.fire(ShortcutMessagesId(id)); + } + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + field->submits( + ) | rpl::start_with_next(submit, field->lifetime()); + box->addButton(tr::lng_settings_save(), submit); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + })); }); - const auto wrap = content->add( - object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( - content, - object_ptr<Ui::VerticalLayout>(content))); - const auto inner = wrap->entity(); + Ui::AddSkip(content); + Ui::AddDivider(content); + Ui::AddSkip(content); - Ui::AddSkip(inner); - Ui::AddDivider(inner); + const auto inner = content->add( + object_ptr<Ui::VerticalLayout>(content)); + rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::start_with_next([=] { + while (inner->count()) { + delete inner->widgetAt(0); + } + const auto &shortcuts = messages->shortcuts(); + auto i = 0; + for (const auto &shortcut : shortcuts.list) { + const auto name = shortcut.second.name; + AddButtonWithLabel( + inner, + rpl::single('/' + name), + tr::lng_forum_messages( + lt_count, + rpl::single(1. * shortcut.second.count)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto id = messages->emplaceShortcut(name); + _showOther.fire(ShortcutMessagesId(id)); + }); + } + }, content->lifetime()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index df5f778ba..e2186a172 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -30,14 +30,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_sticker_toast.h" #include "history/history.h" #include "history/history_item.h" +#include "info/info_wrap_widget.h" #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" #include "main/main_session.h" #include "menu/menu_send.h" #include "settings/business/settings_recipients_helper.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/storage_media_prepare.h" +#include "storage/storage_shared_media.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -72,13 +75,16 @@ public: [[nodiscard]] static Type Id(BusinessShortcutId shortcutId); [[nodiscard]] Type id() const final override { - return Id(_shortcutId); + return Id(_shortcutId.current()); } [[nodiscard]] rpl::producer<QString> title() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; void setInnerFocus() override; + rpl::producer<Info::SelectedItems> selectedListValue() override; + void selectionAction(Info::SelectionAction action) override; + bool paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, @@ -238,8 +244,9 @@ private: const not_null<Window::SessionController*> _controller; const not_null<Main::Session*> _session; const not_null<Ui::ScrollArea*> _scroll; - const BusinessShortcutId _shortcutId; const not_null<History*> _history; + rpl::variable<BusinessShortcutId> _shortcutId; + rpl::variable<QString> _shortcut; std::shared_ptr<Ui::ChatStyle> _style; std::shared_ptr<Ui::ChatTheme> _theme; QPointer<ListWidget> _inner; @@ -248,6 +255,9 @@ private: rpl::event_stream<> _showBackRequests; bool _skipScrollEvent = false; + rpl::variable<Info::SelectedItems> _selectedItems + = Info::SelectedItems(Storage::SharedMediaType::kCount); + std::unique_ptr<StickerToast> _stickerToast; FullMsgId _lastShownAt; @@ -287,12 +297,31 @@ ShortcutMessages::ShortcutMessages( , _controller(controller) , _session(&controller->session()) , _scroll(scroll) -, _shortcutId(shortcutId) , _history(_session->data().history(_session->user()->id)) +, _shortcutId(shortcutId) +, _shortcut( + _session->data().shortcutMessages().lookupShortcut(shortcutId).name) , _cornerButtons( _scroll, controller->chatStyle(), static_cast<HistoryView::CornerButtonsDelegate*>(this)) { + const auto messages = &_session->data().shortcutMessages(); + + messages->shortcutIdChanged( + ) | rpl::start_with_next([=](Data::ShortcutIdChange change) { + if (change.oldId == _shortcutId.current()) { + if (change.newId) { + _shortcutId = change.newId; + } else { + _showBackRequests.fire({}); + } + } + }, lifetime()); + messages->shortcutsChanged( + ) | rpl::start_with_next([=] { + _shortcut = messages->lookupShortcut(_shortcutId.current()).name; + }, lifetime()); + controller->chatStyle()->paletteChanged( ) | rpl::start_with_next([=] { _scroll->updateBars(); @@ -351,7 +380,13 @@ Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { } rpl::producer<QString> ShortcutMessages::title() { - return rpl::single(u"Editing messages list"_q); + return _shortcut.value() | rpl::map([=](const QString &shortcut) { + return (shortcut == u"away"_q) + ? tr::lng_away_title() + : (shortcut == u"hello"_q) + ? tr::lng_greeting_title() + : rpl::single('/' + shortcut); + }) | rpl::flatten_latest(); } void ShortcutMessages::processScroll() { @@ -379,6 +414,18 @@ void ShortcutMessages::setInnerFocus() { _composeControls->focus(); } +rpl::producer<Info::SelectedItems> ShortcutMessages::selectedListValue() { + return _selectedItems.value(); +} + +void ShortcutMessages::selectionAction(Info::SelectionAction action) { + switch (action) { + case Info::SelectionAction::Clear: clearSelected(); return; + case Info::SelectionAction::Delete: confirmDeleteSelected(); return; + } + Unexpected("Action in ShortcutMessages::selectionAction."); +} + bool ShortcutMessages::paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, @@ -572,7 +619,7 @@ bool ShortcutMessages::listScrollTo(int top, bool syntetic) { void ShortcutMessages::listCancelRequest() { if (_inner && !_inner->getSelectedItems().empty()) { - //clearSelected(); + clearSelected(); return; } else if (_composeControls->handleCancelRequest()) { return; @@ -592,13 +639,15 @@ rpl::producer<Data::MessagesSlice> ShortcutMessages::listSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { - const auto data = &_controller->session().data(); - return rpl::single(rpl::empty) | rpl::then( - data->shortcutMessages().updates(_shortcutId) - ) | rpl::map([=] { - return data->shortcutMessages().list(_shortcutId); - }); - return rpl::never<Data::MessagesSlice>(); + const auto messages = &_session->data().shortcutMessages(); + return _shortcutId.value( + ) | rpl::map([=](BusinessShortcutId shortcutId) { + return rpl::single(rpl::empty) | rpl::then( + messages->updates(shortcutId) + ) | rpl::map([=] { + return messages->list(shortcutId); + }); + }) | rpl::flatten_latest(); } bool ShortcutMessages::listAllowsMultiSelect() { @@ -617,6 +666,24 @@ bool ShortcutMessages::listIsLessInOrder( } void ShortcutMessages::listSelectionChanged(SelectedItems &&items) { + auto value = Info::SelectedItems(); + value.title = [](int count) { + return tr::lng_forum_messages( + tr::now, + lt_count, + count, + Ui::StringWithNumbers::FromString); + }; + value.list = items | ranges::views::transform([](SelectedItem item) { + auto result = Info::SelectedItem(GlobalMsgId{ item.msgId }); + result.canDelete = item.canDelete; + return result; + }) | ranges::to_vector; + _selectedItems = std::move(value); + + if (items.empty()) { + doSetInnerFocus(); + } } void ShortcutMessages::listMarkReadTill(not_null<HistoryItem*> item) { @@ -784,20 +851,21 @@ bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { } void ShortcutMessages::pushReplyReturn(not_null<HistoryItem*> item) { - if (item->shortcutId() == _shortcutId) { + if (item->shortcutId() == _shortcutId.current()) { _cornerButtons.pushReplyReturn(item); } } void ShortcutMessages::checkReplyReturns() { const auto currentTop = _scroll->scrollTop(); + const auto shortcutId = _shortcutId.current(); while (const auto replyReturn = _cornerButtons.replyReturn()) { const auto position = replyReturn->position(); const auto scrollTop = _inner->scrollTopForPosition(position); const auto below = scrollTop ? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax())) : _inner->isBelowPosition(position); - if (below) { + if (replyReturn->shortcutId() != shortcutId || below) { _cornerButtons.calculateNextReplyReturn(); } else { break; @@ -858,7 +926,7 @@ Api::SendAction ShortcutMessages::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); - result.options.shortcutId = _shortcutId; + result.options.shortcutId = _shortcutId.current(); result.options.sendAs = _composeControls->sendAsPeer(); return result; } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 41fb908e0..fcdd0a7cd 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -616,3 +616,19 @@ settingsWorkingHoursPicker: 200px; settingsWorkingHoursPickerItemHeight: 40px; settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px); + +settingsAddReplyLabel: FlatLabel(defaultFlatLabel) { + minWidth: 256px; +} +settingsAddReplyField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 10px, 0px, 2px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + + heightMin: 36px; +} \ No newline at end of file diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 02beb1003..e401ad419 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -17,6 +17,11 @@ namespace anim { enum class repeat : uchar; } // namespace anim +namespace Info { +struct SelectedItems; +enum class SelectionAction; +} // namespace Info + namespace Main { class Session; } // namespace Main @@ -91,6 +96,13 @@ public: virtual void setStepDataReference(std::any &data) { } + [[nodiscard]] virtual auto selectedListValue() + -> rpl::producer<Info::SelectedItems> { + return nullptr; + } + virtual void selectionAction(Info::SelectionAction action) { + } + virtual bool paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, From aad8e989d8e8e6050602d7d704dcacc71b3ba300 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 28 Feb 2024 14:55:28 +0400 Subject: [PATCH 058/108] Shortcuts edit / delete menu. --- Telegram/Resources/langs/lang.strings | 4 + .../data/business/data_shortcut_messages.cpp | 65 ++++++++++ .../data/business/data_shortcut_messages.h | 6 + .../info/downloads/info_downloads_widget.cpp | 29 +++++ .../info/downloads/info_downloads_widget.h | 2 + .../SourceFiles/info/info_content_widget.cpp | 19 +++ .../SourceFiles/info/info_content_widget.h | 5 + .../SourceFiles/info/info_wrap_widget.cpp | 85 ++++--------- Telegram/SourceFiles/info/info_wrap_widget.h | 2 +- .../info/settings/info_settings_widget.cpp | 4 + .../info/settings/info_settings_widget.h | 1 + .../business/settings_away_message.cpp | 8 +- .../settings/business/settings_greeting.cpp | 8 +- .../business/settings_quick_replies.cpp | 116 ++++++++++-------- .../business/settings_quick_replies.h | 9 ++ .../business/settings_shortcut_messages.cpp | 60 ++++++++- .../settings_cloud_password_email_confirm.cpp | 22 ++++ .../settings/settings_advanced.cpp | 8 +- .../SourceFiles/settings/settings_advanced.h | 4 - .../settings/settings_business.cpp | 9 +- .../SourceFiles/settings/settings_chat.cpp | 12 +- Telegram/SourceFiles/settings/settings_chat.h | 5 + .../SourceFiles/settings/settings_common.h | 3 + .../settings/settings_common_session.cpp | 46 +------ .../settings/settings_common_session.h | 23 +++- .../settings/settings_local_passcode.cpp | 8 +- .../SourceFiles/settings/settings_main.cpp | 37 ++++-- Telegram/SourceFiles/settings/settings_main.h | 4 +- .../settings/settings_notifications.cpp | 8 +- .../settings/settings_notifications.h | 4 - .../settings/settings_privacy_security.cpp | 8 +- .../settings/settings_privacy_security.h | 4 - 32 files changed, 391 insertions(+), 237 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index dd5746e44..896fdc415 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2212,6 +2212,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_replies_edit_title" = "Edit Shortcut"; "lng_replies_edit_about" = "Edit the name for this shortcut."; "lng_replies_message_placeholder" = "Add a Quick Reply"; +"lng_replies_delete_sure" = "Are you sure you want to delete this quick reply with all its messages?"; +"lng_replies_error_occupied" = "This shortcut is already used."; "lng_greeting_title" = "Greeting Message"; "lng_greeting_about" = "Greet customers when they message you the first time or after a period of no activity."; @@ -3027,6 +3029,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_translate_selected" = "Translate Selected Text"; "lng_context_read_hidden" = "read"; "lng_context_read_show" = "show when"; +"lng_context_edit_shortcut" = "Edit Shortcut"; +"lng_context_delete_shortcut" = "Delete Quick Reply"; "lng_add_tag_about" = "Tag this message with an emoji for quick search."; "lng_subscribe_tag_about" = "Organize your Saved Messages with tags. {link}"; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 8b59309bf..d9d75ec52 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -496,6 +496,71 @@ Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { return i->second; } +void ShortcutMessages::editShortcut( + BusinessShortcutId id, + QString name, + Fn<void()> done, + Fn<void(QString)> fail) { + name = name.trimmed(); + if (name.isEmpty()) { + fail(QString()); + return; + } + const auto finish = [=] { + const auto i = _shortcuts.list.find(id); + if (i != end(_shortcuts.list)) { + i->second.name = name; + _shortcutsChanged.fire({}); + } + done(); + }; + for (const auto &[existingId, shortcut] : _shortcuts.list) { + if (shortcut.name == name) { + if (existingId == id) { + //done(); + //return; + break; + } else if (_data[existingId].items.empty() && !shortcut.count) { + removeShortcut(existingId); + break; + } else { + fail(u"SHORTCUT_OCCUPIED"_q); + return; + } + } + } + _session->api().request(MTPmessages_EditQuickReplyShortcut( + MTP_int(id), + MTP_string(name) + )).done(finish).fail([=](const MTP::Error &error) { + const auto type = error.type(); + if (type == u"SHORTCUT_ID_INVALID"_q) { + // Not on the server (yet). + finish(); + } else { + fail(type); + } + }).send(); +} + +void ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) { + auto i = _data.find(shortcutId); + while (i != end(_data)) { + if (i->second.items.empty()) { + _data.erase(i); + } else { + i->second.items.front()->destroy(); + } + i = _data.find(shortcutId); + } + _shortcuts.list.remove(shortcutId); + _shortcutIdChanges.fire({ shortcutId, 0 }); + + _session->api().request(MTPmessages_DeleteQuickReplyShortcut( + MTP_int(shortcutId) + )).send(); +} + void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) { const auto j = _requests.find(shortcutId); if (j != end(_requests)) { diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h index 6b7343b57..57c1205f8 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.h +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -78,6 +78,12 @@ public: [[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const; [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; + void editShortcut( + BusinessShortcutId id, + QString name, + Fn<void()> done, + Fn<void(QString)> fail); + void removeShortcut(BusinessShortcutId shortcutId); private: using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 6efd528f6..9203981dc 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -10,13 +10,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/downloads/info_downloads_inner_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" +#include "ui/boxes/confirm_box.h" #include "ui/search_field_controller.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/scroll_area.h" #include "data/data_download_manager.h" #include "data/data_user.h" #include "core/application.h" #include "lang/lang_keys.h" #include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" namespace Info::Downloads { @@ -102,6 +106,31 @@ void Widget::selectionAction(SelectionAction action) { _inner->selectionAction(action); } +void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto window = controller()->parentController(); + const auto deleteAll = [=] { + auto &manager = Core::App().downloadManager(); + const auto phrase = tr::lng_downloads_delete_sure_all(tr::now); + const auto added = manager.loadedHasNonCloudFile() + ? QString() + : tr::lng_downloads_delete_in_cloud(tr::now); + const auto deleteSure = [=, &manager](Fn<void()> close) { + Ui::PostponeCall(this, close); + manager.deleteAll(); + }; + window->show(Ui::MakeConfirmBox({ + .text = phrase + (added.isEmpty() ? QString() : "\n\n" + added), + .confirmed = deleteSure, + .confirmText = tr::lng_box_delete(tr::now), + .confirmStyle = &st::attentionBoxButton, + })); + }; + addAction( + tr::lng_context_delete_all_files(tr::now), + deleteAll, + &st::menuIconDelete); +} + rpl::producer<QString> Widget::title() { return tr::lng_downloads_section(); } diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.h b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h index 3da1a4f79..f79a31466 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h @@ -57,6 +57,8 @@ public: rpl::producer<SelectedItems> selectedListValue() const override; void selectionAction(SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; + rpl::producer<QString> title() override; private: diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index ab5a0ef5e..adf4f22d2 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_forum.h" #include "main/main_session.h" +#include "window/window_peer_menu.h" #include "styles/style_info.h" #include "styles/style_profile.h" #include "styles/style_layers.h" @@ -263,6 +264,24 @@ QRect ContentWidget::floatPlayerAvailableRect() const { return mapToGlobal(_scroll->geometry()); } +void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto peer = _controller->key().peer(); + const auto topic = _controller->key().topic(); + if (!peer && !topic) { + return; + } + + Window::FillDialogsEntryMenu( + _controller->parentController(), + Dialogs::EntryState{ + .key = (topic + ? Dialogs::Key{ topic } + : Dialogs::Key{ peer->owner().history(peer) }), + .section = Dialogs::EntryState::Section::Profile, + }, + addAction); +} + rpl::producer<SelectedItems> ContentWidget::selectedListValue() const { return rpl::single(SelectedItems(Storage::SharedMediaType::Photo)); } diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index b8d1ebe4b..29d947c66 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -28,6 +28,10 @@ template <typename Widget> class PaddingWrap; } // namespace Ui +namespace Ui::Menu { +struct MenuCallback; +} // namespace Ui::Menu + namespace Info::Settings { struct Tag; } // namespace Info::Settings @@ -95,6 +99,7 @@ public: virtual rpl::producer<SelectedItems> selectedListValue() const; virtual void selectionAction(SelectionAction action) { } + virtual void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction); [[nodiscard]] virtual rpl::producer<QString> title() = 0; [[nodiscard]] virtual rpl::producer<QString> subtitle() { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index ddab6ef51..a2c6a781c 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_main.h" #include "settings/settings_premium.h" #include "ui/effects/ripple_animation.h" // MaskByDrawer. +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" @@ -31,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/shortcuts.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" -#include "window/window_peer_menu.h" #include "boxes/peer_list_box.h" #include "ui/boxes/confirm_box.h" #include "main/main_session.h" @@ -364,16 +364,24 @@ void WrapWidget::createTopBar() { _controller->searchEnabledByContent(), _controller->takeSearchStartsFocused()); } + _topBar->lower(); + _topBar->resizeToWidth(width()); + _topBar->finishAnimating(); + _topBar->show(); +} + +void WrapWidget::setupTopBarMenuToggle() { + Expects(_content != nullptr); + + if (!_topBar) { + return; + } const auto section = _controller->section(); if (section.type() == Section::Type::Profile - && (wrapValue != Wrap::Side || hasStackHistory())) { + && (wrap() != Wrap::Side || hasStackHistory())) { addTopBarMenuButton(); addProfileCallsButton(); - } else if (section.type() == Section::Type::Settings - && (section.settingsType() - == ::Settings::CloudPasswordEmailConfirmId() - || section.settingsType() == ::Settings::Main::Id() - || section.settingsType() == ::Settings::Chat::Id())) { + } else if (section.type() == Section::Type::Settings) { addTopBarMenuButton(); } else if (section.type() == Section::Type::Downloads) { auto &manager = Core::App().downloadManager(); @@ -399,11 +407,6 @@ void WrapWidget::createTopBar() { } }, _topBar->lifetime()); } - - _topBar->lower(); - _topBar->resizeToWidth(width()); - _topBar->finishAnimating(); - _topBar->show(); } void WrapWidget::checkBeforeClose(Fn<void()> close) { @@ -413,11 +416,12 @@ void WrapWidget::checkBeforeClose(Fn<void()> close) { void WrapWidget::addTopBarMenuButton() { Expects(_topBar != nullptr); + Expects(_content != nullptr); { const auto guard = gsl::finally([&] { _topBarMenu = nullptr; }); showTopBarMenu(true); - if (_topBarMenu->empty()) { + if (!_topBarMenu) { return; } } @@ -486,65 +490,19 @@ void WrapWidget::showTopBarMenu(bool check) { } }); - const auto addAction = Ui::Menu::CreateAddActionCallback(_topBarMenu); - if (key().isDownloads()) { - addAction( - tr::lng_context_delete_all_files(tr::now), - [=] { deleteAllDownloads(); }, - &st::menuIconDelete); - } else if (const auto peer = key().peer()) { - const auto topic = key().topic(); - Window::FillDialogsEntryMenu( - _controller->parentController(), - Dialogs::EntryState{ - .key = (topic - ? Dialogs::Key{ topic } - : Dialogs::Key{ peer->owner().history(peer) }), - .section = Dialogs::EntryState::Section::Profile, - }, - addAction); - } else if (const auto self = key().settingsSelf()) { - const auto showOther = [=](::Settings::Type type) { - const auto controller = _controller.get(); - _topBarMenu = nullptr; - controller->showSettings(type); - }; - ::Settings::FillMenu( - _controller->parentController(), - _controller->section().settingsType(), - showOther, - addAction); - } else { + _content->fillTopBarMenu(Ui::Menu::CreateAddActionCallback(_topBarMenu)); + if (_topBarMenu->empty()) { _topBarMenu = nullptr; return; + } else if (check) { + return; } _topBarMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - if (check) { - return; - } _topBarMenuToggle->setForceRippled(true); _topBarMenu->popup(_topBarMenuToggle->mapToGlobal( st::infoLayerTopBarMenuPosition)); } -void WrapWidget::deleteAllDownloads() { - auto &manager = Core::App().downloadManager(); - const auto phrase = tr::lng_downloads_delete_sure_all(tr::now); - const auto added = manager.loadedHasNonCloudFile() - ? QString() - : tr::lng_downloads_delete_in_cloud(tr::now); - const auto deleteSure = [=, &manager](Fn<void()> close) { - Ui::PostponeCall(this, close); - manager.deleteAll(); - }; - _controller->parentController()->show(Ui::MakeConfirmBox({ - .text = phrase + (added.isEmpty() ? QString() : "\n\n" + added), - .confirmed = deleteSure, - .confirmText = tr::lng_box_delete(tr::now), - .confirmStyle = &st::attentionBoxButton, - })); -} - bool WrapWidget::requireTopBarSearch() const { if (!_topBar || !_controller->searchFieldController()) { return false; @@ -619,6 +577,7 @@ void WrapWidget::showContent(object_ptr<ContentWidget> content) { } void WrapWidget::finishShowContent() { + setupTopBarMenuToggle(); updateContentGeometry(); _content->setIsStackBottom(!hasStackHistory()); if (_topBar) { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 0759c0729..db76d4b43 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -172,6 +172,7 @@ private: not_null<ContentMemento*> memento, const Window::SectionShow ¶ms); void setupTop(); + void setupTopBarMenuToggle(); void createTopBar(); void highlightTopBar(); void setupShortcuts(); @@ -202,7 +203,6 @@ private: void addTopBarMenuButton(); void addProfileCallsButton(); void showTopBarMenu(bool check); - void deleteAllDownloads(); rpl::variable<Wrap> _wrap; std::unique_ptr<Controller> _controller; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 85878baf1..3524be661 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -256,6 +256,10 @@ void Widget::selectionAction(SelectionAction action) { _inner->selectionAction(action); } +void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + _inner->fillTopBarMenu(addAction); +} + void Widget::saveState(not_null<Memento*> memento) { memento->setScrollTop(scrollTopSave()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index 7ab7968d5..09fca4419 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -82,6 +82,7 @@ public: rpl::producer<SelectedItems> selectedListValue() const override; void selectionAction(SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; private: void saveState(not_null<Memento*> memento); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 180145716..69ac74bee 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -38,13 +38,11 @@ public: ~AwayMessage(); [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: void setupContent(not_null<Window::SessionController*> controller); void save(); - rpl::event_stream<Type> _showOther; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<Data::AwaySchedule> _schedule; rpl::variable<bool> _enabled; @@ -201,10 +199,6 @@ rpl::producer<QString> AwayMessage::title() { return tr::lng_away_title(); } -rpl::producer<Type> AwayMessage::sectionShowOther() { - return _showOther.events(); -} - void AwayMessage::setupContent( not_null<Window::SessionController*> controller) { using namespace Data; @@ -268,7 +262,7 @@ void AwayMessage::setupContent( create->setClickedCallback([=] { const auto owner = &controller->session().data(); const auto id = owner->shortcutMessages().emplaceShortcut("away"); - _showOther.fire(ShortcutMessagesId(id)); + showOther(ShortcutMessagesId(id)); }); Ui::AddSkip(createInner); Ui::AddDivider(createInner); diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 81580801c..96b2615c4 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -41,7 +41,6 @@ public: ~Greeting(); [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; const Ui::RoundRect *bottomSkipRounding() const { return &_bottomSkipRounding; @@ -53,7 +52,6 @@ private: Ui::RoundRect _bottomSkipRounding; - rpl::event_stream<Type> _showOther; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<int> _noActivityDays; rpl::variable<bool> _enabled; @@ -177,10 +175,6 @@ rpl::producer<QString> Greeting::title() { return tr::lng_greeting_title(); } -rpl::producer<Type> Greeting::sectionShowOther() { - return _showOther.events(); -} - void Greeting::setupContent( not_null<Window::SessionController*> controller) { using namespace rpl::mappers; @@ -251,7 +245,7 @@ void Greeting::setupContent( create->setClickedCallback([=] { const auto owner = &controller->session().data(); const auto id = owner->shortcutMessages().emplaceShortcut("hello"); - _showOther.fire(ShortcutMessagesId(id)); + showOther(ShortcutMessagesId(id)); }); Ui::AddSkip(createInner); Ui::AddDivider(createInner); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index e13f822a2..431dcd4e1 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -37,14 +37,11 @@ public: ~QuickReplies(); [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: void setupContent(not_null<Window::SessionController*> controller); void save(); - rpl::event_stream<Type> _showOther; - }; QuickReplies::QuickReplies( @@ -64,10 +61,6 @@ rpl::producer<QString> QuickReplies::title() { return tr::lng_replies_title(); } -rpl::producer<Type> QuickReplies::sectionShowOther() { - return _showOther.events(); -} - void QuickReplies::setupContent( not_null<Window::SessionController*> controller) { using namespace rpl::mappers; @@ -94,41 +87,13 @@ void QuickReplies::setupContent( const auto messages = &owner->shortcutMessages(); add->setClickedCallback([=] { - controller->show(Box([=](not_null<Ui::GenericBox*> box) { - box->setTitle(tr::lng_replies_add_title()); - box->addRow(object_ptr<Ui::FlatLabel>( - box, - tr::lng_replies_add_shortcut(), - st::settingsAddReplyLabel)); - const auto field = box->addRow(object_ptr<Ui::InputField>( - box, - st::settingsAddReplyField, - tr::lng_replies_add_placeholder(), - QString())); - box->setFocusCallback([=] { - field->setFocusFast(); - }); - - const auto submit = [=] { - const auto weak = Ui::MakeWeak(box); - const auto name = field->getLastText().trimmed(); - if (name.isEmpty()) { - field->showError(); - } else { - const auto id = messages->emplaceShortcut(name); - _showOther.fire(ShortcutMessagesId(id)); - } - if (const auto strong = weak.data()) { - strong->closeBox(); - } - }; - field->submits( - ) | rpl::start_with_next(submit, field->lifetime()); - box->addButton(tr::lng_settings_save(), submit); - box->addButton(tr::lng_cancel(), [=] { - box->closeBox(); - }); - })); + const auto submit = [=](QString name, Fn<void()> close) { + const auto id = messages->emplaceShortcut(name); + showOther(ShortcutMessagesId(id)); + close(); + }; + controller->show( + Box(EditShortcutNameBox, QString(), crl::guard(this, submit))); }); Ui::AddSkip(content); @@ -140,24 +105,33 @@ void QuickReplies::setupContent( rpl::single(rpl::empty) | rpl::then( messages->shortcutsChanged() ) | rpl::start_with_next([=] { - while (inner->count()) { - delete inner->widgetAt(0); - } + auto old = inner->count(); + const auto &shortcuts = messages->shortcuts(); auto i = 0; - for (const auto &shortcut : shortcuts.list) { - const auto name = shortcut.second.name; + for (const auto &[_, shortcut] : shortcuts.list) { + if (!shortcut.count) { + continue; + } + const auto name = shortcut.name; AddButtonWithLabel( inner, rpl::single('/' + name), tr::lng_forum_messages( lt_count, - rpl::single(1. * shortcut.second.count)), + rpl::single(1. * shortcut.count)), st::settingsButtonNoIcon )->setClickedCallback([=] { const auto id = messages->emplaceShortcut(name); - _showOther.fire(ShortcutMessagesId(id)); + showOther(ShortcutMessagesId(id)); }); + if (old) { + delete inner->widgetAt(0); + --old; + } + } + while (old--) { + delete inner->widgetAt(0); } }, content->lifetime()); @@ -173,4 +147,48 @@ Type QuickRepliesId() { return QuickReplies::Id(); } +void EditShortcutNameBox( + not_null<Ui::GenericBox*> box, + QString name, + Fn<void(QString, Fn<void()>)> submit) { + name = name.trimmed(); + const auto editing = !name.isEmpty(); + box->setTitle(editing + ? tr::lng_replies_edit_title() + : tr::lng_replies_add_title()); + box->addRow(object_ptr<Ui::FlatLabel>( + box, + (editing + ? tr::lng_replies_edit_about() + : tr::lng_replies_add_shortcut()), + st::settingsAddReplyLabel)); + const auto field = box->addRow(object_ptr<Ui::InputField>( + box, + st::settingsAddReplyField, + tr::lng_replies_add_placeholder(), + name)); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto callback = [=] { + const auto name = field->getLastText().trimmed(); + if (name.isEmpty()) { + field->showError(); + } else { + submit(name, [weak = Ui::MakeWeak(box)] { + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + } + }; + field->submits( + ) | rpl::start_with_next(callback, field->lifetime()); + box->addButton(tr::lng_settings_save(), callback); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.h b/Telegram/SourceFiles/settings/business/settings_quick_replies.h index 80cc2f129..4765c4f59 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.h +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.h @@ -9,8 +9,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" +namespace Ui { +class GenericBox; +} // namespace Ui + namespace Settings { [[nodiscard]] Type QuickRepliesId(); +void EditShortcutNameBox( + not_null<Ui::GenericBox*> box, + QString name, + Fn<void(QString, Fn<void()>)> submit); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index e2186a172..0681b078a 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -36,17 +36,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_numbers_animation.h" #include "main/main_session.h" #include "menu/menu_send.h" +#include "settings/business/settings_quick_replies.h" #include "settings/business/settings_recipients_helper.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/storage_media_prepare.h" #include "storage/storage_shared_media.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/controls/jump_down_button.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/scroll_area.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" @@ -54,6 +57,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_chat.h" +#include "styles/style_menu_icons.h" +#include "styles/style_layers.h" namespace Settings { namespace { @@ -84,6 +89,7 @@ public: rpl::producer<Info::SelectedItems> selectedListValue() override; void selectionAction(Info::SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; bool paintOuter( not_null<QWidget*> outer, @@ -268,9 +274,9 @@ private: }; -struct Factory : AbstractSectionFactory { +struct Factory final : AbstractSectionFactory { explicit Factory(BusinessShortcutId shortcutId) - : shortcutId(shortcutId) { + : shortcutId(shortcutId) { } object_ptr<AbstractSection> create( @@ -426,6 +432,56 @@ void ShortcutMessages::selectionAction(Info::SelectionAction action) { Unexpected("Action in ShortcutMessages::selectionAction."); } +void ShortcutMessages::fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + const auto owner = &_controller->session().data(); + const auto messages = &owner->shortcutMessages(); + + addAction(tr::lng_context_edit_shortcut(tr::now), [=] { + const auto submit = [=](QString name, Fn<void()> close) { + const auto id = _shortcutId.current(); + const auto error = [=](QString text) { + if (!text.isEmpty()) { + _controller->showToast((text == u"SHORTCUT_OCCUPIED"_q) + ? tr::lng_replies_error_occupied(tr::now) + : text); + } + }; + messages->editShortcut(id, name, close, crl::guard(this, error)); + }; + const auto name = _shortcut.current(); + _controller->show( + Box(EditShortcutNameBox, name, crl::guard(this, submit))); + }, &st::menuIconEdit); + + const auto justDelete = crl::guard(this, [=] { + messages->removeShortcut(_shortcutId.current()); + }); + const auto confirmDeleteShortcut = [=] { + const auto slice = messages->list(_shortcutId.current()); + if (slice.fullCount == 0) { + justDelete(); + } else { + const auto confirmed = [=](Fn<void()> close) { + justDelete(); + close(); + }; + _controller->show(Ui::MakeConfirmBox({ + .text = { tr::lng_replies_delete_sure() }, + .confirmed = confirmed, + .confirmText = tr::lng_box_delete(), + .confirmStyle = &st::attentionBoxButton, + })); + } + }; + addAction({ + .text = tr::lng_context_delete_shortcut(tr::now), + .handler = crl::guard(this, confirmDeleteShortcut), + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); +} + bool ShortcutMessages::paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp index 60596c5bf..19bb96969 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/cloud_password/settings_cloud_password_email_confirm.h" +#include "apiwrap.h" #include "api/api_cloud_password.h" #include "base/unixtime.h" #include "core/core_cloud_password.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/cloud_password/settings_cloud_password_email.h" #include "settings/cloud_password/settings_cloud_password_hint.h" @@ -20,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/boxes/confirm_box.h" #include "ui/text/format_values.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/buttons.h" #include "ui/widgets/sent_code_field.h" #include "ui/wrap/padding_wrap.h" @@ -27,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" /* @@ -55,6 +59,10 @@ public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer<QString> title() override; + + [[nodiscard]] void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; + void setupContent(); protected: @@ -69,6 +77,20 @@ rpl::producer<QString> EmailConfirm::title() { return tr::lng_settings_cloud_password_email_title(); } +void EmailConfirm::fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + const auto api = &controller()->session().api(); + if (const auto state = api->cloudPassword().stateCurrent()) { + if (state->unconfirmedPattern.isEmpty()) { + return; + } + } + addAction( + tr::lng_settings_password_abort(tr::now), + [=] { api->cloudPassword().clearUnconfirmedPassword(); }, + &st::menuIconCancel); +} + rpl::producer<std::vector<Type>> EmailConfirm::removeTypes() { return rpl::single(std::vector<Type>{ CloudPasswordStartId(), diff --git a/Telegram/SourceFiles/settings/settings_advanced.cpp b/Telegram/SourceFiles/settings/settings_advanced.cpp index d23b2a6d6..b833383f5 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced.cpp @@ -978,10 +978,6 @@ rpl::producer<QString> Advanced::title() { return tr::lng_settings_advanced(); } -rpl::producer<Type> Advanced::sectionShowOther() { - return _showOther.events(); -} - void Advanced::setupContent(not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); @@ -1033,9 +1029,7 @@ void Advanced::setupContent(not_null<Window::SessionController*> controller) { AddSkip(content); AddDivider(content); AddSkip(content); - SetupExport(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupExport(controller, content, showOtherMethod()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_advanced.h b/Telegram/SourceFiles/settings/settings_advanced.h index fce804253..1d46797b4 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.h +++ b/Telegram/SourceFiles/settings/settings_advanced.h @@ -54,13 +54,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index e3aa43129..0881925b5 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -296,7 +296,6 @@ public: void setStepDataReference(std::any &data) override; [[nodiscard]] rpl::producer<> sectionShowBack() override final; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; private: void setupContent(); @@ -311,8 +310,6 @@ private: Fn<void(bool)> _setPaused; std::shared_ptr<Ui::RadiobuttonGroup> _radioGroup; - rpl::event_stream<Type> _showOther; - rpl::event_stream<> _showBack; rpl::event_stream<> _showFinished; rpl::variable<QString> _buttonText; @@ -341,10 +338,6 @@ rpl::producer<> Business::sectionShowBack() { return _showBack.events(); } -rpl::producer<Type> Business::sectionShowOther() { - return _showOther.events(); -} - void Business::setStepDataReference(std::any &data) { using namespace Info::Settings; const auto my = std::any_cast<SectionCustomTopBarData>(&data); @@ -365,7 +358,7 @@ void Business::setupContent() { Ui::AddSkip(content, st::settingsFromFileTop); AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { - _showOther.fire([&] { + showOther([&] { switch (feature) { case BusinessFeature::AwayMessages: return AwayMessageId(); case BusinessFeature::OpeningHours: return WorkingHoursId(); diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 3da8d85db..d4f1e8b7e 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "history/view/history_view_quick_action.h" #include "lang/lang_keys.h" #include "export/export_manager.h" @@ -1732,7 +1733,8 @@ void SetupSupport( } Chat::Chat(QWidget *parent, not_null<Window::SessionController*> controller) -: Section(parent) { +: Section(parent) +, _controller(controller) { setupContent(controller); } @@ -1740,6 +1742,14 @@ rpl::producer<QString> Chat::title() { return tr::lng_settings_section_chat_settings(); } +void Chat::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto window = &_controller->window(); + addAction( + tr::lng_settings_bg_theme_create(tr::now), + [=] { window->show(Box(Window::Theme::CreateBox, window)); }, + &st::menuIconChangeColors); +} + void Chat::setupContent(not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); diff --git a/Telegram/SourceFiles/settings/settings_chat.h b/Telegram/SourceFiles/settings/settings_chat.h index 91cd93101..d1256de96 100644 --- a/Telegram/SourceFiles/settings/settings_chat.h +++ b/Telegram/SourceFiles/settings/settings_chat.h @@ -44,9 +44,14 @@ public: [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; + private: void setupContent(not_null<Window::SessionController*> controller); + const not_null<Window::SessionController*> _controller; + }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index e401ad419..47bf52774 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -102,6 +102,9 @@ public: } virtual void selectionAction(Info::SelectionAction action) { } + [[nodiscard]] virtual void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + } virtual bool paintOuter( not_null<QWidget*> outer, diff --git a/Telegram/SourceFiles/settings/settings_common_session.cpp b/Telegram/SourceFiles/settings/settings_common_session.cpp index 047c2c622..8fac1a399 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.cpp +++ b/Telegram/SourceFiles/settings/settings_common_session.cpp @@ -34,48 +34,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { -void FillMenu( - not_null<Window::SessionController*> controller, - Type type, - Fn<void(Type)> showOther, - Ui::Menu::MenuCallback addAction) { - const auto window = &controller->window(); - if (type == Chat::Id()) { - addAction( - tr::lng_settings_bg_theme_create(tr::now), - [=] { window->show(Box(Window::Theme::CreateBox, window)); }, - &st::menuIconChangeColors); - } else if (type == CloudPasswordEmailConfirmId()) { - const auto api = &controller->session().api(); - if (const auto state = api->cloudPassword().stateCurrent()) { - if (state->unconfirmedPattern.isEmpty()) { - return; - } - } - addAction( - tr::lng_settings_password_abort(tr::now), - [=] { api->cloudPassword().clearUnconfirmedPassword(); }, - &st::menuIconCancel); - } else { - const auto &list = Core::App().domain().accounts(); - if (list.size() < Core::App().domain().maxAccounts()) { - addAction(tr::lng_menu_add_account(tr::now), [=] { - Core::App().domain().addActivated(MTP::Environment{}); - }, &st::menuIconAddAccount); - } - if (!controller->session().supportMode()) { - addAction( - tr::lng_settings_information(tr::now), - [=] { showOther(Information::Id()); }, - &st::menuIconInfo); - } - addAction({ - .text = tr::lng_settings_logout(tr::now), - .handler = [=] { window->showLogoutConfirmation(); }, - .icon = &st::menuIconLeaveAttention, - .isAttention = true, - }); - } +bool HasMenu(Type type) { + return (type == ::Settings::CloudPasswordEmailConfirmId()) + || (type == Main::Id()) + || (type == Chat::Id()); } } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 16a03b9e4..2ebd1319c 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -54,6 +54,7 @@ struct SectionFactory : AbstractSectionFactory { static const auto result = std::make_shared<SectionFactory>(); return result; } + }; template <typename SectionType> @@ -67,12 +68,24 @@ public: [[nodiscard]] Type id() const final override { return Id(); } + + [[nodiscard]] rpl::producer<Type> sectionShowOther() final override { + return _showOtherRequests.events(); + } + [[nodiscard]] void showOther(Type type) { + _showOtherRequests.fire_copy(type); + } + [[nodiscard]] Fn<void(Type)> showOtherMethod() { + return crl::guard(this, [=](Type type) { + showOther(type); + }); + } + +private: + rpl::event_stream<Type> _showOtherRequests; + }; -void FillMenu( - not_null<Window::SessionController*> controller, - Type type, - Fn<void(Type)> showOther, - Ui::Menu::MenuCallback addAction); +bool HasMenu(Type type); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp index d06726cb4..9a766f16d 100644 --- a/Telegram/SourceFiles/settings/settings_local_passcode.cpp +++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp @@ -383,7 +383,6 @@ public: [[nodiscard]] rpl::producer<QString> title() override; void showFinished() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; [[nodiscard]] rpl::producer<std::vector<Type>> removeFromStack() override; @@ -399,7 +398,6 @@ private: rpl::variable<bool> _isBottomFillerShown; rpl::event_stream<> _showFinished; - rpl::event_stream<Type> _showOther; rpl::event_stream<> _showBack; }; @@ -445,7 +443,7 @@ void LocalPasscodeManage::setupContent() { st::settingsButton, { &st::menuIconLock } )->addClickHandler([=] { - _showOther.fire(LocalPasscodeChange::Id()); + showOther(LocalPasscodeChange::Id()); }); auto autolockLabel = state->autoLockBoxClosing.events_starting_with( @@ -542,10 +540,6 @@ void LocalPasscodeManage::showFinished() { _showFinished.fire({}); } -rpl::producer<Type> LocalPasscodeManage::sectionShowOther() { - return _showOther.events(); -} - rpl::producer<> LocalPasscodeManage::sectionShowBack() { return _showBack.events(); } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 854c94b05..4dd57db8f 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "core/application.h" #include "settings/settings_business.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" @@ -28,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/padding_wrap.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/labels.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" @@ -49,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" +#include "main/main_domain.h" #include "main/main_app_config.h" #include "apiwrap.h" #include "api/api_peer_photo.h" @@ -691,6 +694,28 @@ rpl::producer<QString> Main::title() { return tr::lng_menu_settings(); } +void Main::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto &list = Core::App().domain().accounts(); + if (list.size() < Core::App().domain().maxAccounts()) { + addAction(tr::lng_menu_add_account(tr::now), [=] { + Core::App().domain().addActivated(MTP::Environment{}); + }, &st::menuIconAddAccount); + } + if (!_controller->session().supportMode()) { + addAction( + tr::lng_settings_information(tr::now), + [=] { showOther(Information::Id()); }, + &st::menuIconInfo); + } + const auto window = &_controller->window(); + addAction({ + .text = tr::lng_settings_logout(tr::now), + .handler = [=] { window->showLogoutConfirmation(); }, + .icon = &st::menuIconLeaveAttention, + .isAttention = true, + }); +} + void Main::keyPressEvent(QKeyEvent *e) { crl::on_main(this, [=, text = e->text()]{ CodesFeedString(_controller, text); @@ -706,18 +731,14 @@ void Main::setupContent(not_null<Window::SessionController*> controller) { controller, controller->session().user())); - SetupSections(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupSections(controller, content, showOtherMethod()); if (HasInterfaceScale()) { Ui::AddDivider(content); Ui::AddSkip(content); SetupInterfaceScale(&controller->window(), content); Ui::AddSkip(content); } - SetupPremium(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupPremium(controller, content, showOtherMethod()); SetupHelp(controller, content); Ui::ResizeFitChild(this, content); @@ -730,8 +751,4 @@ void Main::setupContent(not_null<Window::SessionController*> controller) { controller->session().data().cloudThemes().refresh(); } -rpl::producer<Type> Main::sectionShowOther() { - return _showOther.events(); -} - } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index e3d26d02b..4356a49b3 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -38,7 +38,8 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; + [[nodiscard]] void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; protected: void keyPressEvent(QKeyEvent *e) override; @@ -47,7 +48,6 @@ private: void setupContent(not_null<Window::SessionController*> controller); const not_null<Window::SessionController*> _controller; - rpl::event_stream<Type> _showOther; }; diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index 4f54017c9..ddfb849f8 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -1281,17 +1281,11 @@ rpl::producer<QString> Notifications::title() { return tr::lng_settings_section_notify(); } -rpl::producer<Type> Notifications::sectionShowOther() { - return _showOther.events(); -} - void Notifications::setupContent( not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - SetupNotifications(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupNotifications(controller, content, showOtherMethod()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_notifications.h b/Telegram/SourceFiles/settings/settings_notifications.h index 1911a48af..f6df4d2e5 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.h +++ b/Telegram/SourceFiles/settings/settings_notifications.h @@ -19,13 +19,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index ed4894275..6d16cac01 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -968,10 +968,6 @@ rpl::producer<QString> PrivacySecurity::title() { return tr::lng_settings_section_privacy(); } -rpl::producer<Type> PrivacySecurity::sectionShowOther() { - return _showOther.events(); -} - void PrivacySecurity::setupContent( not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); @@ -982,9 +978,7 @@ void PrivacySecurity::setupContent( return rpl::duplicate(updateOnTick); }; - SetupSecurity(controller, content, trigger(), [=](Type type) { - _showOther.fire_copy(type); - }); + SetupSecurity(controller, content, trigger(), showOtherMethod()); SetupPrivacy(controller, content, trigger()); #if !defined OS_MAC_STORE && !defined OS_WIN_STORE SetupSensitiveContent(controller, content, trigger()); diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.h b/Telegram/SourceFiles/settings/settings_privacy_security.h index 4fedfce81..01922b01e 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.h +++ b/Telegram/SourceFiles/settings/settings_privacy_security.h @@ -47,13 +47,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings From 8545a14763040d183eb934b371dabf2101bbb88c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 Feb 2024 14:41:17 +0400 Subject: [PATCH 059/108] Fix shortcut messages sizing / emoji panel. --- Telegram/SourceFiles/api/api_editing.cpp | 5 +- .../SourceFiles/boxes/edit_caption_box.cpp | 1 + .../chat_helpers/compose/compose_features.h | 1 + .../data/business/data_shortcut_messages.cpp | 3 +- Telegram/SourceFiles/data/data_drafts.h | 13 +++ Telegram/SourceFiles/dialogs/dialogs_key.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 2 +- .../history_view_compose_controls.cpp | 45 +++++--- .../controls/history_view_compose_controls.h | 8 ++ .../history/view/history_view_bottom_info.cpp | 2 +- .../view/history_view_context_menu.cpp | 4 +- .../history/view/history_view_list_widget.cpp | 27 ++++- .../history/view/history_view_list_widget.h | 9 ++ .../SourceFiles/info/info_content_widget.cpp | 12 ++- .../SourceFiles/info/info_layer_widget.cpp | 100 ++++++++++++------ Telegram/SourceFiles/info/info_layer_widget.h | 4 +- .../SourceFiles/info/info_section_widget.cpp | 2 +- .../info/settings/info_settings_widget.cpp | 8 +- .../business/settings_shortcut_messages.cpp | 86 +++++++++++---- .../SourceFiles/storage/localimageloader.cpp | 1 + Telegram/SourceFiles/ui/chat/chat.style | 1 + 21 files changed, 253 insertions(+), 82 deletions(-) diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index a1e168adf..84f0cbfff 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -89,6 +89,9 @@ mtpRequestId EditMessage( : emptyFlag) | (options.scheduled ? MTPmessages_EditMessage::Flag::f_schedule_date + : emptyFlag) + | (item->isBusinessShortcut() + ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id : emptyFlag); const auto id = item->isScheduled() @@ -105,7 +108,7 @@ mtpRequestId EditMessage( MTPReplyMarkup(), sentEntities, MTP_int(options.scheduled), - MTPint() // quick_reply_shortcut_id + MTP_int(item->shortcutId()) )).done([=]( const MTPUpdates &result, [[maybe_unused]] mtpRequestId requestId) { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 8e94beeb9..0c4b212c5 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -895,6 +895,7 @@ void EditCaptionBox::save() { auto options = Api::SendOptions(); options.scheduled = item->isScheduled() ? item->date() : 0; + options.shortcutId = item->shortcutId(); if (!_preparedList.files.empty()) { if ((_albumType != Ui::AlbumType::None) diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h index ba6f43b4e..5466b34e9 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -23,6 +23,7 @@ struct ComposeFeatures { bool autocompleteHashtags = true; bool autocompleteMentions = true; bool autocompleteCommands = true; + bool commonTabbedPanel = true; }; } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index d9d75ec52..e7b4a65a7 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -60,7 +60,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_int(data.vttl_period().value_or_empty())); }, [&](const MTPDmessage &data) { return MTP_message( - MTP_flags(data.vflags().v | MTPDmessage::Flag::f_quick_reply_shortcut_id), + MTP_flags(data.vflags().v + | MTPDmessage::Flag::f_quick_reply_shortcut_id), data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), MTPint(), // from_boosts_applied diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 3f5e22a23..4fdd9159c 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -96,6 +96,18 @@ public: [[nodiscard]] static constexpr DraftKey ScheduledEdit() { return kScheduledDraftIndex + kEditDraftShift; } + [[nodiscard]] static constexpr DraftKey Shortcut( + BusinessShortcutId shortcutId) { + return (shortcutId < 0 || shortcutId >= ServerMaxMsgId) + ? None() + : (kShortcutDraftShift + shortcutId); + } + [[nodiscard]] static constexpr DraftKey ShortcutEdit( + BusinessShortcutId shortcutId) { + return (shortcutId < 0 || shortcutId >= ServerMaxMsgId) + ? None() + : (kShortcutDraftShift + kEditDraftShift + shortcutId); + } [[nodiscard]] static constexpr DraftKey FromSerialized(qint64 value) { return value; @@ -156,6 +168,7 @@ private: static constexpr auto kScheduledDraftIndex = -3; static constexpr auto kEditDraftShift = ServerMaxMsgId.bare; static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare; + static constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare; static constexpr auto kEditDraftShiftOld = 0x3FFF'FFFF; int64 _value = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 2396b216f..c43dc9f15 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -108,6 +108,7 @@ struct EntryState { Replies, SavedSublist, ContextMenu, + ShortcutMessages, }; Key key; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 807563fc0..b55baa6d2 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2108,7 +2108,7 @@ bool HistoryItem::allowsEdit(TimeId now) const { } bool HistoryItem::canBeEdited() const { - if ((!isRegular() && !isScheduled()) + if ((!isRegular() && !isScheduled() && !isBusinessShortcut()) || Has<HistoryMessageVia>() || Has<HistoryMessageForwarded>()) { return false; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 69f0f2de8..9cfec19f5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -114,6 +114,9 @@ using ForwardPanel = Controls::ForwardPanel; } // namespace +const ChatHelpers::PauseReason kDefaultPanelsLevel + = ChatHelpers::PauseReason::TabbedPanel; + class FieldHeader final : public Ui::RpWidget { public: FieldHeader( @@ -760,7 +763,10 @@ MessageToEdit FieldHeader::queryToEdit() { } return { .fullId = item->fullId(), - .options = { .scheduled = item->isScheduled() ? item->date() : 0 }, + .options = { + .scheduled = item->isScheduled() ? item->date() : 0, + .shortcutId = item->shortcutId(), + }, }; } @@ -788,21 +794,24 @@ ComposeControls::ComposeControls( : st::defaultComposeControls) , _features(descriptor.features) , _parent(parent) +, _panelsParent(descriptor.panelsParent + ? descriptor.panelsParent + : _parent.get()) , _show(std::move(descriptor.show)) , _session(&_show->session()) , _regularWindow(descriptor.regularWindow) -, _ownedSelector(_regularWindow +, _ownedSelector((_regularWindow && _features.commonTabbedPanel) ? nullptr : std::make_unique<ChatHelpers::TabbedSelector>( - _parent, + _panelsParent, ChatHelpers::TabbedSelectorDescriptor{ .show = _show, .st = _st.tabbed, - .level = Window::GifPauseReason::TabbedPanel, + .level = descriptor.panelsLevel, .mode = ChatHelpers::TabbedSelector::Mode::Full, .features = _features, })) -, _selector(_regularWindow +, _selector((_regularWindow && _features.commonTabbedPanel) ? _regularWindow->tabbedSelector() : not_null(_ownedSelector.get())) , _mode(descriptor.mode) @@ -876,6 +885,12 @@ void ComposeControls::updateTopicRootId(MsgId topicRootId) { _header->updateTopicRootId(_topicRootId); } +void ComposeControls::updateShortcutId(BusinessShortcutId shortcutId) { + unregisterDraftSources(); + _shortcutId = shortcutId; + registerDraftSource(); +} + void ComposeControls::setHistory(SetHistoryArgs &&args) { _showSlowmodeError = std::move(args.showSlowmodeError); _sendActionFactory = std::move(args.sendActionFactory); @@ -1592,7 +1607,7 @@ void ComposeControls::initField() { && Data::AllowEmojiWithoutPremium(_history->peer, emoji); }; const auto suggestions = Ui::Emoji::SuggestionsController::Init( - _parent, + _panelsParent, _field, _session, { @@ -1828,6 +1843,10 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { return (type == DraftType::Edit) ? Key::ScheduledEdit() : Key::Scheduled(); + case Section::ShortcutMessages: + return (type == DraftType::Edit) + ? Key::ShortcutEdit(_shortcutId) + : Key::Shortcut(_shortcutId); } return Key::None(); } @@ -2053,7 +2072,9 @@ rpl::producer<SendActionUpdate> ComposeControls::sendActionUpdates() const { } void ComposeControls::initTabbedSelector() { - if (!_regularWindow || _regularWindow->hasTabbedSelectorOwnership()) { + if (!_regularWindow + || !_features.commonTabbedPanel + || _regularWindow->hasTabbedSelectorOwnership()) { createTabbedPanel(); } else { setTabbedPanel(nullptr); @@ -2686,7 +2707,7 @@ void ComposeControls::updateAttachBotsMenu() { return; } _attachBotsMenu = InlineBots::MakeAttachBotsMenu( - _parent, + _panelsParent, _regularWindow, _history->peer, _sendActionFactory, @@ -2729,7 +2750,7 @@ void ComposeControls::escape() { bool ComposeControls::pushTabbedSelectorToThirdSection( not_null<Data::Thread*> thread, const Window::SectionShow ¶ms) { - if (!_tabbedPanel || !_regularWindow) { + if (!_tabbedPanel || !_regularWindow || !_features.commonTabbedPanel) { return true; //} else if (!_canSendMessages) { // Core::App().settings().setTabbedReplacedWithInfo(true); @@ -2764,7 +2785,7 @@ void ComposeControls::createTabbedPanel() { .nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(), }; setTabbedPanel(std::make_unique<TabbedPanel>( - _parent, + _panelsParent, std::move(descriptor))); _tabbedPanel->setDesiredHeightValues( st::emojiPanHeightRatio, @@ -2787,7 +2808,7 @@ void ComposeControls::setTabbedPanel( } void ComposeControls::toggleTabbedSelectorMode() { - if (!_history || !_regularWindow) { + if (!_history || !_regularWindow || !_features.commonTabbedPanel) { return; } if (_tabbedPanel) { @@ -3248,7 +3269,7 @@ void ComposeControls::applyInlineBotQuery( } if (!_inlineResults) { _inlineResults = std::make_unique<InlineBots::Layout::Widget>( - _parent, + _panelsParent, _regularWindow); _inlineResults->setResultSelectedCallback([=]( InlineBots::ResultSelected result) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index d5985d6cb..d21ea9884 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -37,6 +37,7 @@ class TabbedSelector; struct FileChosen; struct PhotoChosen; class Show; +enum class PauseReason; } // namespace ChatHelpers namespace Data { @@ -95,6 +96,8 @@ enum class ComposeControlsMode { Scheduled, }; +extern const ChatHelpers::PauseReason kDefaultPanelsLevel; + struct ComposeControlsDescriptor { const style::ComposeControls *stOverride = nullptr; std::shared_ptr<ChatHelpers::Show> show; @@ -104,6 +107,8 @@ struct ComposeControlsDescriptor { Window::SessionController *regularWindow = nullptr; rpl::producer<ChatHelpers::FileChosen> stickerOrEmojiChosen; rpl::producer<QString> customPlaceholder; + QWidget *panelsParent = nullptr; + ChatHelpers::PauseReason panelsLevel = kDefaultPanelsLevel; QString voiceCustomCancelText; bool voiceLockFromBottom = false; ChatHelpers::ComposeFeatures features; @@ -137,6 +142,7 @@ public: [[nodiscard]] Main::Session &session() const; void setHistory(SetHistoryArgs &&args); void updateTopicRootId(MsgId topicRootId); + void updateShortcutId(BusinessShortcutId shortcutId); void setCurrentDialogsEntryState(Dialogs::EntryState state); [[nodiscard]] PeerData *sendAsPeer() const; @@ -343,6 +349,7 @@ private: const style::ComposeControls &_st; const ChatHelpers::ComposeFeatures _features; const not_null<QWidget*> _parent; + const not_null<QWidget*> _panelsParent; const std::shared_ptr<ChatHelpers::Show> _show; const not_null<Main::Session*> _session; @@ -353,6 +360,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + BusinessShortcutId _shortcutId = 0; Fn<bool()> _showSlowmodeError; Fn<Api::SendAction()> _sendActionFactory; rpl::variable<int> _slowmodeSecondsLeft; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index cfb31eabe..a13eb3fe5 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -551,7 +551,7 @@ void BottomInfo::layoutReactionsText() { QSize BottomInfo::countOptimalSize() { if (_data.flags & Data::Flag::Shortcut) { - return { st::historySendStateSpace / 2, st::msgDateFont->height }; + return { st::historyShortcutStateSpace, st::msgDateFont->height }; } auto width = 0; if (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 9267aeb5c..09c3b89f9 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -102,7 +102,9 @@ bool HasEditMessageAction( || item->hasFailed() || item->isEditingMedia() || !request.selectedItems.empty() - || (context != Context::History && context != Context::Replies)) { + || (context != Context::History + && context != Context::Replies + && context != Context::ShortcutMessages)) { return false; } const auto peer = item->history()->peer; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 31e6b4e9e..c37342fa2 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2166,6 +2166,21 @@ void ListWidget::paintEvent(QPaintEvent *e) { context.translate(0, top); p.translate(0, -top); + paintUserpics(p, context, clip); + paintDates(p, context, clip); + + _reactionsManager->paint(p, context); + _emojiInteractions->paint(p); +} + +void ListWidget::paintUserpics( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip) { + if (_context == Context::ShortcutMessages) { + return; + } + const auto session = &controller()->session(); enumerateUserpics([&](not_null<Element*> view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= clip.top() + clip.height()) { @@ -2210,6 +2225,15 @@ void ListWidget::paintEvent(QPaintEvent *e) { } return true; }); +} + +void ListWidget::paintDates( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip) { + if (_context == Context::ShortcutMessages) { + return; + } auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); @@ -2256,9 +2280,6 @@ void ListWidget::paintEvent(QPaintEvent *e) { } return true; }); - - _reactionsManager->paint(p, context); - _emojiInteractions->paint(p); } void ListWidget::maybeMarkReactionsRead(not_null<HistoryItem*> item) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 2e2fdbae7..c15a041b8 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -600,6 +600,15 @@ private: void showPremiumStickerTooltip( not_null<const HistoryView::Element*> view); + void paintUserpics( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip); + void paintDates( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip); + // This function finds all history items that are displayed and calls template method // for each found message (in given direction) in the passed history with passed top offset. // diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index adf4f22d2..440c125b2 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -167,7 +167,12 @@ Ui::RpWidget *ContentWidget::doSetInnerWidget( const auto bottom = top + height; _innerDesiredHeight = desired; _innerWrap->setVisibleTopBottom(top, bottom); + LOG(("TOP: %1, HEIGHT: %2, DESIRED: %3, TILL: %4").arg(top).arg(height).arg(desired).arg(std::max(desired - bottom, 0))); _scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0)); + //const auto bottom = _scroll->scrollTop() + _scroll->height(); + //_innerDesiredHeight = desired; + //_innerWrap->setVisibleTopBottom(_scroll->scrollTop(), bottom); + //_scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0)); }, _innerWrap->lifetime()); return _innerWrap->entity(); @@ -217,7 +222,12 @@ rpl::producer<int> ContentWidget::desiredHeightValue() const { _innerWrap->entity()->desiredHeightValue(), _scrollTopSkip.value(), _scrollBottomSkip.value() - ) | rpl::map(_1 + _2 + _3); + //) | rpl::map(_1 + _2 + _3); + ) | rpl::map([=](int desired, int, int) { + return desired + + _scrollTopSkip.current() + + _scrollBottomSkip.current(); + }); } rpl::producer<bool> ContentWidget::desiredShadowVisibility() const { diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index b4fdd2414..50eca4456 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -30,7 +30,7 @@ LayerWidget::LayerWidget( not_null<Window::SessionController*> controller, not_null<Memento*> memento) : _controller(controller) -, _content(this, controller, Wrap::Layer, memento) { +, _contentWrap(this, controller, Wrap::Layer, memento) { setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -39,7 +39,7 @@ LayerWidget::LayerWidget( not_null<Window::SessionController*> controller, not_null<MoveMemento*> memento) : _controller(controller) -, _content(memento->takeContent(this, Wrap::Layer)) { +, _contentWrap(memento->takeContent(this, Wrap::Layer)) { setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -64,17 +64,17 @@ void LayerWidget::floatPlayerToggleGifsPaused(bool paused) { auto LayerWidget::floatPlayerGetSection(Window::Column column) -> not_null<::Media::Player::FloatSectionDelegate*> { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - return _content; + return _contentWrap; } void LayerWidget::floatPlayerEnumerateSections(Fn<void( not_null<::Media::Player::FloatSectionDelegate*> widget, Window::Column widgetColumn)> callback) { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - callback(_content, Window::Column::Second); + callback(_contentWrap, Window::Column::Second); } bool LayerWidget::floatPlayerIsVisible(not_null<HistoryItem*> item) { @@ -87,9 +87,9 @@ void LayerWidget::floatPlayerDoubleClickEvent( } void LayerWidget::setupHeightConsumers() { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - _content->scrollTillBottomChanges( + _contentWrap->scrollTillBottomChanges( ) | rpl::filter([this] { if (!_inResize) { return true; @@ -100,10 +100,10 @@ void LayerWidget::setupHeightConsumers() { resizeToWidth(width()); }, lifetime()); - _content->grabbingForExpanding( + _contentWrap->grabbingForExpanding( ) | rpl::start_with_next([=](bool grabbing) { if (grabbing) { - _savedHeight = _contentHeight; + _savedHeight = _contentWrapHeight; _savedHeightAnimation = base::take(_heightAnimation); setContentHeight(_desiredHeight); } else { @@ -112,7 +112,7 @@ void LayerWidget::setupHeightConsumers() { } }, lifetime()); - _content->desiredHeightValue( + _contentWrap->desiredHeightValue( ) | rpl::start_with_next([this](int height) { if (!height) { // New content arrived. @@ -128,32 +128,32 @@ void LayerWidget::setupHeightConsumers() { _heightAnimated = true; _heightAnimation.start([=] { setContentHeight(_heightAnimation.value(_desiredHeight)); - }, _contentHeight, _desiredHeight, st::slideDuration); + }, _contentWrapHeight, _desiredHeight, st::slideDuration); resizeToWidth(width()); } }, lifetime()); } void LayerWidget::setContentHeight(int height) { - if (_contentHeight == height) { + if (_contentWrapHeight == height) { return; } - - _contentHeight = height; + LOG(("CONTENT WRAP HEIGHT: %1 -> %2").arg(_contentWrapHeight).arg(height)); + _contentWrapHeight = height; if (_inResize) { _pendingResize = true; - } else if (_content) { + } else if (_contentWrap) { resizeToWidth(width()); } } void LayerWidget::showFinished() { floatPlayerShowVisible(); - _content->showFast(); + _contentWrap->showFast(); } void LayerWidget::parentResized() { - if (!_content) { + if (!_contentWrap) { return; } @@ -163,7 +163,7 @@ void LayerWidget::parentResized() { Ui::FocusPersister persister(this); restoreFloatPlayerDelegate(); - auto memento = std::make_shared<MoveMemento>(std::move(_content)); + auto memento = std::make_shared<MoveMemento>(std::move(_contentWrap)); // We want to call hideSpecialLayer synchronously to avoid glitches, // but we can't destroy LayerStackWidget from its' resizeEvent, @@ -209,7 +209,7 @@ bool LayerWidget::takeToThirdSection() { // //Ui::FocusPersister persister(this); //auto localCopy = _controller; - //auto memento = MoveMemento(std::move(_content)); + //auto memento = MoveMemento(std::move(_contentWrap)); //localCopy->hideSpecialLayer(anim::type::instant); //// When creating third section in response to the window @@ -235,7 +235,7 @@ bool LayerWidget::takeToThirdSection() { bool LayerWidget::showSectionInternal( not_null<Window::SectionMemento*> memento, const Window::SectionShow ¶ms) { - if (_content && _content->showInternal(memento, params)) { + if (_contentWrap && _contentWrap->showInternal(memento, params)) { if (params.activation != anim::activation::background) { _controller->parentController()->hideLayer(); } @@ -245,7 +245,7 @@ bool LayerWidget::showSectionInternal( } bool LayerWidget::closeByOutsideClick() const { - return _content ? _content->closeByOutsideClick() : true; + return _contentWrap ? _contentWrap->closeByOutsideClick() : true; } int LayerWidget::MinimalSupportedWidth() { @@ -254,19 +254,48 @@ int LayerWidget::MinimalSupportedWidth() { } int LayerWidget::resizeGetHeight(int newWidth) { - if (!parentWidget() || !_content || !newWidth) { + if (!parentWidget() || !_contentWrap || !newWidth) { return 0; } constexpr auto kMaxAttempts = 16; auto attempts = 0; while (true) { _inResize = true; + { + const auto &parentSize = parentWidget()->size(); + const auto windowWidth = parentSize.width(); + const auto windowHeight = parentSize.height(); + const auto newLeft = (windowWidth - newWidth) / 2; + const auto newTop = std::clamp( + windowHeight / 24, + st::infoLayerTopMinimal, + st::infoLayerTopMaximal); + const auto newBottom = newTop; + + const auto bottomRadius = st::boxRadius; + const auto maxVisibleHeight = windowHeight - newTop; + // Top rounding is included in _contentWrapHeight. + auto desiredHeight = _contentWrapHeight + bottomRadius; + accumulate_min(desiredHeight, maxVisibleHeight - newBottom); + + // First resize content to new width and get the new desired height. + const auto contentLeft = 0; + const auto contentTop = 0; + const auto contentBottom = bottomRadius; + const auto contentWidth = newWidth; + auto contentHeight = desiredHeight - contentTop - contentBottom; + LOG(("ATTEMPT %1: WIDTH %2, WRAP HEIGHT %3, SCROLL TILL BOTTOM: %4" + ).arg(attempts + 1 + ).arg(newWidth + ).arg(_contentWrapHeight + ).arg(_contentWrap->scrollTillBottom(contentHeight))); + } const auto newGeometry = countGeometry(newWidth); _inResize = false; if (!_pendingResize) { const auto oldGeometry = geometry(); if (newGeometry != oldGeometry) { - _content->forceContentRepaint(); + _contentWrap->forceContentRepaint(); } if (newGeometry.topLeft() != oldGeometry.topLeft()) { move(newGeometry.topLeft()); @@ -292,8 +321,8 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto bottomRadius = st::boxRadius; const auto maxVisibleHeight = windowHeight - newTop; - // Top rounding is included in _contentHeight. - auto desiredHeight = _contentHeight + bottomRadius; + // Top rounding is included in _contentWrapHeight. + auto desiredHeight = _contentWrapHeight + bottomRadius; accumulate_min(desiredHeight, maxVisibleHeight - newBottom); // First resize content to new width and get the new desired height. @@ -302,10 +331,11 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto contentBottom = bottomRadius; const auto contentWidth = newWidth; auto contentHeight = desiredHeight - contentTop - contentBottom; - const auto scrollTillBottom = _content->scrollTillBottom(contentHeight); + const auto scrollTillBottom = _contentWrap->scrollTillBottom( + contentHeight); auto additionalScroll = std::min(scrollTillBottom, newBottom); - const auto expanding = (_desiredHeight > _contentHeight); + const auto expanding = (_desiredHeight > _contentWrapHeight); desiredHeight += additionalScroll; contentHeight += additionalScroll; @@ -313,11 +343,11 @@ QRect LayerWidget::countGeometry(int newWidth) { if (_tillBottom) { additionalScroll += contentBottom; } - _contentTillBottom = _tillBottom && !_content->scrollBottomSkip(); + _contentTillBottom = _tillBottom && !_contentWrap->scrollBottomSkip(); if (_contentTillBottom) { contentHeight += contentBottom; } - _content->updateGeometry({ + _contentWrap->updateGeometry({ contentLeft, contentTop, contentWidth, @@ -328,8 +358,8 @@ QRect LayerWidget::countGeometry(int newWidth) { } void LayerWidget::doSetInnerFocus() { - if (_content) { - _content->setInnerFocus(); + if (_contentWrap) { + _contentWrap->setInnerFocus(); } } @@ -342,7 +372,7 @@ void LayerWidget::paintEvent(QPaintEvent *e) { if (!_tillBottom) { const auto bottom = QRect{ 0, height() - radius, width(), radius }; if (clip.intersects(bottom)) { - if (const auto rounding = _content->bottomSkipRounding()) { + if (const auto rounding = _contentWrap->bottomSkipRounding()) { rounding->paint(p, rect(), RectPart::FullBottom); } else { Ui::FillRoundRect(p, bottom, st::boxBg, { @@ -351,11 +381,11 @@ void LayerWidget::paintEvent(QPaintEvent *e) { } } } else if (!_contentTillBottom) { - const auto rounding = _content->bottomSkipRounding(); + const auto rounding = _contentWrap->bottomSkipRounding(); const auto &color = rounding ? rounding->color() : st::boxBg; p.fillRect(0, height() - radius, width(), radius, color); } - if (_content->animatingShow()) { + if (_contentWrap->animatingShow()) { const auto top = QRect{ 0, 0, width(), radius }; if (clip.intersects(top)) { Ui::FillRoundRect(p, top, st::boxBg, { diff --git a/Telegram/SourceFiles/info/info_layer_widget.h b/Telegram/SourceFiles/info/info_layer_widget.h index c223b9d82..df8bc2726 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.h +++ b/Telegram/SourceFiles/info/info_layer_widget.h @@ -73,10 +73,10 @@ private: [[nodiscard]] QRect countGeometry(int newWidth); not_null<Window::SessionController*> _controller; - object_ptr<WrapWidget> _content; + object_ptr<WrapWidget> _contentWrap; int _desiredHeight = 0; - int _contentHeight = 0; + int _contentWrapHeight = 0; int _savedHeight = 0; Ui::Animations::Simple _heightAnimation; Ui::Animations::Simple _savedHeightAnimation; diff --git a/Telegram/SourceFiles/info/info_section_widget.cpp b/Telegram/SourceFiles/info/info_section_widget.cpp index a70131428..0a7b7da67 100644 --- a/Telegram/SourceFiles/info/info_section_widget.cpp +++ b/Telegram/SourceFiles/info/info_section_widget.cpp @@ -50,8 +50,8 @@ void SectionWidget::init() { return (_content != nullptr); }) | rpl::start_with_next([=](QSize size, int) { const auto expanding = false; - const auto additionalScroll = st::boxRadius; const auto full = !_content->scrollBottomSkip(); + const auto additionalScroll = (full ? st::boxRadius : 0); const auto height = size.height() - (full ? 0 : st::boxRadius); const auto wrapGeometry = QRect{ 0, 0, size.width(), height }; _content->updateGeometry( diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 3524be661..8a53dc553 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -117,17 +117,17 @@ Widget::Widget( } if (_pinnedToBottom) { - const auto processHeight = [=](int bottomHeight, int height) { - setScrollBottomSkip(bottomHeight); + const auto processHeight = [=] { + setScrollBottomSkip(_pinnedToBottom->height()); _pinnedToBottom->moveToLeft( _pinnedToBottom->x(), - height - bottomHeight); + height() - _pinnedToBottom->height()); }; _inner->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { _pinnedToBottom->resizeToWidth(s.width()); - processHeight(_pinnedToBottom->height(), height()); + //processHeight(); }, _pinnedToBottom->lifetime()); rpl::combine( diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 0681b078a..8df33ebf0 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -98,6 +98,7 @@ public: private: void outerResized(QSize outer); + void updateComposeControlsPosition(); // ListDelegate interface. Context listContext() override; @@ -505,7 +506,7 @@ void ShortcutMessages::outerResized(QSize outer) { ? base::make_optional(_scroll->scrollTop()) : 0; _skipScrollEvent = true; - _inner->resizeToWidth(contentWidth, _scroll->height()); + _inner->resizeToWidth(contentWidth, st::boxWidth); _skipScrollEvent = false; if (!_scroll->isHidden()) { @@ -514,25 +515,34 @@ void ShortcutMessages::outerResized(QSize outer) { } updateInnerVisibleArea(); } - _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); + updateComposeControlsPosition(); _cornerButtons.updatePositions(); } +void ShortcutMessages::updateComposeControlsPosition() { + const auto bottom = _scroll->parentWidget()->height(); + const auto controlsHeight = _composeControls->heightCurrent(); + _composeControls->move(0, bottom - controlsHeight + st::boxRadius); + _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); +} + void ShortcutMessages::setupComposeControls() { + _shortcutId.value() | rpl::start_with_next([=](BusinessShortcutId id) { + _composeControls->updateShortcutId(id); + }, lifetime()); + + const auto state = Dialogs::EntryState{ + .key = Dialogs::Key{ _history }, + .section = Dialogs::EntryState::Section::ShortcutMessages, + .currentReplyTo = replyTo(), + }; + _composeControls->setCurrentDialogsEntryState(state); + _composeControls->setHistory({ .history = _history.get(), .writeRestriction = rpl::single(Controls::WriteRestriction()), }); - _composeControls->height( - ) | rpl::start_with_next([=](int height) { - const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); - _controlsWrap->resize(width(), height); - if (wasMax) { - listScrollTo(_scroll->scrollTopMax()); - } - }, lifetime()); - _composeControls->cancelRequests( ) | rpl::start_with_next([=] { listCancelRequest(); @@ -637,20 +647,58 @@ void ShortcutMessages::setupComposeControls() { _controlsWrap->widthValue() | rpl::start_with_next([=](int width) { _composeControls->resizeToWidth(width); }, _controlsWrap->lifetime()); - _composeControls->height() | rpl::start_with_next([=](int height) { - _controlsWrap->resize(_controlsWrap->width(), height); - }, _controlsWrap->lifetime()); + + _composeControls->height( + ) | rpl::start_with_next([=](int height) { + const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); + _controlsWrap->resize(width(), height - st::boxRadius); + updateComposeControlsPosition(); + if (wasMax) { + listScrollTo(_scroll->scrollTopMax()); + } + }, lifetime()); } QPointer<Ui::RpWidget> ShortcutMessages::createPinnedToBottom( not_null<Ui::RpWidget*> parent) { + auto placeholder = rpl::deferred([=] { + return _shortcutId.value(); + }) | rpl::map([=](BusinessShortcutId id) { + return _session->data().shortcutMessages().lookupShortcut(id).name; + }) | rpl::map([=](const QString &shortcut) { + return (shortcut == u"away"_q) + ? tr::lng_away_message_placeholder() + : (shortcut == u"hello"_q) + ? tr::lng_greeting_message_placeholder() + : tr::lng_replies_message_placeholder(); + }) | rpl::flatten_latest(); + _controlsWrap = std::make_unique<Ui::RpWidget>(parent); _composeControls = std::make_unique<ComposeControls>( - _controlsWrap.get(), - _controller, - [=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); }, - ComposeControls::Mode::Normal, - SendMenu::Type::Disabled); + dynamic_cast<Ui::RpWidget*>(_scroll->parentWidget()), + ComposeControlsDescriptor{ + .show = _controller->uiShow(), + .unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) { + listShowPremiumToast(emoji); + }, + .mode = HistoryView::ComposeControlsMode::Normal, + .sendMenuType = SendMenu::Type::Disabled, + .regularWindow = _controller, + .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), + .customPlaceholder = std::move(placeholder), + .panelsLevel = Window::GifPauseReason::Layer, + .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), + .voiceLockFromBottom = true, + .features = { + .sendAs = false, + .ttlInfo = false, + .botCommandSend = false, + .silentBroadcastToggle = false, + .attachBotsMenu = false, + .megagroupSet = false, + .commonTabbedPanel = false, + }, + }); setupComposeControls(); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 532a1d35e..c26b68458 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -517,6 +517,7 @@ FileLoadTask::FileLoadTask( , _caption(caption) , _spoiler(spoiler) { Expects(to.options.scheduled + || to.options.shortcutId || !to.replaceMediaOf || IsServerMsgId(to.replaceMediaOf)); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 2a2733f2a..4ec9954a5 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -301,6 +301,7 @@ historySentInvertedIcon: icon {{ "history_sent", historyIconFgInverted, point(2p historyReceivedIcon: icon {{ "history_received", historyOutIconFg, point(2px, 4px) }}; historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }}; historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }}; +historyShortcutStateSpace: 18px; historyViewsSpace: 8px; historyViewsWidth: 20px; From d5e920e45ae1782e39e9ff4b9c15ba1c0acbfad9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 09:06:48 +0400 Subject: [PATCH 060/108] Update API scheme on layer 176. --- .../data/business/data_business_common.h | 1 + .../data/business/data_business_info.cpp | 2 ++ .../SourceFiles/data/data_chat_filters.cpp | 6 ++++-- Telegram/SourceFiles/data/data_user.cpp | 1 + Telegram/SourceFiles/intro/intro_step.cpp | 4 ++-- Telegram/SourceFiles/mtproto/scheme/api.tl | 19 ++++++++++++++----- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 6f3b716dc..a47dce3a2 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -192,6 +192,7 @@ struct AwaySettings { BusinessRecipients recipients; AwaySchedule schedule; BusinessShortcutId shortcutId = 0; + bool offlineOnly = false; explicit operator bool() const { return schedule.type != AwayScheduleType::Never; diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 5fa0844ee..bd75f4af7 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -94,7 +94,9 @@ template <typename Flag> } [[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) { + using Flag = MTPDinputBusinessAwayMessage::Flag; return MTP_inputBusinessAwayMessage( + MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()), MTP_int(data.shortcutId), ToMTP(data.schedule), ToMTP(data.recipients)); diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 237bf5c29..47cd8f4d0 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -196,6 +196,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), + MTPint(), // color MTP_vector<MTPInputPeer>(pinned), MTP_vector<MTPInputPeer>(include)); } @@ -221,6 +222,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), + MTPint(), // color MTP_vector<MTPInputPeer>(pinned), MTP_vector<MTPInputPeer>(include), MTP_vector<MTPInputPeer>(never)); @@ -361,8 +363,8 @@ void ChatFilters::load(bool force) { auto &api = _owner->session().api(); api.request(_loadRequestId).cancel(); _loadRequestId = api.request(MTPmessages_GetDialogFilters( - )).done([=](const MTPVector<MTPDialogFilter> &result) { - received(result.v); + )).done([=](const MTPmessages_DialogFilters &result) { + received(result.data().vfilters().v); _loadRequestId = 0; }).fail([=] { _loadRequestId = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index a351ce678..daa9bb593 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -97,6 +97,7 @@ Data::BusinessRecipients FromMTP( auto result = Data::AwaySettings{ .recipients = FromMTP(owner, data.vrecipients()), .shortcutId = data.vshortcut_id().v, + .offlineOnly = data.is_offline_only(), }; data.vschedule().match([&]( const MTPDbusinessAwayMessageScheduleAlways &) { diff --git a/Telegram/SourceFiles/intro/intro_step.cpp b/Telegram/SourceFiles/intro/intro_step.cpp index 68ec48cdb..6c8a18227 100644 --- a/Telegram/SourceFiles/intro/intro_step.cpp +++ b/Telegram/SourceFiles/intro/intro_step.cpp @@ -193,8 +193,8 @@ void Step::finish(const MTPUser &user, QImage &&photo) { } api().request(MTPmessages_GetDialogFilters( - )).done([=](const MTPVector<MTPDialogFilter> &result) { - createSession(user, photo, result.v); + )).done([=](const MTPmessages_DialogFilters &result) { + createSession(user, photo, result.data().vfilters().v); }).fail([=] { createSession(user, photo, QVector<MTPDialogFilter>()); }).send(); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index cd07a4e19..af421910b 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1234,9 +1234,9 @@ bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector<BankCardOpenUrl> = payments.BankCardData; -dialogFilter#7438f7e8 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter; +dialogFilter#5fb5523b flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter; dialogFilterDefault#363293ae = DialogFilter; -dialogFilterChatlist#d64a04a8 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> = DialogFilter; +dialogFilterChatlist#9fe28ea4 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; @@ -1683,9 +1683,9 @@ inputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRec businessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage; -inputBusinessAwayMessage#edac03f4 shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage; +inputBusinessAwayMessage#832175e0 flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage; -businessAwayMessage#1bd9bebc shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage; +businessAwayMessage#ef156a5c flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage; timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; @@ -1700,6 +1700,12 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; +connectedBot#e7e999e7 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessRecipients = ConnectedBot; + +account.connectedBots#17d7f87b connected_bots:Vector<ConnectedBot> users:Vector<User> = account.ConnectedBots; + +messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector<DialogFilter> = messages.DialogFilters; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1829,6 +1835,8 @@ account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?Bus account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; +account.updateConnectedBot#9c2d527d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessRecipients = Updates; +account.getConnectedBots#4ea4c80f = account.ConnectedBots; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1977,7 +1985,7 @@ messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates; messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates; messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList; messages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector<InputStickerSet> = Bool; -messages.getDialogFilters#f19ed96d = Vector<DialogFilter>; +messages.getDialogFilters#efd48c89 = messages.DialogFilters; messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool; @@ -2067,6 +2075,7 @@ messages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool; messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages; messages.sendQuickReplyMessages#33153ad4 peer:InputPeer shortcut_id:int = Updates; messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates; +messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; From 7f7d5449439e6ce3d88d99b165227c9203efe92d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 10:25:37 +0400 Subject: [PATCH 061/108] Show nice empty quick reply placeholder. --- Telegram/Resources/icons/chat/large_away.png | Bin 0 -> 1996 bytes .../Resources/icons/chat/large_away@2x.png | Bin 0 -> 4375 bytes .../Resources/icons/chat/large_away@3x.png | Bin 0 -> 5879 bytes .../Resources/icons/chat/large_greeting.png | Bin 0 -> 2042 bytes .../icons/chat/large_greeting@2x.png | Bin 0 -> 4529 bytes .../icons/chat/large_greeting@3x.png | Bin 0 -> 4818 bytes .../Resources/icons/chat/large_quickreply.png | Bin 0 -> 1940 bytes .../icons/chat/large_quickreply@2x.png | Bin 0 -> 4363 bytes .../icons/chat/large_quickreply@3x.png | Bin 0 -> 5764 bytes .../info/settings/info_settings_widget.cpp | 6 +- .../business/settings_quick_replies.cpp | 18 +- .../business/settings_shortcut_messages.cpp | 170 ++++++++++++++---- .../settings/settings_business.cpp | 3 +- .../settings/settings_common_session.h | 11 +- .../settings/settings_notifications_type.cpp | 3 +- .../SourceFiles/settings/settings_premium.cpp | 3 +- Telegram/SourceFiles/ui/chat/chat.style | 7 + 17 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 Telegram/Resources/icons/chat/large_away.png create mode 100644 Telegram/Resources/icons/chat/large_away@2x.png create mode 100644 Telegram/Resources/icons/chat/large_away@3x.png create mode 100644 Telegram/Resources/icons/chat/large_greeting.png create mode 100644 Telegram/Resources/icons/chat/large_greeting@2x.png create mode 100644 Telegram/Resources/icons/chat/large_greeting@3x.png create mode 100644 Telegram/Resources/icons/chat/large_quickreply.png create mode 100644 Telegram/Resources/icons/chat/large_quickreply@2x.png create mode 100644 Telegram/Resources/icons/chat/large_quickreply@3x.png diff --git a/Telegram/Resources/icons/chat/large_away.png b/Telegram/Resources/icons/chat/large_away.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a943e0c0746f4ed2c041fcea764492cc1ab4c0 GIT binary patch literal 1996 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_away@2x.png b/Telegram/Resources/icons/chat/large_away@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2b6adb6104e97f20a7b84aec2690c6aa6ee4b3 GIT binary patch literal 4375 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 HHu8S}Voiia literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_away@3x.png b/Telegram/Resources/icons/chat/large_away@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..064090f7689ca16dc908bfdd8758dcab39c3bbce GIT binary patch literal 5879 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 Nx{8i+rJ_aX{{ivne4hXS literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_greeting.png b/Telegram/Resources/icons/chat/large_greeting.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1cb033e5bb043da32f96eb79eb9e1a5b72dd9f GIT binary patch literal 2042 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 Y4?4+*H@&G#W&i*H07*qoM6N<$g4HJP!T<mO literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_greeting@2x.png b/Telegram/Resources/icons/chat/large_greeting@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66fd705ad59fb3ccaceb89cab5eef2844b21e743 GIT binary patch literal 4529 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 Kc-cLx!2bbAtG%-T literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_greeting@3x.png b/Telegram/Resources/icons/chat/large_greeting@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..cd08060ff30c7af15232081cf4047f41d28b63c4 GIT binary patch literal 4818 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_quickreply.png b/Telegram/Resources/icons/chat/large_quickreply.png new file mode 100644 index 0000000000000000000000000000000000000000..084b399ea386d5025874dc41c57bef637f0d4a7b GIT binary patch literal 1940 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 a@x(t${@f+R=!cg80000<MNUMnLSTZ+&!B4n literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_quickreply@2x.png b/Telegram/Resources/icons/chat/large_quickreply@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec1ffe4fe582289267356dc0ab165ae510d86f8 GIT binary patch literal 4363 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 F{{Wbvh`ay* literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_quickreply@3x.png b/Telegram/Resources/icons/chat/large_quickreply@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc18f9ade734a10abf715683c6948dbe8e39f70 GIT binary patch literal 5764 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 literal 0 HcmV?d00001 diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 8a53dc553..5bc315aa1 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -47,7 +47,11 @@ Widget::Widget( auto inner = _type->create( this, controller->parentController(), - scroll()); + scroll(), + controller->wrapValue( + ) | rpl::map([](Wrap wrap) { return (wrap == Wrap::Layer) + ? ::Settings::Container::Layer + : ::Settings::Container::Section; })); if (inner->hasFlexibleTopBar()) { auto filler = setInnerWidget(object_ptr<Ui::RpWidget>(this)); filler->resize(1, 1); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index 431dcd4e1..aac5f918c 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -96,9 +96,8 @@ void QuickReplies::setupContent( Box(EditShortcutNameBox, QString(), crl::guard(this, submit))); }); - Ui::AddSkip(content); - Ui::AddDivider(content); - Ui::AddSkip(content); + const auto dividerWrap = content->add( + object_ptr<Ui::VerticalLayout>(content)); const auto inner = content->add( object_ptr<Ui::VerticalLayout>(content)); @@ -133,6 +132,18 @@ void QuickReplies::setupContent( while (old--) { delete inner->widgetAt(0); } + if (!inner->count()) { + while (dividerWrap->count()) { + delete dividerWrap->widgetAt(0); + } + } else if (!dividerWrap->count()) { + AddSkip(dividerWrap); + AddDivider(dividerWrap); + AddSkip(dividerWrap); + } + if (const auto width = content->width()) { + content->resizeToWidth(width); + } }, content->lifetime()); Ui::ResizeFitChild(this, content); @@ -170,6 +181,7 @@ void EditShortcutNameBox( box->setFocusCallback([=] { field->setFocusFast(); }); + field->selectAll(); const auto callback = [=] { const auto name = field->getLastText().trimmed(); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 8df33ebf0..b72acab4b 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_corner_buttons.h" #include "history/view/history_view_empty_list_bubble.h" #include "history/view/history_view_list_widget.h" +#include "history/view/history_view_service_message.h" #include "history/view/history_view_sticker_toast.h" #include "history/history.h" #include "history/history_item.h" @@ -51,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/scroll_area.h" +#include "ui/painter.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_session_controller.h" @@ -74,6 +76,7 @@ public: QWidget *parent, not_null<Window::SessionController*> controller, not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue, BusinessShortcutId shortcutId); ~ShortcutMessages(); @@ -97,7 +100,7 @@ public: QRect clip) override; private: - void outerResized(QSize outer); + void outerResized(); void updateComposeControlsPosition(); // ListDelegate interface. @@ -247,6 +250,7 @@ private: const Window::SectionShow ¶ms); void showAtEnd(); void finishSending(); + void refreshEmptyText(); const not_null<Window::SessionController*> _controller; const not_null<Main::Session*> _session; @@ -254,6 +258,7 @@ private: const not_null<History*> _history; rpl::variable<BusinessShortcutId> _shortcutId; rpl::variable<QString> _shortcut; + rpl::variable<Container> _container; std::shared_ptr<Ui::ChatStyle> _style; std::shared_ptr<Ui::ChatTheme> _theme; QPointer<ListWidget> _inner; @@ -262,6 +267,14 @@ private: rpl::event_stream<> _showBackRequests; bool _skipScrollEvent = false; + QSize _inOuterResize; + QSize _pendingOuterResize; + + const style::icon *_emptyIcon = nullptr; + Ui::Text::String _emptyText; + int _emptyTextWidth = 0; + int _emptyTextHeight = 0; + rpl::variable<Info::SelectedItems> _selectedItems = Info::SelectedItems(Storage::SharedMediaType::kCount); @@ -283,22 +296,33 @@ struct Factory final : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<ShortcutMessages>( parent, controller, scroll, + std::move(containerValue), shortcutId); } const BusinessShortcutId shortcutId = {}; }; +[[nodiscard]] bool IsAway(const QString &shortcut) { + return (shortcut == u"away"_q); +} + +[[nodiscard]] bool IsGreeting(const QString &shortcut) { + return (shortcut == u"hello"_q); +} + ShortcutMessages::ShortcutMessages( QWidget *parent, not_null<Window::SessionController*> controller, not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue, BusinessShortcutId shortcutId) : AbstractSection(parent) , _controller(controller) @@ -308,6 +332,7 @@ ShortcutMessages::ShortcutMessages( , _shortcutId(shortcutId) , _shortcut( _session->data().shortcutMessages().lookupShortcut(shortcutId).name) +, _container(std::move(containerValue)) , _cornerButtons( _scroll, controller->chatStyle(), @@ -345,8 +370,8 @@ ShortcutMessages::ShortcutMessages( _scroll->sizeValue() | rpl::filter([](QSize size) { return !size.isEmpty(); - }) | rpl::start_with_next([=](QSize size) { - outerResized(size); + }) | rpl::start_with_next([=] { + outerResized(); }, lifetime()); _scroll->scrolls( @@ -354,6 +379,11 @@ ShortcutMessages::ShortcutMessages( processScroll(); }, lifetime()); + _shortcut.value() | rpl::start_with_next([=] { + refreshEmptyText(); + _inner->update(); + }, lifetime()); + _inner->editMessageRequested( ) | rpl::start_with_next([=](auto fullId) { if (const auto item = _session->data().message(fullId)) { @@ -364,17 +394,6 @@ ShortcutMessages::ShortcutMessages( } }, _inner->lifetime()); - { - auto emptyInfo = base::make_unique_q<EmptyListBubbleWidget>( - _inner, - _style.get(), - st::msgServicePadding); - const auto emptyText = Ui::Text::Semibold( - u"give me your money.."_q); - emptyInfo->setText(emptyText); - _inner->setEmptyInfoWidget(std::move(emptyInfo)); - } - _inner->heightValue() | rpl::start_with_next([=](int height) { resize(width(), height); }, lifetime()); @@ -382,15 +401,62 @@ ShortcutMessages::ShortcutMessages( ShortcutMessages::~ShortcutMessages() = default; +void ShortcutMessages::refreshEmptyText() { + const auto &shortcut = _shortcut.current(); + const auto away = IsAway(shortcut); + const auto greeting = !away && IsGreeting(shortcut); + auto text = away + ? tr::lng_away_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_away_empty_about(tr::now)) + : greeting + ? tr::lng_greeting_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_greeting_empty_about(tr::now)) + : tr::lng_replies_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_replies_empty_about( + tr::now, + lt_shortcut, + Ui::Text::Bold('/' + shortcut), + Ui::Text::WithEntities)); + _emptyIcon = away + ? &st::awayEmptyIcon + : greeting + ? &st::greetingEmptyIcon + : &st::repliesEmptyIcon; + const auto padding = st::repliesEmptyPadding; + const auto minWidth = st::repliesEmptyWidth / 4; + const auto maxWidth = std::max( + minWidth + 1, + st::repliesEmptyWidth - padding.left() - padding.right()); + _emptyText = Ui::Text::String( + st::messageTextStyle, + text, + kMarkupTextOptions, + minWidth); + const auto countHeight = [&](int width) { + return _emptyText.countHeight(width); + }; + _emptyTextWidth = Ui::FindNiceTooltipWidth( + minWidth, + maxWidth, + countHeight); + _emptyTextHeight = countHeight(_emptyTextWidth); +} + Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { return std::make_shared<Factory>(shortcutId); } rpl::producer<QString> ShortcutMessages::title() { return _shortcut.value() | rpl::map([=](const QString &shortcut) { - return (shortcut == u"away"_q) + return IsAway(shortcut) ? tr::lng_away_title() - : (shortcut == u"hello"_q) + : IsGreeting(shortcut) ? tr::lng_greeting_title() : rpl::single('/' + shortcut); }) | rpl::flatten_latest(); @@ -497,22 +563,36 @@ bool ShortcutMessages::paintOuter( return true; } -void ShortcutMessages::outerResized(QSize outer) { - const auto contentWidth = outer.width(); +void ShortcutMessages::outerResized() { + const auto outer = _scroll->size(); + if (!_inOuterResize.isEmpty()) { + _pendingOuterResize = (_inOuterResize != outer) + ? outer + : QSize(); + return; + } + _inOuterResize = outer; - const auto newScrollTop = _scroll->isHidden() - ? std::nullopt - : _scroll->scrollTop() - ? base::make_optional(_scroll->scrollTop()) - : 0; - _skipScrollEvent = true; - _inner->resizeToWidth(contentWidth, st::boxWidth); - _skipScrollEvent = false; + do { + const auto newScrollTop = _scroll->isHidden() + ? std::nullopt + : _scroll->scrollTop() + ? base::make_optional(_scroll->scrollTop()) + : 0; + _skipScrollEvent = true; + const auto minHeight = (_container.current() == Container::Layer) + ? st::boxWidth + : _inOuterResize.height(); + _inner->resizeToWidth(_inOuterResize.width(), minHeight); + _skipScrollEvent = false; - if (!_scroll->isHidden()) { - if (newScrollTop) { + if (!_scroll->isHidden() && newScrollTop) { _scroll->scrollToY(*newScrollTop); } + _inOuterResize = base::take(_pendingOuterResize); + } while (!_inOuterResize.isEmpty()); + + if (!_scroll->isHidden()) { updateInnerVisibleArea(); } updateComposeControlsPosition(); @@ -896,8 +976,36 @@ void ShortcutMessages::listOpenDocument( } void ShortcutMessages::listPaintEmpty( - Painter &p, - const Ui::ChatPaintContext &context) { + Painter &p, + const Ui::ChatPaintContext &context) { + Expects(_emptyIcon != nullptr); + + const auto width = st::repliesEmptyWidth; + const auto padding = st::repliesEmptyPadding; + const auto height = padding.top() + + _emptyIcon->height() + + st::repliesEmptySkip + + _emptyTextHeight + + padding.bottom(); + const auto r = QRect( + (this->width() - width) / 2, + (this->height() - height) / 3, + width, + height); + HistoryView::ServiceMessagePainter::PaintBubble(p, context.st, r); + + _emptyIcon->paint( + p, + r.x() + (r.width() - _emptyIcon->width()) / 2, + r.y() + padding.top(), + this->width()); + p.setPen(st::msgServiceFg); + _emptyText.draw( + p, + r.x() + (r.width() - _emptyTextWidth) / 2, + r.y() + padding.top() + _emptyIcon->height() + st::repliesEmptySkip, + _emptyTextWidth, + style::al_top); } QString ShortcutMessages::listElementAuthorRank( diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 0881925b5..a8792783c 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -562,7 +562,8 @@ struct SectionFactory<Business> : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<Business>(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 2ebd1319c..2b439bacf 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -26,13 +26,19 @@ class SessionController; namespace Settings { +enum class Container { + Section, + Layer, +}; + class AbstractSection; struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll) const = 0; + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue) const = 0; [[nodiscard]] virtual bool hasCustomTopBar() const { return false; } @@ -45,7 +51,8 @@ struct SectionFactory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<SectionType>(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 717627573..802a82ee6 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -44,7 +44,8 @@ struct Factory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<NotificationsType>(parent, controller, type); } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index ec17c8aac..809b8ba8e 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1268,7 +1268,8 @@ struct SectionFactory<Premium> : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, not_null<Window::SessionController*> controller, - not_null<Ui::ScrollArea*> scroll + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<Premium>(parent, controller); } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 4ec9954a5..b4217d85b 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1044,6 +1044,13 @@ premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; premiumRequiredCircle: 60px; +repliesEmptyIcon: icon{{ "chat/large_quickreply", msgServiceFg }}; +greetingEmptyIcon: icon{{ "chat/large_greeting", msgServiceFg }}; +awayEmptyIcon: icon{{ "chat/large_away", msgServiceFg }}; +repliesEmptyWidth: 264px; +repliesEmptySkip: 16px; +repliesEmptyPadding: margins(10px, 20px, 10px, 16px); + boostMessageIcon: icon {{ "stories/boost_mini", windowFg }}; boostMessageIconPadding: margins(0px, 2px, 0px, 0px); boostsMessageIcon: icon {{ "stories/boosts_mini", windowFg }}; From dd6768a476e6501ed3ba241b303099ffa8af2b2f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 12:26:27 +0400 Subject: [PATCH 062/108] Add stories rights by default to new admins. --- Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index ff9398bf0..6268db65b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -216,6 +216,9 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { : peer()->isMegagroup() ? ChatAdminRightsInfo{ (Flag::ChangeInfo | Flag::DeleteMessages + | Flag::PostStories + | Flag::EditStories + | Flag::DeleteStories | Flag::BanUsers | Flag::InviteByLinkOrAdd | Flag::ManageTopics @@ -225,6 +228,9 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::PostMessages | Flag::EditMessages | Flag::DeleteMessages + | Flag::PostStories + | Flag::EditStories + | Flag::DeleteStories | Flag::InviteByLinkOrAdd | Flag::ManageCall) }; } From ca4cbddba6a3856bcc3303c4e897c7bc0a218e63 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 12:29:03 +0400 Subject: [PATCH 063/108] Check shortcuts / messages limits. --- Telegram/Resources/langs/lang.strings | 3 + .../chat_helpers/stickers_list_footer.cpp | 5 +- .../history_view_compose_controls.cpp | 1 + .../business/settings_away_message.cpp | 45 ++++++++-- .../settings/business/settings_greeting.cpp | 44 ++++++++-- .../business/settings_quick_replies.cpp | 71 +++++++++------- .../business/settings_recipients_helper.cpp | 83 +++++++++++++++++++ .../business/settings_recipients_helper.h | 22 +++++ .../business/settings_shortcut_messages.cpp | 24 +++++- .../SourceFiles/settings/settings_main.cpp | 4 - Telegram/lib_ui | 2 +- 11 files changed, 253 insertions(+), 51 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 896fdc415..add970d06 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2226,6 +2226,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_greeting_empty_title" = "New Greeting Message"; "lng_greeting_empty_about" = "Create greetings that will be automatically sent to new customers."; "lng_greeting_message_placeholder" = "Add a Greeting"; +"lng_greeting_limit_reached" = "You have too many quick replies. Remove one to add a greeting message."; "lng_away_title" = "Away Message"; "lng_away_about" = "Automatically reply with a message when you are away."; @@ -2242,7 +2243,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_away_empty_title" = "New Away Message"; "lng_away_empty_about" = "Add messages that will be automatically sent when you are off."; "lng_away_message_placeholder" = "Add an Away Message"; +"lng_away_limit_reached" = "You have too many quick replies. Remove one to add an away message."; +"lng_business_edit_messages" = "Edit messages"; "lng_business_limit_reached#one" = "Limit of {count} message reached."; "lng_business_limit_reached#other" = "Limit of {count} messages reached."; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index 4534818d3..6659343d2 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -114,10 +114,7 @@ std::optional<EmojiSection> SetIdEmojiSection(uint64 id) { rpl::producer<std::vector<GifSection>> GifSectionsValue( not_null<Main::Session*> session) { const auto config = &session->account().appConfig(); - return rpl::single( - rpl::empty_value() - ) | rpl::then( - config->refreshed() + return config->value( ) | rpl::map([=] { return config->get<std::vector<QString>>( u"gif_search_emojies"_q, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 9cfec19f5..bd8d6cb12 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2347,6 +2347,7 @@ void SetupRestrictionView( }); state->label = makeLabel(value.text, st->premiumRequired.label); } + state->updateGeometries(); }, widget->lifetime()); widget->sizeValue( diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 69ac74bee..739d12d2a 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_shortcut_messages.h" #include "ui/boxes/choose_date_time.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" @@ -43,6 +44,8 @@ private: void setupContent(not_null<Window::SessionController*> controller); void save(); + rpl::variable<bool> _canHave; + rpl::event_stream<> _deactivateOnAttempt; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<Data::AwaySchedule> _schedule; rpl::variable<bool> _enabled; @@ -231,13 +234,35 @@ void AwayMessage::setupContent( .aboutMargins = st::peerAppearanceCoverLabelMargin, }); + const auto session = &controller->session(); + _canHave = rpl::combine( + ShortcutsCountValue(session), + ShortcutsLimitValue(session), + ShortcutExistsValue(session, u"away"_q), + (_1 < _2) || _3); + Ui::AddSkip(content); const auto enabled = content->add(object_ptr<Ui::SettingsButton>( content, tr::lng_away_enable(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(!disabled)); + ))->toggleOn(rpl::single( + !disabled + ) | rpl::then(rpl::merge( + _canHave.value() | rpl::filter(!_1), + _deactivateOnAttempt.events() | rpl::map_to(false) + ))); + _enabled = enabled->toggledValue(); + _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { + if (!_canHave.current()) { + controller->showToast({ + .text = tr::lng_away_limit_reached(tr::now), + .adaptive = true, + }); + _deactivateOnAttempt.fire({}); + } + }, lifetime()); const auto wrap = content->add( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( @@ -254,11 +279,21 @@ void AwayMessage::setupContent( object_ptr<Ui::VerticalLayout>(inner))); const auto createInner = createWrap->entity(); Ui::AddSkip(createInner); - const auto create = createInner->add(object_ptr<Ui::SettingsButton>( + const auto create = AddButtonWithLabel( createInner, - tr::lng_away_create(), - st::settingsButtonLightNoIcon - )); + rpl::conditional( + ShortcutExistsValue(session, u"away"_q), + tr::lng_business_edit_messages(), + tr::lng_away_create()), + ShortcutMessagesCountValue( + session, + u"away"_q + ) | rpl::map([=](int count) { + return count + ? tr::lng_forum_messages(tr::now, lt_count, count) + : QString(); + }), + st::settingsButtonLightNoIcon); create->setClickedCallback([=] { const auto owner = &controller->session().data(); const auto id = owner->shortcutMessages().emplaceShortcut("away"); diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 96b2615c4..43cba0763 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_recipients_helper.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/buttons.h" #include "ui/widgets/vertical_drum_picker.h" @@ -53,6 +54,8 @@ private: Ui::RoundRect _bottomSkipRounding; rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<bool> _canHave; + rpl::event_stream<> _deactivateOnAttempt; rpl::variable<int> _noActivityDays; rpl::variable<bool> _enabled; @@ -198,14 +201,35 @@ void Greeting::setupContent( .aboutMargins = st::peerAppearanceCoverLabelMargin, }); + const auto session = &controller->session(); + _canHave = rpl::combine( + ShortcutsCountValue(session), + ShortcutsLimitValue(session), + ShortcutExistsValue(session, u"hello"_q), + (_1 < _2) || _3); + Ui::AddSkip(content); const auto enabled = content->add(object_ptr<Ui::SettingsButton>( content, tr::lng_greeting_enable(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(!disabled)); + ))->toggleOn(rpl::single( + !disabled + ) | rpl::then(rpl::merge( + _canHave.value() | rpl::filter(!_1), + _deactivateOnAttempt.events() | rpl::map_to(false) + ))); _enabled = enabled->toggledValue(); + _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { + if (!_canHave.current()) { + controller->showToast({ + .text = tr::lng_greeting_limit_reached(tr::now), + .adaptive = true, + }); + _deactivateOnAttempt.fire({}); + } + }, lifetime()); Ui::AddSkip(content); @@ -237,11 +261,21 @@ void Greeting::setupContent( object_ptr<Ui::VerticalLayout>(inner))); const auto createInner = createWrap->entity(); Ui::AddSkip(createInner); - const auto create = createInner->add(object_ptr<Ui::SettingsButton>( + const auto create = AddButtonWithLabel( createInner, - tr::lng_greeting_create(), - st::settingsButtonLightNoIcon - )); + rpl::conditional( + ShortcutExistsValue(session, u"hello"_q), + tr::lng_business_edit_messages(), + tr::lng_greeting_create()), + ShortcutMessagesCountValue( + session, + u"hello"_q + ) | rpl::map([=](int count) { + return count + ? tr::lng_forum_messages(tr::now, lt_count, count) + : QString(); + }), + st::settingsButtonLightNoIcon); create->setClickedCallback([=] { const auto owner = &controller->session().data(); const auto id = owner->shortcutMessages().emplaceShortcut("hello"); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index aac5f918c..53fb228ba 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" +#include "main/main_account.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" #include "settings/business/settings_shortcut_messages.h" @@ -42,6 +43,8 @@ private: void setupContent(not_null<Window::SessionController*> controller); void save(); + rpl::variable<int> _count; + }; QuickReplies::QuickReplies( @@ -75,29 +78,47 @@ void QuickReplies::setupContent( .about = tr::lng_replies_about(Ui::Text::WithEntities), .aboutMargins = st::peerAppearanceCoverLabelMargin, }); - Ui::AddSkip(content); - const auto add = content->add(object_ptr<Ui::SettingsButton>( - content, - tr::lng_replies_add(), - st::settingsButtonNoIcon - )); + + const auto addWrap = content->add( + object_ptr<Ui::VerticalLayout>(content)); const auto owner = &controller->session().data(); const auto messages = &owner->shortcutMessages(); - add->setClickedCallback([=] { - const auto submit = [=](QString name, Fn<void()> close) { - const auto id = messages->emplaceShortcut(name); - showOther(ShortcutMessagesId(id)); - close(); - }; - controller->show( - Box(EditShortcutNameBox, QString(), crl::guard(this, submit))); - }); + rpl::combine( + _count.value(), + ShortcutsLimitValue(&controller->session()) + ) | rpl::start_with_next([=](int count, int limit) { + while (addWrap->count()) { + delete addWrap->widgetAt(0); + } + if (count < limit) { + const auto add = addWrap->add(object_ptr<Ui::SettingsButton>( + addWrap, + tr::lng_replies_add(), + st::settingsButtonNoIcon + )); - const auto dividerWrap = content->add( - object_ptr<Ui::VerticalLayout>(content)); + add->setClickedCallback([=] { + const auto submit = [=](QString name, Fn<void()> close) { + const auto id = messages->emplaceShortcut(name); + showOther(ShortcutMessagesId(id)); + close(); + }; + controller->show( + Box(EditShortcutNameBox, QString(), crl::guard(this, submit))); + }); + if (count > 0) { + AddSkip(addWrap); + AddDivider(addWrap); + AddSkip(addWrap); + } + } + if (const auto width = content->width()) { + content->resizeToWidth(width); + } + }, lifetime()); const auto inner = content->add( object_ptr<Ui::VerticalLayout>(content)); @@ -108,7 +129,8 @@ void QuickReplies::setupContent( const auto &shortcuts = messages->shortcuts(); auto i = 0; - for (const auto &[_, shortcut] : shortcuts.list) { + for (const auto &[_, shortcut] + : shortcuts.list | ranges::views::reverse) { if (!shortcut.count) { continue; } @@ -132,18 +154,7 @@ void QuickReplies::setupContent( while (old--) { delete inner->widgetAt(0); } - if (!inner->count()) { - while (dividerWrap->count()) { - delete dividerWrap->widgetAt(0); - } - } else if (!dividerWrap->count()) { - AddSkip(dividerWrap); - AddDivider(dividerWrap); - AddSkip(dividerWrap); - } - if (const auto width = content->width()) { - content->resizeToWidth(width); - } + _count = inner->count(); }, content->lifetime()); Ui::ResizeFitChild(this, content); diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index 83c11a9cd..5787bb234 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -9,10 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/filters/edit_filter_chats_list.h" #include "boxes/filters/edit_filter_chats_preview.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" #include "settings/settings_common.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" @@ -291,4 +295,83 @@ void AddBusinessRecipientsSelector( }); } +int ShortcutsCount(not_null<Main::Session*> session) { + const auto &shortcuts = session->data().shortcutMessages().shortcuts(); + auto result = 0; + for (const auto &[_, shortcut] : shortcuts.list) { + if (shortcut.count > 0) { + ++result; + } + } + return result; +} + +rpl::producer<int> ShortcutsCountValue(not_null<Main::Session*> session) { + const auto messages = &session->data().shortcutMessages(); + return rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::map([=] { + return ShortcutsCount(session); + }); +} + +int ShortcutMessagesCount( + not_null<Main::Session*> session, + const QString &name) { + const auto &shortcuts = session->data().shortcutMessages().shortcuts(); + for (const auto &[_, shortcut] : shortcuts.list) { + if (shortcut.name == name) { + return shortcut.count; + } + } + return 0; +} + +rpl::producer<int> ShortcutMessagesCountValue( + not_null<Main::Session*> session, + const QString &name) { + const auto messages = &session->data().shortcutMessages(); + return rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::map([=] { + return ShortcutMessagesCount(session, name); + }); +} + +bool ShortcutExists(not_null<Main::Session*> session, const QString &name) { + return ShortcutMessagesCount(session, name) > 0; +} + +rpl::producer<bool> ShortcutExistsValue( + not_null<Main::Session*> session, + const QString &name) { + return ShortcutMessagesCountValue(session, name) + | rpl::map(rpl::mappers::_1 > 0); +} + +int ShortcutsLimit(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->get<int>("quick_replies_limit", 100); +} + +rpl::producer<int> ShortcutsLimitValue(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->value() | rpl::map([=] { + return ShortcutsLimit(session); + }); +} + +int ShortcutMessagesLimit(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->get<int>("quick_reply_messages_limit", 20); +} + +rpl::producer<int> ShortcutMessagesLimitValue( + not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->value() | rpl::map([=] { + return ShortcutMessagesLimit(session); + }); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h index 60efd7425..e05f702e6 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -71,4 +71,26 @@ void AddBusinessRecipientsSelector( not_null<Ui::VerticalLayout*> container, BusinessRecipientsSelectorDescriptor &&descriptor); +[[nodiscard]] int ShortcutsCount(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutsCountValue( + not_null<Main::Session*> session); +[[nodiscard]] int ShortcutMessagesCount( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] rpl::producer<int> ShortcutMessagesCountValue( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] bool ShortcutExists( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] rpl::producer<bool> ShortcutExistsValue( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] int ShortcutsLimit(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutsLimitValue( + not_null<Main::Session*> session); +[[nodiscard]] int ShortcutMessagesLimit(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutMessagesLimitValue( + not_null<Main::Session*> session); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index b72acab4b..91ba0c63f 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -35,6 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "menu/menu_send.h" #include "settings/business/settings_quick_replies.h" @@ -259,6 +261,7 @@ private: rpl::variable<BusinessShortcutId> _shortcutId; rpl::variable<QString> _shortcut; rpl::variable<Container> _container; + rpl::variable<int> _count; std::shared_ptr<Ui::ChatStyle> _style; std::shared_ptr<Ui::ChatTheme> _theme; QPointer<ListWidget> _inner; @@ -618,9 +621,22 @@ void ShortcutMessages::setupComposeControls() { }; _composeControls->setCurrentDialogsEntryState(state); + auto writeRestriction = rpl::combine( + _count.value(), + ShortcutMessagesLimitValue(_session) + ) | rpl::map([=](int count, int limit) { + return (count >= limit) + ? Controls::WriteRestriction{ + .text = tr::lng_business_limit_reached( + tr::now, + lt_count, + limit), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }); _composeControls->setHistory({ .history = _history.get(), - .writeRestriction = rpl::single(Controls::WriteRestriction()), + .writeRestriction = std::move(writeRestriction), }); _composeControls->cancelRequests( @@ -831,7 +847,11 @@ rpl::producer<Data::MessagesSlice> ShortcutMessages::listSource( ) | rpl::map([=] { return messages->list(shortcutId); }); - }) | rpl::flatten_latest(); + }) | rpl::flatten_latest( + ) | rpl::after_next([=](const Data::MessagesSlice &slice) { + _count = slice.fullCount.value_or( + messages->count(_shortcutId.current())); + }); } bool ShortcutMessages::listAllowsMultiSelect() { diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 4dd57db8f..c746a881a 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -446,10 +446,6 @@ void SetupPremium( button->addClickHandler([=] { controller->showGiftPremiumsBox(u"gift"_q); }); - constexpr auto kNewExpiresAt = int(1735689600); - if (base::unixtime::now() < kNewExpiresAt) { - Ui::NewBadge::AddToRight(button); - } } Ui::AddSkip(container); } diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 333587d95..14794d222 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 333587d95edefcae1ebaf8838d3f499639fc2de8 +Subproject commit 14794d22210cb21b82db20aa55b1f3c8733b5fbd From cf8aaf5f9d20531731dd087ee8e3bc6b9d0e9e46 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 13:17:41 +0400 Subject: [PATCH 064/108] Save away / greeting settings. --- .../icons/folders/folder_existing_chats.png | Bin 0 -> 592 bytes .../folders/folder_existing_chats@2x.png | Bin 0 -> 1101 bytes .../folders/folder_existing_chats@3x.png | Bin 0 -> 1628 bytes .../icons/folders/folder_new_chats.png | Bin 0 -> 598 bytes .../icons/folders/folder_new_chats@2x.png | Bin 0 -> 1111 bytes .../icons/folders/folder_new_chats@3x.png | Bin 0 -> 1598 bytes Telegram/Resources/langs/lang.strings | 2 ++ .../business/settings_away_message.cpp | 18 +++++++++++++++++- .../settings/business/settings_greeting.cpp | 4 +++- .../business/settings_recipients_helper.cpp | 14 +++++++++++++- .../business/settings_recipients_helper.h | 4 ++++ Telegram/SourceFiles/window/window.style | 4 ++-- 12 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 Telegram/Resources/icons/folders/folder_existing_chats.png create mode 100644 Telegram/Resources/icons/folders/folder_existing_chats@2x.png create mode 100644 Telegram/Resources/icons/folders/folder_existing_chats@3x.png create mode 100644 Telegram/Resources/icons/folders/folder_new_chats.png create mode 100644 Telegram/Resources/icons/folders/folder_new_chats@2x.png create mode 100644 Telegram/Resources/icons/folders/folder_new_chats@3x.png diff --git a/Telegram/Resources/icons/folders/folder_existing_chats.png b/Telegram/Resources/icons/folders/folder_existing_chats.png new file mode 100644 index 0000000000000000000000000000000000000000..e54a10425911300ae4738dcd3daa509860f1edb6 GIT binary patch literal 592 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfY?r5tV~B;| z+YsA+#z2wOV;2@GsHk*!Y5ZYRocM$LRi=oD5Lc&*sut@~7su!F9s#~fAE;|&aP+P} za{SH7eWuo*i`>g@pY=2TKCk+G<@dSY({!fU|L#b$7Irn1;AwMG^ql0keDbPQX>*rV zs0c~&u(cm%O!@YvY<JvxaSj$A%b0^{n@y#9Pe1(>WU#0Ic=Fb$tx;=bx;@$tFKqNu zy_+{Z;nl(#JAXk=o$04vcGm2P(c=_iJ)34cZ_U%V_0tz!xbgVohTCsXrUWfk7KnKC z`RA7xzas=#`i}=|h@5(AbpPk-&UfFf^}?55b`<D8KJ(gjm)md4Hb#8-q-JTe>u%n~ z43X(!(^B_oPxZ2$?|(~Fd6wVuSiS3Ct5|Jj`cx=B*`e&lHF0Z{Zi(4!-!SQ3x0Zmt zaq27dGxpy<oN!^&O`UDW9(XN1#QjhwLHN{%)%V}$o6Vlyv@2@ut+!@=T+1FTIrrRH zd+qh??YGZzry9-tS<`p&Z=L;W%em(YEO@LZBuUQbVDpuATp98x^xhM`_QheV6Mgow zbT7ZW@*D5}2f_bhRK&Vfg*g8#d@yB^Mu5Ip_YRj0r8aW=XFlHl<?R7>qsiR+S1)<i P4T?=qS3j3^P6<r_W+m^$ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/folders/folder_existing_chats@2x.png b/Telegram/Resources/icons/folders/folder_existing_chats@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a73f7e1fbf9da405d8efc9527eb5ad98e1875c GIT binary patch literal 1101 zcmV-T1hV^yP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFw@E}nR9Fe^SWPH&Q5a?rLNrr; z(u^T8ni4w`MM_eWg|e8&l!ekHMY6?g6pLBSf(12_2+2mwLdi5mHf&goqD+3GQH1jI zeUEOPZs**&_uM=4t-d>p>AmlH-sgGmd(M5&xi>9M|LGObEAXFHAYH@#78DdzRaKRg zloS^ie}8{JJUrar-$$d-kB<)xY}!MrtE(p_CJqk|KR-YJFu(Wr_fRM_K0aPnR;FEp zGqS$Eesy&f#bDBqaG*|eb90rIm7L`2U>O-1Q&Ur4Utbc+vaYwcx52?dbz+KWqtO@+ zhh=pY1~)f1v$L}mg}4A%xyQ%H3W6NN+uPfjnVDRYG9ZJ&u)n|0A;$$-TwIjV;u`XL zy<Ae<pw7-tE@4R!y36zPGq)BO@bdDKb1@m%ocjCw3kwTbe=;64Qm50Yk>T|8l<Tfl zg0{A{v?NqsUanGz7mik*p8mU2Wo2c_WJXV!)oPVNCo`0jlT%t+3I><U<?(nDQPBxm zES5yj<o#x|86<aicS}o4larH@gmgkbKR=SVlsK^)g6I1B8vodldU|@4X<J%aco&6w z92yETAj=fZg`E((LO9kyNI7x!h}YLwoScM&glIIEk&zK)rlX@H^*65^v9huvXk(@b zIv$Vb<>iTUqAP^4_4RczB31VC^0GkXa5xBOQBhHAYb&{wHuCfHZ*OnOO{By)J3ABh ze6*sgt1B_*@bIv79%Apnz<@X=NgGctsS5kh`2BvdbV3^yPBIS?J~J~T$e=vziEny( zI$;vx0W{W&iwi=k*;rUu5OxfZqd~JgLqkJiT{#;ajEz_22?PQ->j@e*o9*f8i4C{g z?Q%(wG19(R3Lekn<70PsH(|rO5w(^D5=;WYC>VWxeLTV}2C3ln4KD~p0JS+kKWCBf zK!v~*45~dI930fv)*^mwZ4KRwWsk*T!C=tsc2kY6x3~A@<%Pw=g9T2OmqIpUV`GE@ zD+#RvFKFTzG_<<9y7u<=wzf7HCX-1#D%g#UjXOI#goiS41Hx&94V3WA&(CjcY~X?H z=;$D;QUWLCLWNKjoDb1{s3gRfXq8gYX}?Ff<bM?v6<u9jH8nM;7HXE8o6E!5+}s?A zM0k*@F<1)dF^N_#DGc>>U~g|vwUXL^rluyGymohY@jT<OadUHXa&m%~*U`~Y)DP!l ze6$dMDVUv|r6f`_WfZCWB|03YwV|N_yEu~e`F#KMm@1!)KB`whuYg_wrUHKfWV<&? TEVi1~00000NkvXXu0mjfI9=Yn literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/folders/folder_existing_chats@3x.png b/Telegram/Resources/icons/folders/folder_existing_chats@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2b37db754e2f5615b839f1518f866d437ee63da5 GIT binary patch literal 1628 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 ac;G+Cna;S=%MO?T0000<MNUMnLSTX*Pw!Fy literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/folders/folder_new_chats.png b/Telegram/Resources/icons/folders/folder_new_chats.png new file mode 100644 index 0000000000000000000000000000000000000000..03c8380d448c5b9f8afa1458deb63944aa84d48f GIT binary patch literal 598 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf?0~0>V~B;| z+bL(e9Rnqf-7Vw|6T8%MqeCOwHSuV?qN+9bQ~7{z|JbdMo^m^R>bOw0{6<I4jT1fv zCe15c_V~=+<EF1O&pGwa|6aN0`0qHatMhGNO*gOJKmYVoCoNV-4z}h-hlKt2-<Rzc zFAEe&-Waho=w^=D_TQ1~ud7YwocXnC?u5@j=Xj`0Y5LGQ`D9DT<?C*X8U58JFJyUC zU?I`#HtDNcXV3A+XVWe}Eb#bIV#WILXN}JF*I(n-PoE&~_^@C{fX0febL7uGEjnl* z)q7*X>#tUF{gI!RK79ZEch2)Qc5?k`n{Or<@UStTD&9HAg!R4hM2@u&y0W%*O?+r| zGR0{2*+sQ}Tmg3TpZ|)yk+s!Ej{oChwm)l%cAlvc`BJp=P1)|-Z_74DTv+C^%5S;w z)l-ii*9A1|E}YuGlDFO2t6o#Z`A^x-yLmmq58XcX&&w-Y88Ru{UO9u^pJ#vho8LKR z)z4Ba<@omBKku)&C2aLZwspt$EV{TNsOxIhQMTf<ERW7FiO{)~V^(=8`A^hZvDHg1 zzYJ(G^;~|rY`3ilSEP<uuiNIRwN-oNe$74q{F8`4W6<);Grg8>NPb(k8>sZ3z8#}R W!@nbJHfQBPaq8*n=d#Wzp$PyA`~WHd literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/folders/folder_new_chats@2x.png b/Telegram/Resources/icons/folders/folder_new_chats@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f91df76357aeb853e9f883ef2970c5594c19a916 GIT binary patch literal 1111 zcmV-d1gQIoP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NF!AV3xR9Fe^SWPH&Q5a^7L_{+3 z7x^htEQFGuqKJiTWF^T$l&okLO0rOFDa_6aD-kJ4i3rI;3Dbn3{Qv(<3E%hB;dDFa z+~eNsT6}$T7c=j9&U4=PIq#gG_l}Q`_wi=Hn}Po<1HM0>ub-b^L_|bzaPZ^f<K5le z>+37y(`vQW*VmVqm&e4!+}zwOEiDZX4};0jj5nbmKR>^_yL)wY_5A$&{r&yV*8|(> zbRstOo{^EUxw-k(|M`7#a#B%IK{>{au&}Vs&d#f=D<*)li9CgdhLRXjQBi7SNXM*< zOY!&j@9OHhzrR<;7TQBYLp03P)Rd4zK;$woFp!dU7>S9A>+9<h*UGNO#zx{gIXPL0 z3+<Yk8b*hOY4gppva+qMt(=@3zDtO>TrPrVWn~EoN|5JRmEF9ZmzOs&F%c9L#CNG7 z4-XHdD<B|1jYDysBg;^Fdiu=Fj4m~ZguPATVTsVdYK9ih=U9uyvazut{Nd#wjsxNX z9uQALetv$Ajg3{)%Hqw<%@SXduI=q@>Z_ojKzT|rqgq;8DDxX5BqRh!Dl=Ytv$C?1 z2EfET9uG72<Kv^hzu(ta^F2{pTg$l6)!f_L!<49|)YR0;$w?yX?d@%OdAX#dg!XDO zu$FZx;K|d|(>P0&!LX#l!^3f}DM3v(wiPZ&K89XoadFXRvuO$`C#9vO>5Z+%zrDS+ z+wDjU6A`6BYiny?Umt^`DOgserKNLob3D~Bf{nJava-Lwe|dQs9UYyLl9HL3iA9Dw zq^PLqd$)CUb!sy<7vI_0;nfIEYI^qbY;zI(pu9tYpwrV+p7$Hb@$oV5Rv_r`@Q~;I z2I6+RdA9;Vs7g_9@Kn>0`T2R?r$7)$sPuTMxd?XL!oq@9pGiqcrkstET{$o?pj8dw z4Gs=U-2Jc%F9JFW4<ZP4mX?y<n6kFEHht+K?5e6N)CtByN=B`%tr@vuiKgD(UWsc1 zUAw!xc+EAE9x|r7y4pZgwIN4GNAdCTay8;@Psilu=2H8I#_a5Dc6PS#Fx2W9@9^>} zT`dy~{_^tD-rlaGU{w>?)YQbd*4xDP#wv`Aj8qGu14nxCn!+m>8yhPwF7D~+!S%xT z66E^&dVGAmp`n2<&AwG2D9|t_E-nr)^C+g2;|~rFIyyQ=M@K1$3N;}i0e^AeLE(FG zae;gA?CcChKF$kDGr<@c8KJYsGITf`g@uJCECLPE#2G?;iGMtvo}Msid}{FHrFb*o d&A>mCfxmLC0o@lrjs5@t002ovPDHLkV1n&74>SM( literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/folders/folder_new_chats@3x.png b/Telegram/Resources/icons/folders/folder_new_chats@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d379a9e4b0dace8fd4f1a55e6c8d122867f2360a GIT binary patch literal 1598 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index add970d06..c78969caa 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2238,6 +2238,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_away_schedule_custom" = "Custom Schedule"; "lng_away_custom_start" = "Start Time"; "lng_away_custom_end" = "End Time"; +"lng_away_offline_only" = "Only if Offline"; +"lng_away_offline_only_about" = "Don't send the away message if you've recently been online."; "lng_away_recipients" = "Recipients"; "lng_away_select" = "Select chats or entire chat categories for sending an away message."; "lng_away_empty_title" = "New Away Message"; diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 739d12d2a..ae600e0de 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -48,6 +48,7 @@ private: rpl::event_stream<> _deactivateOnAttempt; rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<Data::AwaySchedule> _schedule; + rpl::variable<bool> _offlineOnly; rpl::variable<bool> _enabled; }; @@ -311,6 +312,18 @@ void AwayMessage::setupContent( }); Ui::AddSkip(inner); Ui::AddDivider(inner); + Ui::AddSkip(inner); + + const auto offlineOnly = inner->add( + object_ptr<Ui::SettingsButton>( + inner, + tr::lng_away_offline_only(), + st::settingsButtonNoIcon) + )->toggleOn(rpl::single(current.offlineOnly)); + _offlineOnly = offlineOnly->toggledValue(); + + Ui::AddSkip(inner); + Ui::AddDividerText(inner, tr::lng_away_offline_only_about()); AddBusinessRecipientsSelector(inner, { .controller = controller, @@ -327,10 +340,13 @@ void AwayMessage::setupContent( } void AwayMessage::save() { - controller()->session().data().businessInfo().saveAwaySettings( + const auto session = &controller()->session(); + session->data().businessInfo().saveAwaySettings( _enabled.current() ? Data::AwaySettings{ .recipients = _recipients.current(), .schedule = _schedule.current(), + .shortcutId = LookupShortcutId(session, u"away"_q), + .offlineOnly = _offlineOnly.current(), } : Data::AwaySettings()); } diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 43cba0763..e16cc6477 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -326,10 +326,12 @@ void Greeting::setupContent( } void Greeting::save() { - controller()->session().data().businessInfo().saveGreetingSettings( + const auto session = &controller()->session(); + session->data().businessInfo().saveGreetingSettings( _enabled.current() ? Data::GreetingSettings{ .recipients = _recipients.current(), .noActivityDays = _noActivityDays.current(), + .shortcutId = LookupShortcutId(session, u"hello"_q), } : Data::GreetingSettings()); } diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index 5787bb234..960c354b2 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -280,7 +280,7 @@ void AddBusinessRecipientsSelector( }); }, lifetime); - SetupBusinessChatsPreview(includeInner, excluded); + SetupBusinessChatsPreview(includeInner, included); includeWrap->toggleOn(data->value( ) | rpl::map([](const Data::BusinessRecipients &value) { @@ -374,4 +374,16 @@ rpl::producer<int> ShortcutMessagesLimitValue( }); } +BusinessShortcutId LookupShortcutId( + not_null<Main::Session*> session, + const QString &name) { + const auto messages = &session->data().shortcutMessages(); + for (const auto &[id, shortcut] : messages->shortcuts().list) { + if (shortcut.name == name) { + return id; + } + } + return {}; +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h index e05f702e6..f4432ea3b 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -93,4 +93,8 @@ void AddBusinessRecipientsSelector( [[nodiscard]] rpl::producer<int> ShortcutMessagesLimitValue( not_null<Main::Session*> session); +[[nodiscard]] BusinessShortcutId LookupShortcutId( + not_null<Main::Session*> session, + const QString &name); + } // namespace Settings diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 645c78285..8afc8d243 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -303,8 +303,8 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }}; windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }}; windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; -windowFilterTypeNewChats: icon {{ "folders/folders_unread", historyPeerUserpicFg }}; -windowFilterTypeExistingChats: windowFilterTypeNoRead; +windowFilterTypeNewChats: icon {{ "folders/folder_new_chats", historyPeerUserpicFg }}; +windowFilterTypeExistingChats: icon {{ "folders/folder_existing_chats", historyPeerUserpicFg }}; windowFilterChatsSectionSubtitleHeight: 28px; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { From a47c6f9c9a24877ae0b4bdcbf6c29a6ffba5dde2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 17:28:51 +0400 Subject: [PATCH 065/108] Show errors on business info savings. --- .../data/business/data_business_chatbots.cpp | 2 +- .../data/business/data_business_chatbots.h | 2 +- .../data/business/data_business_info.cpp | 63 +++++++--- .../data/business/data_business_info.h | 8 +- .../business/settings_away_message.cpp | 17 ++- .../settings/business/settings_chatbots.cpp | 14 ++- .../settings/business/settings_greeting.cpp | 110 +++++------------- .../business/settings_quick_replies.cpp | 10 +- .../business/settings_working_hours.cpp | 4 +- 9 files changed, 111 insertions(+), 119 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index 26dd21687..89215a2e0 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -27,7 +27,7 @@ rpl::producer<ChatbotsSettings> Chatbots::value() const { return _settings.value(); } -void Chatbots::save(ChatbotsSettings settings) { +void Chatbots::save(ChatbotsSettings settings, Fn<void(QString)> fail) { _settings = settings; } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index 13b8a894b..da088c394 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -30,7 +30,7 @@ public: [[nodiscard]] rpl::producer<ChatbotsSettings> changes() const; [[nodiscard]] rpl::producer<ChatbotsSettings> value() const; - void save(ChatbotsSettings settings); + void save(ChatbotsSettings settings, Fn<void(QString)> fail); private: const not_null<Session*> _session; diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index bd75f4af7..43ef345e6 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -118,20 +118,31 @@ BusinessInfo::BusinessInfo(not_null<Session*> owner) BusinessInfo::~BusinessInfo() = default; -void BusinessInfo::saveWorkingHours(WorkingHours data) { - auto details = _owner->session().user()->businessDetails(); - if (details.hours == data) { +void BusinessInfo::saveWorkingHours( + WorkingHours data, + Fn<void(QString)> fail) { + const auto session = &_owner->session(); + auto details = session->user()->businessDetails(); + const auto &was = details.hours; + if (was == data) { return; } using Flag = MTPaccount_UpdateBusinessWorkHours::Flag; - _owner->session().api().request(MTPaccount_UpdateBusinessWorkHours( + session->api().request(MTPaccount_UpdateBusinessWorkHours( MTP_flags(data ? Flag::f_business_work_hours : Flag()), ToMTP(data) - )).send(); + )).fail([=](const MTP::Error &error) { + auto details = session->user()->businessDetails(); + details.hours = was; + session->user()->setBusinessDetails(std::move(details)); + if (fail) { + fail(error.type()); + } + }).send(); details.hours = std::move(data); - _owner->session().user()->setBusinessDetails(std::move(details)); + session->user()->setBusinessDetails(std::move(details)); } void BusinessInfo::applyAwaySettings(AwaySettings data) { @@ -142,15 +153,25 @@ void BusinessInfo::applyAwaySettings(AwaySettings data) { _awaySettingsChanged.fire({}); } -void BusinessInfo::saveAwaySettings(AwaySettings data) { - if (_awaySettings == data) { +void BusinessInfo::saveAwaySettings( + AwaySettings data, + Fn<void(QString)> fail) { + const auto &was = _awaySettings; + if (was == data) { return; } using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; - _owner->session().api().request(MTPaccount_UpdateBusinessAwayMessage( + const auto session = &_owner->session(); + session->api().request(MTPaccount_UpdateBusinessAwayMessage( MTP_flags(data ? Flag::f_message : Flag()), data ? ToMTP(data) : MTPInputBusinessAwayMessage() - )).send(); + )).fail([=](const MTP::Error &error) { + _awaySettings = was; + _awaySettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); _awaySettings = std::move(data); _awaySettingsChanged.fire({}); @@ -176,15 +197,25 @@ void BusinessInfo::applyGreetingSettings(GreetingSettings data) { _greetingSettingsChanged.fire({}); } -void BusinessInfo::saveGreetingSettings(GreetingSettings data) { - if (_greetingSettings == data) { +void BusinessInfo::saveGreetingSettings( + GreetingSettings data, + Fn<void(QString)> fail) { + const auto &was = _greetingSettings; + if (was == data) { return; } using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag; - _owner->session().api().request(MTPaccount_UpdateBusinessGreetingMessage( - MTP_flags(data ? Flag::f_message : Flag()), - data ? ToMTP(data) : MTPInputBusinessGreetingMessage() - )).send(); + _owner->session().api().request( + MTPaccount_UpdateBusinessGreetingMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessGreetingMessage()) + ).fail([=](const MTP::Error &error) { + _greetingSettings = was; + _greetingSettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); _greetingSettings = std::move(data); _greetingSettingsChanged.fire({}); diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index e572d2757..3b747eb0f 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -20,15 +20,17 @@ public: void preload(); - void saveWorkingHours(WorkingHours data); + void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail); - void saveAwaySettings(AwaySettings data); + void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail); void applyAwaySettings(AwaySettings data); [[nodiscard]] AwaySettings awaySettings() const; [[nodiscard]] bool awaySettingsLoaded() const; [[nodiscard]] rpl::producer<> awaySettingsChanged() const; - void saveGreetingSettings(GreetingSettings data); + void saveGreetingSettings( + GreetingSettings data, + Fn<void(QString)> fail); void applyGreetingSettings(GreetingSettings data); [[nodiscard]] GreetingSettings greetingSettings() const; [[nodiscard]] bool greetingSettingsLoaded() const; diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index ae600e0de..4953bbdbd 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -213,7 +213,9 @@ void AwayMessage::setupContent( const auto current = info->awaySettings(); const auto disabled = (current.schedule.type == AwayScheduleType::Never); - _recipients = current.recipients; + _recipients = disabled + ? Data::BusinessRecipients{ .allButExcluded = true } + : current.recipients; auto initialSchedule = disabled ? AwaySchedule{ .type = AwayScheduleType::Always, } : current.schedule; @@ -340,14 +342,25 @@ void AwayMessage::setupContent( } void AwayMessage::save() { + const auto show = controller()->uiShow(); const auto session = &controller()->session(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + AssertIsDebug(); + show->showToast(u"Please choose at least one recipient."_q); + //tr::lng_greeting_recipients_empty(tr::now)); + } else if (error != u"SHORTCUT_INVALID"_q) { + show->showToast(error); + } + }; session->data().businessInfo().saveAwaySettings( _enabled.current() ? Data::AwaySettings{ .recipients = _recipients.current(), .schedule = _schedule.current(), .shortcutId = LookupShortcutId(session, u"away"_q), .offlineOnly = _offlineOnly.current(), - } : Data::AwaySettings()); + } : Data::AwaySettings(), + fail); } } // namespace diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 18d9c9545..d9879f470 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -189,12 +189,20 @@ void Chatbots::setupContent( } void Chatbots::save() { - const auto settings = Data::ChatbotsSettings{ + const auto show = controller()->uiShow(); + const auto session = &controller()->session(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + AssertIsDebug(); + show->showToast(u"Please choose at least one recipient."_q); + //tr::lng_greeting_recipients_empty(tr::now)); + } + }; + controller()->session().data().chatbots().save({ .bot = _botValue.current().bot, .recipients = _recipients.current(), .repliesAllowed = _repliesAllowed.current(), - }; - controller()->session().data().chatbots().save(settings); + }, [=](QString error) { show->showToast(error); }); } } // namespace diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index e16cc6477..6db47a7a0 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "settings/business/settings_shortcut_messages.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/boxes/time_picker_box.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -73,92 +74,22 @@ void EditPeriodBox( not_null<Ui::GenericBox*> box, int days, Fn<void(int)> save) { - auto values = base::flat_set<int>{ 7, 14, 21, 28 }; - if (!values.contains(days)) { - values.emplace(days); + auto values = std::vector{ 7, 14, 21, 28 }; + if (!ranges::contains(values, days)) { + values.push_back(days); + ranges::sort(values); } - const auto startIndex = int(values.find(days) - begin(values)); - const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>( - box, - st::settingsWorkingHoursPicker)); - - const auto font = st::boxTextFont; - const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; - auto paintCallback = [=]( - QPainter &p, - int index, - float64 y, - float64 distanceFromCenter, - int outerWidth) { - const auto r = QRectF(0, y, outerWidth, itemHeight); - const auto progress = std::abs(distanceFromCenter); - const auto revProgress = 1. - progress; - p.save(); - p.translate(r.center()); - constexpr auto kMinYScale = 0.2; - const auto yScale = kMinYScale - + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); - p.scale(1., yScale); - p.translate(-r.center()); - p.setOpacity(revProgress); - p.setFont(font); - p.setPen(st::defaultFlatLabel.textFg); - p.drawText( - r, - tr::lng_days(tr::now, lt_count, *(values.begin() + index)), - style::al_center); - p.restore(); - }; - - const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>( - content, - std::move(paintCallback), - int(values.size()), - itemHeight, - startIndex); - - content->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - picker->resize(s.width(), s.height()); - picker->moveToLeft((s.width() - picker->width()) / 2, 0); - }, content->lifetime()); - - content->paintRequest( - ) | rpl::start_with_next([=](const QRect &r) { - auto p = QPainter(content); - - p.fillRect(r, Qt::transparent); - - const auto lineRect = QRect( - 0, - content->height() / 2, - content->width(), - st::defaultInputField.borderActive); - p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); - p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); - }, content->lifetime()); - - base::install_event_filter(content, [=](not_null<QEvent*> e) { - if ((e->type() == QEvent::MouseButtonPress) - || (e->type() == QEvent::MouseButtonRelease) - || (e->type() == QEvent::MouseMove)) { - picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get())); - } else if (e->type() == QEvent::Wheel) { - picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get())); - } - return base::EventFilterResult::Continue; - }); - base::install_event_filter(box, [=](not_null<QEvent*> e) { - if (e->type() == QEvent::KeyPress) { - picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); - } - return base::EventFilterResult::Continue; - }); + const auto phrases = ranges::views::all( + values + ) | ranges::views::transform([](int days) { + return tr::lng_days(tr::now, lt_count, days); + }) | ranges::to_vector; + const auto take = TimePickerBox(box, values, phrases, days); box->addButton(tr::lng_settings_save(), [=] { const auto weak = Ui::MakeWeak(box); - save(*(begin(values) + picker->index())); + save(take()); if (const auto strong = weak.data()) { strong->closeBox(); } @@ -187,7 +118,9 @@ void Greeting::setupContent( const auto current = info->greetingSettings(); const auto disabled = !current.noActivityDays; - _recipients = current.recipients; + _recipients = disabled + ? Data::BusinessRecipients{ .allButExcluded = true } + : current.recipients; _noActivityDays = disabled ? kDefaultNoActivityDays : current.noActivityDays; @@ -326,13 +259,24 @@ void Greeting::setupContent( } void Greeting::save() { + const auto show = controller()->uiShow(); const auto session = &controller()->session(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + AssertIsDebug(); + show->showToast(u"Please choose at least one recipient."_q); + //tr::lng_greeting_recipients_empty(tr::now)); + } else if (error != u"SHORTCUT_INVALID"_q) { + show->showToast(error); + } + }; session->data().businessInfo().saveGreetingSettings( _enabled.current() ? Data::GreetingSettings{ .recipients = _recipients.current(), .noActivityDays = _noActivityDays.current(), .shortcutId = LookupShortcutId(session, u"hello"_q), - } : Data::GreetingSettings()); + } : Data::GreetingSettings(), + fail); } } // namespace diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index 53fb228ba..d45d94750 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -41,7 +41,6 @@ public: private: void setupContent(not_null<Window::SessionController*> controller); - void save(); rpl::variable<int> _count; @@ -54,11 +53,7 @@ QuickReplies::QuickReplies( setupContent(controller); } -QuickReplies::~QuickReplies() { - if (!Core::Quitting()) { - save(); - } -} +QuickReplies::~QuickReplies() = default; rpl::producer<QString> QuickReplies::title() { return tr::lng_replies_title(); @@ -160,9 +155,6 @@ void QuickReplies::setupContent( Ui::ResizeFitChild(this, content); } -void QuickReplies::save() { -} - } // namespace Type QuickRepliesId() { diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 42865a2c0..39ef6e793 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -673,8 +673,10 @@ void WorkingHours::setupContent( } void WorkingHours::save() { + const auto show = controller()->uiShow(); controller()->session().data().businessInfo().saveWorkingHours( - _enabled.current() ? _hours.current() : Data::WorkingHours()); + _enabled.current() ? _hours.current() : Data::WorkingHours(), + [=](QString error) { show->showToast(error); }); } } // namespace From f812166249a3ab52988bb360bddf39f06f7b6e6e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 18:01:35 +0400 Subject: [PATCH 066/108] Use server-side order for business features list. --- .../settings/business/settings_location.cpp | 2 + .../settings/settings_business.cpp | 55 ++++++++++++++----- .../SourceFiles/settings/settings_business.h | 3 + .../SourceFiles/settings/settings_premium.cpp | 8 +-- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index 67a865696..2d9895ed9 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -70,6 +70,7 @@ void Location::setupContent( const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); +#if 0 // #TODO location choosing AddDividerTextWithLottie(content, { .lottie = u"location"_q, .lottieSize = st::settingsCloudPasswordIconSize, @@ -91,6 +92,7 @@ void Location::setupContent( showFinishes() | rpl::start_with_next([=] { address->setFocus(); }, address->lifetime()); +#endif if (!mapSupported()) { AddDividerTextWithLottie(content, { diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index a8792783c..c94c63464 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/business/settings_away_message.h" #include "settings/business/settings_chatbots.h" @@ -58,19 +60,19 @@ using Order = std::vector<QString>; [[nodiscard]] Order FallbackOrder() { return Order{ - u"location"_q, - u"opening_hours"_q, + u"greeting_message"_q, + u"away_message"_q, u"quick_replies"_q, - u"greeting_messages"_q, - u"away_messages"_q, - u"chatbots"_q, + u"business_hours"_q, + u"business_location"_q, + u"business_bots"_q, }; } [[nodiscard]] base::flat_map<QString, Entry> EntryMap() { return base::flat_map<QString, Entry>{ { - u"location"_q, + u"business_location"_q, Entry{ &st::settingsBusinessIconLocation, tr::lng_business_subtitle_location(), @@ -79,7 +81,7 @@ using Order = std::vector<QString>; }, }, { - u"opening_hours"_q, + u"business_hours"_q, Entry{ &st::settingsBusinessIconHours, tr::lng_business_subtitle_opening_hours(), @@ -97,7 +99,7 @@ using Order = std::vector<QString>; }, }, { - u"greeting_messages"_q, + u"greeting_message"_q, Entry{ &st::settingsBusinessIconGreeting, tr::lng_business_subtitle_greeting_messages(), @@ -106,7 +108,7 @@ using Order = std::vector<QString>; }, }, { - u"away_messages"_q, + u"away_message"_q, Entry{ &st::settingsBusinessIconAway, tr::lng_business_subtitle_away_messages(), @@ -115,7 +117,7 @@ using Order = std::vector<QString>; }, }, { - u"chatbots"_q, + u"business_bots"_q, Entry{ &st::settingsBusinessIconChatbots, tr::lng_business_subtitle_chatbots(), @@ -222,9 +224,9 @@ void AddBusinessSummary( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = FallbackOrder();/* session->account().appConfig().get<Order>( - "premium_promo_order", - FallbackOrder());*/ AssertIsDebug() + const auto mtpOrder = account.appConfig().get<Order>( + "business_promo_order", + FallbackOrder()); const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); @@ -589,4 +591,31 @@ void ShowBusiness(not_null<Window::SessionController*> controller) { controller->showSettings(Settings::BusinessId()); } +std::vector<BusinessFeature> BusinessFeaturesOrder( + not_null<::Main::Session*> session) { + const auto mtpOrder = session->account().appConfig().get<Order>( + "business_promo_order", + FallbackOrder()); + return ranges::views::all( + mtpOrder + ) | ranges::views::transform([](const QString &s) { + if (s == u"greeting_message"_q) { + return BusinessFeature::GreetingMessages; + } else if (s == u"away_message"_q) { + return BusinessFeature::AwayMessages; + } else if (s == u"quick_replies"_q) { + return BusinessFeature::QuickReplies; + } else if (s == u"business_hours"_q) { + return BusinessFeature::OpeningHours; + } else if (s == u"business_location"_q) { + return BusinessFeature::Location; + } else if (s == u"business_bots"_q) { + return BusinessFeature::Chatbots; + } + return BusinessFeature::kCount; + }) | ranges::views::filter([](BusinessFeature feature) { + return (feature != BusinessFeature::kCount); + }) | ranges::to_vector; +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.h b/Telegram/SourceFiles/settings/settings_business.h index e255fd715..47bc2bca3 100644 --- a/Telegram/SourceFiles/settings/settings_business.h +++ b/Telegram/SourceFiles/settings/settings_business.h @@ -34,4 +34,7 @@ enum class BusinessFeature { void ShowBusiness(not_null<Window::SessionController*> controller); +[[nodiscard]] std::vector<BusinessFeature> BusinessFeaturesOrder( + not_null<::Main::Session*> session); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 809b8ba8e..3e3b8abac 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -180,7 +180,6 @@ using Order = std::vector<QString>; u"stories"_q, u"more_upload"_q, u"double_limits"_q, - u"business"_q, u"last_seen"_q, u"voice_to_text"_q, u"faster_download"_q, @@ -198,6 +197,7 @@ using Order = std::vector<QString>; u"infinite_reactions"_q, u"animated_userpics"_q, u"premium_stickers"_q, + u"business"_q, }; } @@ -1537,7 +1537,7 @@ not_null<Ui::GradientButton*> CreateSubscribeButton( return result; } -[[nodiscard]] std::vector<PremiumPreview> PremiumPreviewOrder( +std::vector<PremiumPreview> PremiumPreviewOrder( not_null<Main::Session*> session) { const auto mtpOrder = session->account().appConfig().get<Order>( "premium_promo_order", @@ -1684,9 +1684,9 @@ void AddSummaryPremium( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = FallbackOrder();/* session->account().appConfig().get<Order>( + const auto mtpOrder = account.appConfig().get<Order>( "premium_promo_order", - FallbackOrder());*/ AssertIsDebug() + FallbackOrder()); const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); From 88751896af79260517a6c95841b2571c9818975e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 18:37:50 +0400 Subject: [PATCH 067/108] PremiumPreview/BusinessFeature -> PremiumFeature. --- .../boxes/background_preview_box.cpp | 2 +- .../SourceFiles/boxes/edit_caption_box.cpp | 2 +- .../SourceFiles/boxes/gift_premium_box.cpp | 2 +- Telegram/SourceFiles/boxes/language_box.cpp | 2 +- .../SourceFiles/boxes/premium_preview_box.cpp | 217 +++++++++++------- .../SourceFiles/boxes/premium_preview_box.h | 17 +- Telegram/SourceFiles/boxes/send_files_box.cpp | 2 +- .../SourceFiles/core/local_url_handlers.cpp | 2 +- .../dialogs/dialogs_search_tags.cpp | 2 +- .../history/history_inner_widget.cpp | 2 +- .../history/history_item_helpers.cpp | 4 +- .../history/view/history_view_message.cpp | 2 +- .../view/history_view_sticker_toast.cpp | 2 +- .../view/history_view_transcribe_button.cpp | 2 +- .../info_profile_emoji_status_panel.cpp | 2 +- .../media/stories/media_stories_stealth.cpp | 2 +- .../media/view/media_view_overlay_widget.cpp | 2 +- .../settings/settings_business.cpp | 57 +++-- .../SourceFiles/settings/settings_business.h | 15 +- .../SourceFiles/settings/settings_premium.cpp | 86 +++---- .../SourceFiles/settings/settings_premium.h | 8 +- .../SourceFiles/window/section_widget.cpp | 4 +- .../SourceFiles/window/window_main_menu.cpp | 2 +- 23 files changed, 242 insertions(+), 196 deletions(-) diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 048362638..d445ce1f5 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -776,7 +776,7 @@ void BackgroundPreviewBox::applyForPeer() { } else { ShowPremiumPreviewBox( _controller->uiShow(), - PremiumPreview::Wallpapers); + PremiumFeature::Wallpapers); } }); const auto cancel = CreateChild<RoundButton>( diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 0c4b212c5..5ffdfb6f3 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -682,7 +682,7 @@ void EditCaptionBox::setupEmojiPanel() { && !_controller->session().premium()) { ShowPremiumPreviewBox( _controller, - PremiumPreview::AnimatedEmoji); + PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_field.get(), data.document); } diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index f544b5647..41f9cc097 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -587,7 +587,7 @@ void GiftsBox( const auto content = box->addRow( object_ptr<Ui::VerticalLayout>(box), {}); - auto buttonCallback = [=](PremiumPreview section) { + auto buttonCallback = [=](PremiumFeature section) { stars->setPaused(true); const auto previewBoxShown = [=]( not_null<Ui::BoxContent*> previewBox) { diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index aa69a6fee..98c49900a 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1216,7 +1216,7 @@ void LanguageBox::setupTop(not_null<Ui::VerticalLayout*> container) { if (checked && !premium) { ShowPremiumPreviewToBuy( _controller, - PremiumPreview::RealTimeTranslation); + PremiumFeature::RealTimeTranslation); _translateChatTurnOff.fire(false); } return premium diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index c3ce95189..76df0c2d7 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -60,7 +60,7 @@ constexpr auto kStarPeriod = 3 * crl::time(1000); using Data::ReactionId; struct Descriptor { - PremiumPreview section = PremiumPreview::Stickers; + PremiumFeature section = PremiumFeature::Stickers; DocumentData *requestedSticker = nullptr; bool fromSettings = false; Fn<void()> hiddenCallback; @@ -91,92 +91,118 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { media->videoThumbnailWanted(origin); } -[[nodiscard]] rpl::producer<QString> SectionTitle(PremiumPreview section) { +[[nodiscard]] rpl::producer<QString> SectionTitle(PremiumFeature section) { switch (section) { - case PremiumPreview::Wallpapers: + case PremiumFeature::Wallpapers: return tr::lng_premium_summary_subtitle_wallpapers(); - case PremiumPreview::Stories: + case PremiumFeature::Stories: return tr::lng_premium_summary_subtitle_stories(); - case PremiumPreview::DoubleLimits: + case PremiumFeature::DoubleLimits: return tr::lng_premium_summary_subtitle_double_limits(); - case PremiumPreview::MoreUpload: + case PremiumFeature::MoreUpload: return tr::lng_premium_summary_subtitle_more_upload(); - case PremiumPreview::FasterDownload: + case PremiumFeature::FasterDownload: return tr::lng_premium_summary_subtitle_faster_download(); - case PremiumPreview::VoiceToText: + case PremiumFeature::VoiceToText: return tr::lng_premium_summary_subtitle_voice_to_text(); - case PremiumPreview::NoAds: + case PremiumFeature::NoAds: return tr::lng_premium_summary_subtitle_no_ads(); - case PremiumPreview::EmojiStatus: + case PremiumFeature::EmojiStatus: return tr::lng_premium_summary_subtitle_emoji_status(); - case PremiumPreview::InfiniteReactions: + case PremiumFeature::InfiniteReactions: return tr::lng_premium_summary_subtitle_infinite_reactions(); - case PremiumPreview::TagsForMessages: + case PremiumFeature::TagsForMessages: return tr::lng_premium_summary_subtitle_tags_for_messages(); - case PremiumPreview::LastSeen: + case PremiumFeature::LastSeen: return tr::lng_premium_summary_subtitle_last_seen(); - case PremiumPreview::MessagePrivacy: + case PremiumFeature::MessagePrivacy: return tr::lng_premium_summary_subtitle_message_privacy(); - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return tr::lng_premium_summary_subtitle_premium_stickers(); - case PremiumPreview::AnimatedEmoji: + case PremiumFeature::AnimatedEmoji: return tr::lng_premium_summary_subtitle_animated_emoji(); - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::AdvancedChatManagement: return tr::lng_premium_summary_subtitle_advanced_chat_management(); - case PremiumPreview::ProfileBadge: + case PremiumFeature::ProfileBadge: return tr::lng_premium_summary_subtitle_profile_badge(); - case PremiumPreview::AnimatedUserpics: + case PremiumFeature::AnimatedUserpics: return tr::lng_premium_summary_subtitle_animated_userpics(); - case PremiumPreview::RealTimeTranslation: + case PremiumFeature::RealTimeTranslation: return tr::lng_premium_summary_subtitle_translation(); - case PremiumPreview::Business: + case PremiumFeature::Business: return tr::lng_premium_summary_subtitle_business(); + + case PremiumFeature::BusinessLocation: + return tr::lng_business_subtitle_location(); + case PremiumFeature::BusinessHours: + return tr::lng_business_subtitle_opening_hours(); + case PremiumFeature::QuickReplies: + return tr::lng_business_subtitle_quick_replies(); + case PremiumFeature::GreetingMessage: + return tr::lng_business_subtitle_greeting_messages(); + case PremiumFeature::AwayMessage: + return tr::lng_business_subtitle_away_messages(); + case PremiumFeature::BusinessBots: + return tr::lng_business_subtitle_chatbots(); } - Unexpected("PremiumPreview in SectionTitle."); + Unexpected("PremiumFeature in SectionTitle."); } -[[nodiscard]] rpl::producer<QString> SectionAbout(PremiumPreview section) { +[[nodiscard]] rpl::producer<QString> SectionAbout(PremiumFeature section) { switch (section) { - case PremiumPreview::Wallpapers: + case PremiumFeature::Wallpapers: return tr::lng_premium_summary_about_wallpapers(); - case PremiumPreview::Stories: + case PremiumFeature::Stories: return tr::lng_premium_summary_about_stories(); - case PremiumPreview::DoubleLimits: + case PremiumFeature::DoubleLimits: return tr::lng_premium_summary_about_double_limits(); - case PremiumPreview::MoreUpload: + case PremiumFeature::MoreUpload: return tr::lng_premium_summary_about_more_upload(); - case PremiumPreview::FasterDownload: + case PremiumFeature::FasterDownload: return tr::lng_premium_summary_about_faster_download(); - case PremiumPreview::VoiceToText: + case PremiumFeature::VoiceToText: return tr::lng_premium_summary_about_voice_to_text(); - case PremiumPreview::NoAds: + case PremiumFeature::NoAds: return tr::lng_premium_summary_about_no_ads(); - case PremiumPreview::EmojiStatus: + case PremiumFeature::EmojiStatus: return tr::lng_premium_summary_about_emoji_status(); - case PremiumPreview::InfiniteReactions: + case PremiumFeature::InfiniteReactions: return tr::lng_premium_summary_about_infinite_reactions(); - case PremiumPreview::TagsForMessages: + case PremiumFeature::TagsForMessages: return tr::lng_premium_summary_about_tags_for_messages(); - case PremiumPreview::LastSeen: + case PremiumFeature::LastSeen: return tr::lng_premium_summary_about_last_seen(); - case PremiumPreview::MessagePrivacy: + case PremiumFeature::MessagePrivacy: return tr::lng_premium_summary_about_message_privacy(); - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return tr::lng_premium_summary_about_premium_stickers(); - case PremiumPreview::AnimatedEmoji: + case PremiumFeature::AnimatedEmoji: return tr::lng_premium_summary_about_animated_emoji(); - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::AdvancedChatManagement: return tr::lng_premium_summary_about_advanced_chat_management(); - case PremiumPreview::ProfileBadge: + case PremiumFeature::ProfileBadge: return tr::lng_premium_summary_about_profile_badge(); - case PremiumPreview::AnimatedUserpics: + case PremiumFeature::AnimatedUserpics: return tr::lng_premium_summary_about_animated_userpics(); - case PremiumPreview::RealTimeTranslation: + case PremiumFeature::RealTimeTranslation: return tr::lng_premium_summary_about_translation(); - case PremiumPreview::Business: + case PremiumFeature::Business: return tr::lng_premium_summary_about_business(); + + case PremiumFeature::BusinessLocation: + return tr::lng_business_about_location(); + case PremiumFeature::BusinessHours: + return tr::lng_business_about_opening_hours(); + case PremiumFeature::QuickReplies: + return tr::lng_business_about_quick_replies(); + case PremiumFeature::GreetingMessage: + return tr::lng_business_about_greeting_messages(); + case PremiumFeature::AwayMessage: + return tr::lng_business_about_away_messages(); + case PremiumFeature::BusinessBots: + return tr::lng_business_about_chatbots(); } - Unexpected("PremiumPreview in SectionTitle."); + Unexpected("PremiumFeature in SectionTitle."); } [[nodiscard]] object_ptr<Ui::RpWidget> ChatBackPreview( @@ -468,33 +494,40 @@ struct VideoPreviewDocument { RectPart align = RectPart::Bottom; }; -[[nodiscard]] bool VideoAlignToTop(PremiumPreview section) { - return (section == PremiumPreview::MoreUpload) - || (section == PremiumPreview::NoAds) - || (section == PremiumPreview::AnimatedEmoji); +[[nodiscard]] bool VideoAlignToTop(PremiumFeature section) { + return (section == PremiumFeature::MoreUpload) + || (section == PremiumFeature::NoAds) + || (section == PremiumFeature::AnimatedEmoji); } [[nodiscard]] DocumentData *LookupVideo( not_null<Main::Session*> session, - PremiumPreview section) { + PremiumFeature section) { const auto name = [&] { switch (section) { - case PremiumPreview::MoreUpload: return "more_upload"; - case PremiumPreview::FasterDownload: return "faster_download"; - case PremiumPreview::VoiceToText: return "voice_to_text"; - case PremiumPreview::NoAds: return "no_ads"; - case PremiumPreview::AnimatedEmoji: return "animated_emoji"; - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::MoreUpload: return "more_upload"; + case PremiumFeature::FasterDownload: return "faster_download"; + case PremiumFeature::VoiceToText: return "voice_to_text"; + case PremiumFeature::NoAds: return "no_ads"; + case PremiumFeature::AnimatedEmoji: return "animated_emoji"; + case PremiumFeature::AdvancedChatManagement: return "advanced_chat_management"; - case PremiumPreview::EmojiStatus: return "emoji_status"; - case PremiumPreview::InfiniteReactions: return "infinite_reactions"; - case PremiumPreview::TagsForMessages: return "saved_tags"; - case PremiumPreview::ProfileBadge: return "profile_badge"; - case PremiumPreview::AnimatedUserpics: return "animated_userpics"; - case PremiumPreview::RealTimeTranslation: return "translations"; - case PremiumPreview::Wallpapers: return "wallpapers"; - case PremiumPreview::LastSeen: return "last_seen"; - case PremiumPreview::MessagePrivacy: return "message_privacy"; + case PremiumFeature::EmojiStatus: return "emoji_status"; + case PremiumFeature::InfiniteReactions: return "infinite_reactions"; + case PremiumFeature::TagsForMessages: return "saved_tags"; + case PremiumFeature::ProfileBadge: return "profile_badge"; + case PremiumFeature::AnimatedUserpics: return "animated_userpics"; + case PremiumFeature::RealTimeTranslation: return "translations"; + case PremiumFeature::Wallpapers: return "wallpapers"; + case PremiumFeature::LastSeen: return "last_seen"; + case PremiumFeature::MessagePrivacy: return "message_privacy"; + + case PremiumFeature::BusinessLocation: return "business_location"; + case PremiumFeature::BusinessHours: return "business_hours"; + case PremiumFeature::QuickReplies: return "quick_replies"; + case PremiumFeature::GreetingMessage: return "greeting_message"; + case PremiumFeature::AwayMessage: return "away_message"; + case PremiumFeature::BusinessBots: return "business_bots"; } return ""; }(); @@ -721,7 +754,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null<Ui::RpWidget*> GenericPreview( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void()> readyCallback) { const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get()); result->show(); @@ -762,10 +795,10 @@ struct VideoPreviewDocument { [[nodiscard]] not_null<Ui::RpWidget*> GenerateDefaultPreview( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void()> readyCallback) { switch (section) { - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return StickersPreview(parent, std::move(show), readyCallback); default: return GenericPreview( @@ -789,8 +822,8 @@ struct VideoPreviewDocument { [[nodiscard]] object_ptr<Ui::RpWidget> CreateSwitch( not_null<Ui::RpWidget*> parent, - not_null<rpl::variable<PremiumPreview>*> selected, - std::vector<PremiumPreview> order) { + not_null<rpl::variable<PremiumFeature>*> selected, + std::vector<PremiumFeature> order) { const auto padding = st::premiumDotPadding; const auto width = padding.left() + st::premiumDot + padding.right(); const auto height = padding.top() + st::premiumDot + padding.bottom(); @@ -861,14 +894,20 @@ void PreviewBox( Ui::Animations::Simple animation; Fn<void()> preload; std::vector<Hiding> hiding; - rpl::variable<PremiumPreview> selected; - std::vector<PremiumPreview> order; + rpl::variable<PremiumFeature> selected; + std::vector<PremiumFeature> order; }; const auto state = outer->lifetime().make_state<State>(); state->selected = descriptor.section; - state->order = Settings::PremiumPreviewOrder(&show->session()); + auto premiumOrder = Settings::PremiumFeaturesOrder(&show->session()); + auto businessOrder = Settings::BusinessFeaturesOrder(&show->session()); + state->order = ranges::contains(businessOrder, descriptor.section) + ? std::move(businessOrder) + : ranges::contains(businessOrder, descriptor.section) + ? std::move(premiumOrder) + : std::vector{ descriptor.section }; - const auto index = [=](PremiumPreview section) { + const auto index = [=](PremiumFeature section) { const auto it = ranges::find(state->order, section); return (it == end(state->order)) ? 0 @@ -911,7 +950,7 @@ void PreviewBox( return; } const auto now = state->selected.current(); - if (now != PremiumPreview::Stickers && !state->stickersPreload) { + if (now != PremiumFeature::Stickers && !state->stickersPreload) { const auto ready = [=] { if (state->stickersPreload) { state->stickersPreloadReady = true; @@ -922,14 +961,14 @@ void PreviewBox( state->stickersPreload = GenerateDefaultPreview( outer, show, - PremiumPreview::Stickers, + PremiumFeature::Stickers, ready); state->stickersPreload->hide(); } }; switch (descriptor.section) { - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: state->content = media ? StickerPreview(outer, show, media, state->preload) : StickersPreview(outer, show, state->preload); @@ -945,7 +984,7 @@ void PreviewBox( state->selected.value( ) | rpl::combine_previous( - ) | rpl::start_with_next([=](PremiumPreview was, PremiumPreview now) { + ) | rpl::start_with_next([=](PremiumFeature was, PremiumFeature now) { const auto animationCallback = [=] { if (!state->animation.animating()) { for (const auto &hiding : base::take(state->hiding)) { @@ -987,7 +1026,7 @@ void PreviewBox( .leftTill = state->content->x() - start, }); state->leftFrom = start; - if (now == PremiumPreview::Stickers && state->stickersPreload) { + if (now == PremiumFeature::Stickers && state->stickersPreload) { state->content = base::take(state->stickersPreload); state->content->show(); if (base::take(state->stickersPreloadReady)) { @@ -1058,14 +1097,14 @@ void PreviewBox( return Settings::LookupPremiumRef(state->selected.current()); }; auto unlock = state->selected.value( - ) | rpl::map([=](PremiumPreview section) { - return (section == PremiumPreview::InfiniteReactions) + ) | rpl::map([=](PremiumFeature section) { + return (section == PremiumFeature::InfiniteReactions) ? tr::lng_premium_unlock_reactions() - : (section == PremiumPreview::Stickers) + : (section == PremiumFeature::Stickers) ? tr::lng_premium_unlock_stickers() - : (section == PremiumPreview::AnimatedEmoji) + : (section == PremiumFeature::AnimatedEmoji) ? tr::lng_premium_unlock_emoji() - : (section == PremiumPreview::EmojiStatus) + : (section == PremiumFeature::EmojiStatus) ? tr::lng_premium_unlock_status() : tr::lng_premium_more_about(); }) | rpl::flatten_latest(); @@ -1212,19 +1251,19 @@ void Show( descriptor.shownCallback(raw); } return; - } else if (descriptor.section == PremiumPreview::DoubleLimits) { + } else if (descriptor.section == PremiumFeature::DoubleLimits) { show->showBox(Box([=](not_null<Ui::GenericBox*> box) { DoubledLimitsPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; - } else if (descriptor.section == PremiumPreview::Stories) { + } else if (descriptor.section == PremiumFeature::Stories) { show->showBox(Box([=](not_null<Ui::GenericBox*> box) { UpgradedStoriesPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; - } else if (descriptor.section == PremiumPreview::Business) { + } else if (descriptor.section == PremiumFeature::Business) { const auto window = show->resolveWindow( ChatHelpers::WindowUsage::PremiumPromo); if (window) { @@ -1298,21 +1337,21 @@ void ShowStickerPreviewBox( std::shared_ptr<ChatHelpers::Show> show, not_null<DocumentData*> document) { Show(std::move(show), Descriptor{ - .section = PremiumPreview::Stickers, + .section = PremiumFeature::Stickers, .requestedSticker = document, }); } void ShowPremiumPreviewBox( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown) { ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); } void ShowPremiumPreviewBox( std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown, bool hideSubscriptionButton) { Show(std::move(show), Descriptor{ @@ -1324,7 +1363,7 @@ void ShowPremiumPreviewBox( void ShowPremiumPreviewToBuy( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void()> hiddenCallback) { Show(controller->uiShow(), Descriptor{ .section = section, diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 9fc4da279..80400a6ee 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -45,7 +45,8 @@ void UpgradedStoriesPreviewBox( not_null<Ui::GenericBox*> box, not_null<Main::Session*> session); -enum class PremiumPreview { +enum class PremiumFeature { + // Premium features. Stories, DoubleLimits, MoreUpload, @@ -66,23 +67,31 @@ enum class PremiumPreview { MessagePrivacy, Business, + // Business features. + BusinessLocation, + BusinessHours, + QuickReplies, + GreetingMessage, + AwayMessage, + BusinessBots, + kCount, }; void ShowPremiumPreviewBox( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr); void ShowPremiumPreviewBox( std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr, bool hideSubscriptionButton = false); void ShowPremiumPreviewToBuy( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void()> hiddenCallback = nullptr); void PremiumUnavailableBox(not_null<Ui::GenericBox*> box); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 455630fb1..ea208d42f 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1135,7 +1135,7 @@ void SendFilesBox::setupEmojiPanel() { _captionToPeer, data.document) : (_limits & SendFilesAllow::EmojiWithoutPremium))) { - ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); } diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 82c115774..92ed6c9e8 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -665,7 +665,7 @@ bool ShowSearchTagsPromo( if (!controller) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages); + ShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages); return true; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index a3d1264ff..276d4e5b6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -47,7 +47,7 @@ namespace { if (const auto controller = my.sessionWindow.get()) { ShowPremiumPreviewBox( controller, - PremiumPreview::TagsForMessages); + PremiumFeature::TagsForMessages); } }); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0943e3ac1..a2188d7d5 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -160,7 +160,7 @@ void FillSponsoredMessagesMenu( menu->addSeparator(&st::expandedMenuSeparator); } menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { - ShowPremiumPreviewBox(controller, PremiumPreview::NoAds); + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); }, &st::menuIconCancel); } diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 65153bf86..e6c9b1e5a 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -362,7 +362,7 @@ ClickHandlerPtr HideSponsoredClickHandler() { return std::make_shared<LambdaClickHandler>([=](ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); if (const auto controller = my.sessionWindow.get()) { - ShowPremiumPreviewBox(controller, PremiumPreview::NoAds); + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); } }); } @@ -793,7 +793,7 @@ void ShowTrialTranscribesToast(int left, TimeId until) { } const auto filter = [=](const auto &...) { if (const auto controller = window->sessionController()) { - ShowPremiumPreviewBox(controller, PremiumPreview::VoiceToText); + ShowPremiumPreviewBox(controller, PremiumFeature::VoiceToText); window->activate(); } return false; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index f08ed8414..0c88e71e6 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3031,7 +3031,7 @@ void Message::refreshReactions() { = ExtractController(context)) { ShowPremiumPreviewBox( controller, - PremiumPreview::TagsForMessages); + PremiumFeature::TagsForMessages); } return; } diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp index f10d8e328..fc4f41b73 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp @@ -238,7 +238,7 @@ void StickerToast::showWithTitle(const QString &title) { && (i->second->flags & Data::StickersSetFlag::Installed)) { ShowPremiumPreviewBox( _controller, - PremiumPreview::AnimatedEmoji); + PremiumFeature::AnimatedEmoji); } else { _controller->show(Box<StickerSetBox>( _controller->uiShow(), diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp index dccca0f3a..1688b8f6c 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp @@ -272,7 +272,7 @@ ClickHandlerPtr TranscribeButton::link() { if (const auto controller = my.sessionWindow.get()) { ShowPremiumPreviewBox( controller, - PremiumPreview::VoiceToText); + PremiumFeature::VoiceToText); } } else { const auto max = session->api().transcribes().trialsMaxLengthMs(); diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index 8881dbc99..c7f26b4ef 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -281,7 +281,7 @@ bool EmojiStatusPanel::filter( if (_chooseFilter) { return _chooseFilter(chosenId); } else if (chosenId && !controller->session().premium()) { - ShowPremiumPreviewBox(controller, PremiumPreview::EmojiStatus); + ShowPremiumPreviewBox(controller, PremiumFeature::EmojiStatus); return false; } return true; diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp index 404ff1aea..80d4f20a7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -352,7 +352,7 @@ struct Feature { data->requested = false; const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto window = show->resolveWindow(usage)) { - ShowPremiumPreviewBox(window, PremiumPreview::Stories); + ShowPremiumPreviewBox(window, PremiumFeature::Stories); window->window().activate(); } } else if (now.mode.cooldownTill > now.now) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 3bbc98fb9..4c8a0cf70 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1254,7 +1254,7 @@ void OverlayWidget::showPremiumDownloadPromo() { const auto filter = [=](const auto &...) { const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto window = uiShow()->resolveWindow(usage)) { - ShowPremiumPreviewBox(window, PremiumPreview::Stories); + ShowPremiumPreviewBox(window, PremiumFeature::Stories); window->window().activate(); } return false; diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index c94c63464..c0848e341 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -53,7 +53,7 @@ struct Entry { const style::icon *icon; rpl::producer<QString> title; rpl::producer<QString> description; - BusinessFeature feature = BusinessFeature::Location; + PremiumFeature feature = PremiumFeature::BusinessLocation; }; using Order = std::vector<QString>; @@ -77,7 +77,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconLocation, tr::lng_business_subtitle_location(), tr::lng_business_about_location(), - BusinessFeature::Location, + PremiumFeature::BusinessLocation, }, }, { @@ -86,7 +86,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconHours, tr::lng_business_subtitle_opening_hours(), tr::lng_business_about_opening_hours(), - BusinessFeature::OpeningHours, + PremiumFeature::BusinessHours, }, }, { @@ -95,7 +95,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconReplies, tr::lng_business_subtitle_quick_replies(), tr::lng_business_about_quick_replies(), - BusinessFeature::QuickReplies, + PremiumFeature::QuickReplies, }, }, { @@ -104,7 +104,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconGreeting, tr::lng_business_subtitle_greeting_messages(), tr::lng_business_about_greeting_messages(), - BusinessFeature::GreetingMessages, + PremiumFeature::GreetingMessage, }, }, { @@ -113,7 +113,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconAway, tr::lng_business_subtitle_away_messages(), tr::lng_business_about_away_messages(), - BusinessFeature::AwayMessages, + PremiumFeature::AwayMessage, }, }, { @@ -122,7 +122,7 @@ using Order = std::vector<QString>; &st::settingsBusinessIconChatbots, tr::lng_business_subtitle_chatbots(), tr::lng_business_about_chatbots(), - BusinessFeature::Chatbots, + PremiumFeature::BusinessBots, }, }, }; @@ -131,7 +131,7 @@ using Order = std::vector<QString>; void AddBusinessSummary( not_null<Ui::VerticalLayout*> content, not_null<Window::SessionController*> controller, - Fn<void(BusinessFeature)> buttonCallback) { + Fn<void(PremiumFeature)> buttonCallback) { const auto &stDefault = st::settingsButton; const auto &stLabel = st::defaultFlatLabel; const auto iconSize = st::settingsPremiumIconDouble.size(); @@ -359,15 +359,22 @@ void Business::setupContent() { Ui::AddSkip(content, st::settingsFromFileTop); - AddBusinessSummary(content, _controller, [=](BusinessFeature feature) { + AddBusinessSummary(content, _controller, [=](PremiumFeature feature) { + if (!_controller->session().premium()) { + _setPaused(true); + const auto hidden = crl::guard(this, [=] { _setPaused(false); }); + + ShowPremiumPreviewToBuy(_controller, feature, hidden); + return; + } showOther([&] { switch (feature) { - case BusinessFeature::AwayMessages: return AwayMessageId(); - case BusinessFeature::OpeningHours: return WorkingHoursId(); - case BusinessFeature::Location: return LocationId(); - case BusinessFeature::GreetingMessages: return GreetingId(); - case BusinessFeature::QuickReplies: return QuickRepliesId(); - case BusinessFeature::Chatbots: return ChatbotsId(); + case PremiumFeature::AwayMessage: return AwayMessageId(); + case PremiumFeature::BusinessHours: return WorkingHoursId(); + case PremiumFeature::BusinessLocation: return LocationId(); + case PremiumFeature::GreetingMessage: return GreetingId(); + case PremiumFeature::QuickReplies: return QuickRepliesId(); + case PremiumFeature::BusinessBots: return ChatbotsId(); } Unexpected("Feature in Business::setupContent."); }()); @@ -591,7 +598,7 @@ void ShowBusiness(not_null<Window::SessionController*> controller) { controller->showSettings(Settings::BusinessId()); } -std::vector<BusinessFeature> BusinessFeaturesOrder( +std::vector<PremiumFeature> BusinessFeaturesOrder( not_null<::Main::Session*> session) { const auto mtpOrder = session->account().appConfig().get<Order>( "business_promo_order", @@ -600,21 +607,21 @@ std::vector<BusinessFeature> BusinessFeaturesOrder( mtpOrder ) | ranges::views::transform([](const QString &s) { if (s == u"greeting_message"_q) { - return BusinessFeature::GreetingMessages; + return PremiumFeature::GreetingMessage; } else if (s == u"away_message"_q) { - return BusinessFeature::AwayMessages; + return PremiumFeature::AwayMessage; } else if (s == u"quick_replies"_q) { - return BusinessFeature::QuickReplies; + return PremiumFeature::QuickReplies; } else if (s == u"business_hours"_q) { - return BusinessFeature::OpeningHours; + return PremiumFeature::BusinessHours; } else if (s == u"business_location"_q) { - return BusinessFeature::Location; + return PremiumFeature::BusinessLocation; } else if (s == u"business_bots"_q) { - return BusinessFeature::Chatbots; + return PremiumFeature::BusinessBots; } - return BusinessFeature::kCount; - }) | ranges::views::filter([](BusinessFeature feature) { - return (feature != BusinessFeature::kCount); + return PremiumFeature::kCount; + }) | ranges::views::filter([](PremiumFeature feature) { + return (feature != PremiumFeature::kCount); }) | ranges::to_vector; } diff --git a/Telegram/SourceFiles/settings/settings_business.h b/Telegram/SourceFiles/settings/settings_business.h index 47bc2bca3..6bd077af0 100644 --- a/Telegram/SourceFiles/settings/settings_business.h +++ b/Telegram/SourceFiles/settings/settings_business.h @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" +enum class PremiumFeature; + namespace Main { class Session; } // namespace Main @@ -19,22 +21,11 @@ class SessionController; namespace Settings { -enum class BusinessFeature { - Location, - OpeningHours, - QuickReplies, - GreetingMessages, - AwayMessages, - Chatbots, - - kCount, -}; - [[nodiscard]] Type BusinessId(); void ShowBusiness(not_null<Window::SessionController*> controller); -[[nodiscard]] std::vector<BusinessFeature> BusinessFeaturesOrder( +[[nodiscard]] std::vector<PremiumFeature> BusinessFeaturesOrder( not_null<::Main::Session*> session); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 3e3b8abac..2b9d5e35a 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -169,7 +169,7 @@ struct Entry { const style::icon *icon; rpl::producer<QString> title; rpl::producer<QString> description; - PremiumPreview section = PremiumPreview::DoubleLimits; + PremiumFeature section = PremiumFeature::DoubleLimits; bool newBadge = false; }; @@ -209,7 +209,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconTags, tr::lng_premium_summary_subtitle_tags_for_messages(), tr::lng_premium_summary_about_tags_for_messages(), - PremiumPreview::TagsForMessages, + PremiumFeature::TagsForMessages, true, }, }, @@ -219,7 +219,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconLastSeen, tr::lng_premium_summary_subtitle_last_seen(), tr::lng_premium_summary_about_last_seen(), - PremiumPreview::LastSeen, + PremiumFeature::LastSeen, true, }, }, @@ -229,7 +229,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconPrivacy, tr::lng_premium_summary_subtitle_message_privacy(), tr::lng_premium_summary_about_message_privacy(), - PremiumPreview::MessagePrivacy, + PremiumFeature::MessagePrivacy, true, }, }, @@ -239,7 +239,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconWallpapers, tr::lng_premium_summary_subtitle_wallpapers(), tr::lng_premium_summary_about_wallpapers(), - PremiumPreview::Wallpapers, + PremiumFeature::Wallpapers, }, }, { @@ -248,7 +248,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStories, tr::lng_premium_summary_subtitle_stories(), tr::lng_premium_summary_about_stories(), - PremiumPreview::Stories, + PremiumFeature::Stories, }, }, { @@ -257,7 +257,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconDouble, tr::lng_premium_summary_subtitle_double_limits(), tr::lng_premium_summary_about_double_limits(), - PremiumPreview::DoubleLimits, + PremiumFeature::DoubleLimits, }, }, { @@ -266,7 +266,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconFiles, tr::lng_premium_summary_subtitle_more_upload(), tr::lng_premium_summary_about_more_upload(), - PremiumPreview::MoreUpload, + PremiumFeature::MoreUpload, }, }, { @@ -275,7 +275,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconSpeed, tr::lng_premium_summary_subtitle_faster_download(), tr::lng_premium_summary_about_faster_download(), - PremiumPreview::FasterDownload, + PremiumFeature::FasterDownload, }, }, { @@ -284,7 +284,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconVoice, tr::lng_premium_summary_subtitle_voice_to_text(), tr::lng_premium_summary_about_voice_to_text(), - PremiumPreview::VoiceToText, + PremiumFeature::VoiceToText, }, }, { @@ -293,7 +293,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconChannelsOff, tr::lng_premium_summary_subtitle_no_ads(), tr::lng_premium_summary_about_no_ads(), - PremiumPreview::NoAds, + PremiumFeature::NoAds, }, }, { @@ -302,7 +302,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStatus, tr::lng_premium_summary_subtitle_emoji_status(), tr::lng_premium_summary_about_emoji_status(), - PremiumPreview::EmojiStatus, + PremiumFeature::EmojiStatus, }, }, { @@ -311,7 +311,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconLike, tr::lng_premium_summary_subtitle_infinite_reactions(), tr::lng_premium_summary_about_infinite_reactions(), - PremiumPreview::InfiniteReactions, + PremiumFeature::InfiniteReactions, }, }, { @@ -320,7 +320,7 @@ using Order = std::vector<QString>; &st::settingsIconStickers, tr::lng_premium_summary_subtitle_premium_stickers(), tr::lng_premium_summary_about_premium_stickers(), - PremiumPreview::Stickers, + PremiumFeature::Stickers, }, }, { @@ -329,7 +329,7 @@ using Order = std::vector<QString>; &st::settingsIconEmoji, tr::lng_premium_summary_subtitle_animated_emoji(), tr::lng_premium_summary_about_animated_emoji(), - PremiumPreview::AnimatedEmoji, + PremiumFeature::AnimatedEmoji, }, }, { @@ -338,7 +338,7 @@ using Order = std::vector<QString>; &st::settingsIconChat, tr::lng_premium_summary_subtitle_advanced_chat_management(), tr::lng_premium_summary_about_advanced_chat_management(), - PremiumPreview::AdvancedChatManagement, + PremiumFeature::AdvancedChatManagement, }, }, { @@ -347,7 +347,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStar, tr::lng_premium_summary_subtitle_profile_badge(), tr::lng_premium_summary_about_profile_badge(), - PremiumPreview::ProfileBadge, + PremiumFeature::ProfileBadge, }, }, { @@ -356,7 +356,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconPlay, tr::lng_premium_summary_subtitle_animated_userpics(), tr::lng_premium_summary_about_animated_userpics(), - PremiumPreview::AnimatedUserpics, + PremiumFeature::AnimatedUserpics, }, }, { @@ -365,7 +365,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconTranslations, tr::lng_premium_summary_subtitle_translation(), tr::lng_premium_summary_about_translation(), - PremiumPreview::RealTimeTranslation, + PremiumFeature::RealTimeTranslation, }, }, { @@ -374,7 +374,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconPlay, AssertIsDebug() tr::lng_premium_summary_subtitle_business(), tr::lng_premium_summary_about_business(), - PremiumPreview::Business, + PremiumFeature::Business, true, }, }, @@ -971,7 +971,7 @@ void Premium::setupContent() { setupSubscriptionOptions(content); - auto buttonCallback = [=](PremiumPreview section) { + auto buttonCallback = [=](PremiumFeature section) { _setPaused(true); const auto hidden = crl::guard(this, [=] { _setPaused(false); }); @@ -1350,7 +1350,7 @@ void StartPremiumPayment( } } -QString LookupPremiumRef(PremiumPreview section) { +QString LookupPremiumRef(PremiumFeature section) { for (const auto &[ref, entry] : EntryMap()) { if (entry.section == section) { return ref; @@ -1537,7 +1537,7 @@ not_null<Ui::GradientButton*> CreateSubscribeButton( return result; } -std::vector<PremiumPreview> PremiumPreviewOrder( +std::vector<PremiumFeature> PremiumFeaturesOrder( not_null<Main::Session*> session) { const auto mtpOrder = session->account().appConfig().get<Order>( "premium_promo_order", @@ -1546,41 +1546,41 @@ std::vector<PremiumPreview> PremiumPreviewOrder( mtpOrder ) | ranges::views::transform([](const QString &s) { if (s == u"more_upload"_q) { - return PremiumPreview::MoreUpload; + return PremiumFeature::MoreUpload; } else if (s == u"faster_download"_q) { - return PremiumPreview::FasterDownload; + return PremiumFeature::FasterDownload; } else if (s == u"voice_to_text"_q) { - return PremiumPreview::VoiceToText; + return PremiumFeature::VoiceToText; } else if (s == u"no_ads"_q) { - return PremiumPreview::NoAds; + return PremiumFeature::NoAds; } else if (s == u"emoji_status"_q) { - return PremiumPreview::EmojiStatus; + return PremiumFeature::EmojiStatus; } else if (s == u"infinite_reactions"_q) { - return PremiumPreview::InfiniteReactions; + return PremiumFeature::InfiniteReactions; } else if (s == u"saved_tags"_q) { - return PremiumPreview::TagsForMessages; + return PremiumFeature::TagsForMessages; } else if (s == u"last_seen"_q) { - return PremiumPreview::LastSeen; + return PremiumFeature::LastSeen; } else if (s == u"message_privacy"_q) { - return PremiumPreview::MessagePrivacy; + return PremiumFeature::MessagePrivacy; } else if (s == u"premium_stickers"_q) { - return PremiumPreview::Stickers; + return PremiumFeature::Stickers; } else if (s == u"animated_emoji"_q) { - return PremiumPreview::AnimatedEmoji; + return PremiumFeature::AnimatedEmoji; } else if (s == u"advanced_chat_management"_q) { - return PremiumPreview::AdvancedChatManagement; + return PremiumFeature::AdvancedChatManagement; } else if (s == u"profile_badge"_q) { - return PremiumPreview::ProfileBadge; + return PremiumFeature::ProfileBadge; } else if (s == u"animated_userpics"_q) { - return PremiumPreview::AnimatedUserpics; + return PremiumFeature::AnimatedUserpics; } else if (s == u"translations"_q) { - return PremiumPreview::RealTimeTranslation; + return PremiumFeature::RealTimeTranslation; } else if (s == u"wallpapers"_q) { - return PremiumPreview::Wallpapers; + return PremiumFeature::Wallpapers; } - return PremiumPreview::kCount; - }) | ranges::views::filter([](PremiumPreview type) { - return (type != PremiumPreview::kCount); + return PremiumFeature::kCount; + }) | ranges::views::filter([](PremiumFeature type) { + return (type != PremiumFeature::kCount); }) | ranges::to_vector; } @@ -1588,7 +1588,7 @@ void AddSummaryPremium( not_null<Ui::VerticalLayout*> content, not_null<Window::SessionController*> controller, const QString &ref, - Fn<void(PremiumPreview)> buttonCallback) { + Fn<void(PremiumFeature)> buttonCallback) { const auto &stDefault = st::settingsButton; const auto &stLabel = st::defaultFlatLabel; const auto iconSize = st::settingsPremiumIconDouble.size(); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index a8603aca6..6dd571812 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" -enum class PremiumPreview; +enum class PremiumFeature; namespace style { struct RoundButton; @@ -57,7 +57,7 @@ void StartPremiumPayment( not_null<Window::SessionController*> controller, const QString &ref); -[[nodiscard]] QString LookupPremiumRef(PremiumPreview section); +[[nodiscard]] QString LookupPremiumRef(PremiumFeature section); void ShowPremiumPromoToast( std::shared_ptr<ChatHelpers::Show> show, @@ -91,14 +91,14 @@ struct SubscribeButtonArgs final { [[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton( SubscribeButtonArgs &&args); -[[nodiscard]] std::vector<PremiumPreview> PremiumPreviewOrder( +[[nodiscard]] std::vector<PremiumFeature> PremiumFeaturesOrder( not_null<::Main::Session*> session); void AddSummaryPremium( not_null<Ui::VerticalLayout*> content, not_null<Window::SessionController*> controller, const QString &ref, - Fn<void(PremiumPreview)> buttonCallback); + Fn<void(PremiumFeature)> buttonCallback); } // namespace Settings diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index dcba6476c..4d7a48c7f 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -529,7 +529,7 @@ bool ShowReactPremiumError( if (controller->session().premium()) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages); + ShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages); return true; } else if (controller->session().premium() || ranges::contains(item->chosenReactions(), id) @@ -538,7 +538,7 @@ bool ShowReactPremiumError( } else if (!id.custom()) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::InfiniteReactions); + ShowPremiumPreviewBox(controller, PremiumFeature::InfiniteReactions); return true; } diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 051a99d8b..e4f941d9e 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -1031,7 +1031,7 @@ void MainMenu::chooseEmojiStatus() { if (const auto widget = _badge->widget()) { _emojiStatusPanel->show(_controller, widget, _badge->sizeTag()); } else { - ShowPremiumPreviewBox(_controller, PremiumPreview::EmojiStatus); + ShowPremiumPreviewBox(_controller, PremiumFeature::EmojiStatus); } } From c94da177d79848766c1c47e13898c37761c48065 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 1 Mar 2024 22:18:01 +0400 Subject: [PATCH 068/108] Fix build with Xcode. --- Telegram/SourceFiles/data/business/data_business_common.cpp | 2 +- .../SourceFiles/settings/business/settings_away_message.cpp | 4 ++-- Telegram/SourceFiles/settings/business/settings_chatbots.cpp | 4 ++-- Telegram/SourceFiles/settings/business/settings_greeting.cpp | 4 ++-- Telegram/SourceFiles/settings/business/settings_location.cpp | 2 +- .../SourceFiles/settings/business/settings_working_hours.cpp | 2 +- .../cloud_password/settings_cloud_password_email_confirm.cpp | 2 +- Telegram/SourceFiles/settings/settings_chat.h | 2 +- Telegram/SourceFiles/settings/settings_common.h | 2 +- Telegram/SourceFiles/settings/settings_common_session.h | 2 +- Telegram/SourceFiles/settings/settings_main.h | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 956807f5d..34a46f3cc 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -149,7 +149,7 @@ WorkingIntervals ReplaceDayIntervals( end(result.list), begin(replacement.list), end(replacement.list)); - for (auto &interval : ranges::subrange(first, end(result.list))) { + for (auto &interval : ranges::make_subrange(first, end(result.list))) { interval = interval.shifted(dayIndex * kDay); } return result.normalized(); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 4953bbdbd..04dfe993a 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -55,7 +55,7 @@ private: [[nodiscard]] TimeId StartTimeMin() { // Telegram was launched in August 2013 :) - return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1))); + return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1), QTime(0, 0))); } [[nodiscard]] TimeId EndTimeMin() { @@ -260,7 +260,7 @@ void AwayMessage::setupContent( _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { if (!_canHave.current()) { controller->showToast({ - .text = tr::lng_away_limit_reached(tr::now), + .text = { tr::lng_away_limit_reached(tr::now) }, .adaptive = true, }); _deactivateOnAttempt.fire({}); diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d9879f470..d3f112483 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -47,7 +47,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - const Ui::RoundRect *bottomSkipRounding() const { + const Ui::RoundRect *bottomSkipRounding() const override { return &_bottomSkipRounding; } @@ -59,7 +59,7 @@ private: rpl::variable<Data::BusinessRecipients> _recipients; rpl::variable<QString> _usernameValue; - rpl::variable<BotState> _botValue = nullptr; + rpl::variable<BotState> _botValue; rpl::variable<bool> _repliesAllowed = true; }; diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 6db47a7a0..3c73f391f 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -44,7 +44,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - const Ui::RoundRect *bottomSkipRounding() const { + const Ui::RoundRect *bottomSkipRounding() const override { return &_bottomSkipRounding; } @@ -157,7 +157,7 @@ void Greeting::setupContent( _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { if (!_canHave.current()) { controller->showToast({ - .text = tr::lng_greeting_limit_reached(tr::now), + .text = { tr::lng_greeting_limit_reached(tr::now) }, .adaptive = true, }); _deactivateOnAttempt.fire({}); diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index 2d9895ed9..4a2f14e73 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -32,7 +32,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - const Ui::RoundRect *bottomSkipRounding() const { + const Ui::RoundRect *bottomSkipRounding() const override { return mapSupported() ? nullptr : &_bottomSkipRounding; } diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 39ef6e793..dd6c54b66 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -462,7 +462,7 @@ void AddWeekButton( [=] { toggleButton->update(); }); auto status = data->value( - ) | rpl::map([=](const Data::WorkingHours &data) { + ) | rpl::map([=](const Data::WorkingHours &data) -> rpl::producer<QString> { using namespace Data; const auto intervals = ExtractDayIntervals(data.intervals, index); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp index 19bb96969..b1879387e 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp @@ -60,7 +60,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] void fillTopBarMenu( + void fillTopBarMenu( const Ui::Menu::MenuCallback &addAction) override; void setupContent(); diff --git a/Telegram/SourceFiles/settings/settings_chat.h b/Telegram/SourceFiles/settings/settings_chat.h index d1256de96..70724ea2b 100644 --- a/Telegram/SourceFiles/settings/settings_chat.h +++ b/Telegram/SourceFiles/settings/settings_chat.h @@ -44,7 +44,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] void fillTopBarMenu( + void fillTopBarMenu( const Ui::Menu::MenuCallback &addAction) override; private: diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 47bf52774..c279640a5 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -102,7 +102,7 @@ public: } virtual void selectionAction(Info::SelectionAction action) { } - [[nodiscard]] virtual void fillTopBarMenu( + virtual void fillTopBarMenu( const Ui::Menu::MenuCallback &addAction) { } diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 2b439bacf..8bea347a0 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -79,7 +79,7 @@ public: [[nodiscard]] rpl::producer<Type> sectionShowOther() final override { return _showOtherRequests.events(); } - [[nodiscard]] void showOther(Type type) { + void showOther(Type type) { _showOtherRequests.fire_copy(type); } [[nodiscard]] Fn<void(Type)> showOtherMethod() { diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index 4356a49b3..7040fb9fa 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -38,7 +38,7 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - [[nodiscard]] void fillTopBarMenu( + void fillTopBarMenu( const Ui::Menu::MenuCallback &addAction) override; protected: From ee847bc1a36f086ce97bf9d0ae74e4017fa0973c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Sat, 2 Mar 2024 11:12:34 +0400 Subject: [PATCH 069/108] Fix legacy group pins loading on first group open. Fixes #27466. --- Telegram/SourceFiles/apiwrap.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 4676c5a50..ad49b37a9 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3132,6 +3132,7 @@ void ApiWrap::sharedMediaDone( if (topicRootId && !topic) { return; } + const auto hasMessages = !parsed.messageIds.empty(); _session->storage().add(Storage::SharedMediaAddSlice( peer->id, topicRootId, @@ -3140,7 +3141,7 @@ void ApiWrap::sharedMediaDone( parsed.noSkipRange, parsed.fullCount )); - if (type == SharedMediaType::Pinned && !parsed.messageIds.empty()) { + if (type == SharedMediaType::Pinned && hasMessages) { peer->owner().history(peer)->setHasPinnedMessages(true); if (topic) { topic->setHasPinnedMessages(true); From 37f5160d1c635bd372667b89e4837351cc9728bb Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 4 Mar 2024 11:26:43 +0400 Subject: [PATCH 070/108] Fix bold formatting in the beginning of a quote. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 14794d222..7eaf7f8aa 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 14794d22210cb21b82db20aa55b1f3c8733b5fbd +Subproject commit 7eaf7f8aaa5c7aac9cbc6e5dc92dea7944003eba From 5e828603767ad83ff496e262d8ef55b4a20b77a8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 4 Mar 2024 17:00:25 +0400 Subject: [PATCH 071/108] Allow sending quick replies like bot commands. --- Telegram/SourceFiles/apiwrap.cpp | 12 +++++++++ Telegram/SourceFiles/apiwrap.h | 3 +++ .../chat_helpers/field_autocomplete.cpp | 25 ++++++++++++++++- .../chat_helpers/field_autocomplete.h | 1 + .../data/business/data_shortcut_messages.cpp | 10 +++++++ .../data/business/data_shortcut_messages.h | 2 ++ .../SourceFiles/history/history_widget.cpp | 27 ++++++++++++++++--- 7 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index ad49b37a9..d6d9679da 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -953,6 +953,7 @@ void ApiWrap::requestMoreDialogsIfNeeded() { } } requestContacts(); + _session->data().shortcutMessages().preloadShortcuts(); } void ApiWrap::updateDialogsOffset( @@ -3606,6 +3607,17 @@ void ApiWrap::cancelLocalItem(not_null<HistoryItem*> item) { } } +void ApiWrap::sendShortcutMessages( + not_null<PeerData*> peer, + BusinessShortcutId id) { + request(MTPmessages_SendQuickReplyMessages( + peer->input, + MTP_int(id) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).send(); +} + void ApiWrap::sendMessage(MessageToSend &&message) { const auto history = message.action.history; const auto peer = history->peer; diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 615960126..58165adec 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -337,6 +337,9 @@ public: void cancelLocalItem(not_null<HistoryItem*> item); + void sendShortcutMessages( + not_null<PeerData*> peer, + BusinessShortcutId id); void sendMessage(MessageToSend &&message); void sendBotStart( not_null<UserData*> bot, diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 382d850e3..1cf45d762 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/field_autocomplete.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_channel.h" @@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "core/application.h" #include "core/core_settings.h" +#include "lang/lang_keys.h" #include "lottie/lottie_single_player.h" #include "media/clip/media_clip_reader.h" #include "ui/widgets/popup_menu.h" @@ -636,6 +638,27 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } } } + const auto shortcuts = _user + ? _user->owner().shortcutMessages().shortcuts().list + : base::flat_map<BusinessShortcutId, Data::Shortcut>(); + if (!hasUsername && !shortcuts.empty()) { + const auto self = _user->session().user(); + for (const auto &[id, shortcut] : shortcuts) { + if (shortcut.count < 1) { + continue; + } else if (!listAllSuggestions) { + if (!shortcut.name.startsWith(_filter, Qt::CaseInsensitive)) { + continue; + } + } + brows.push_back(BotCommandRow{ + self, + shortcut.name, + tr::lng_forum_messages(tr::now, lt_count, shortcut.count), + self->activeUserpicView() + }); + } + } } rowsUpdated( std::move(mrows), @@ -1247,7 +1270,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex( command, insertUsername ? ('@' + PrimaryUsername(user)) : QString()); - _botCommandChosen.fire({ commandString, method }); + _botCommandChosen.fire({ user, commandString, method }); return true; } } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 2606dc1f2..5c9e291f3 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -96,6 +96,7 @@ public: ChooseMethod method = ChooseMethod::ByEnter; }; struct BotCommandChosen { + not_null<UserData*> user; QString command; ChooseMethod method = ChooseMethod::ByEnter; }; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index e7b4a65a7..df2f5f880 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -497,6 +497,16 @@ Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { return i->second; } +BusinessShortcutId ShortcutMessages::lookupShortcutId( + const QString &name) const { + for (const auto &[id, shortcut] : _shortcuts.list) { + if (!shortcut.name.compare(name, Qt::CaseInsensitive)) { + return id; + } + } + return {}; +} + void ShortcutMessages::editShortcut( BusinessShortcutId id, QString name, diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h index 57c1205f8..76a1df56d 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.h +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -78,6 +78,8 @@ public: [[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const; [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; + [[nodiscard]] BusinessShortcutId lookupShortcutId( + const QString &name) const; void editShortcut( BusinessShortcutId id, QString name, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index b658b8555..043fd7b8c 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -52,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "base/call_delayed.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_drafts.h" @@ -431,7 +432,20 @@ HistoryWidget::HistoryWidget( _fieldAutocomplete->botCommandChosen( ) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) { - insertHashtagOrBotCommand(data.command, data.method); + using Method = FieldAutocomplete::ChooseMethod; + const auto messages = &data.user->owner().shortcutMessages(); + const auto shortcutId = (_peer + && data.user->isSelf() + && data.method != Method::ByTab) + ? messages->lookupShortcutId(data.command.mid(1)) + : BusinessShortcutId(); + if (shortcutId) { + session().api().sendShortcutMessages(_peer, shortcutId); + session().api().finishForwarding(prepareSendAction({})); + setFieldText(_field->getTextWithTagsPart(_field->textCursor().position())); + } else { + insertHashtagOrBotCommand(data.command, data.method); + } }, lifetime()); _fieldAutocomplete->setModerateKeyActivateCallback([=](int key) { @@ -1410,9 +1424,14 @@ AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { } else if (result.query[0] == '@' && cRecentInlineBots().isEmpty()) { session().local().readRecentHashtagsAndBots(); - } else if (result.query[0] == '/' - && ((_peer->isUser() && !_peer->asUser()->isBot()) || _editMsgId)) { - return AutocompleteQuery(); + } else if (result.query[0] == '/') { + if (_editMsgId) { + return {}; + } else if (_peer->isUser() + && !_peer->asUser()->isBot() + && _peer->owner().shortcutMessages().shortcuts().list.empty()) { + return {}; + } } return result; } From ea36345eee546fc4bd6c9f6a22a690d7a34f2474 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 5 Mar 2024 14:05:48 +0400 Subject: [PATCH 072/108] Show location and working hours in profile. --- Telegram/Resources/langs/lang.strings | 16 +- .../data/business/data_business_common.cpp | 5 + .../data/business/data_business_common.h | 3 +- .../data/business/data_business_info.cpp | 21 + .../data/business/data_business_info.h | 3 + Telegram/SourceFiles/data/data_user.cpp | 1 + Telegram/SourceFiles/info/info.style | 18 + .../info/profile/info_profile_actions.cpp | 457 ++++++++++++++++++ .../business/settings_working_hours.cpp | 27 +- 9 files changed, 525 insertions(+), 26 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c78969caa..96055ee66 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1313,6 +1313,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_info_link_label" = "Link"; "lng_info_location_label" = "Location"; "lng_info_about_label" = "About"; +"lng_info_work_open" = "Open"; +"lng_info_work_closed" = "Closed"; +"lng_info_hours_label" = "Business hours"; +"lng_info_hours_closed" = "closed"; +"lng_info_hours_opens_in_minutes#one" = "opens in {count} minute"; +"lng_info_hours_opens_in_minutes#other" = "opens in {count} minutes"; +"lng_info_hours_opens_in_hours#one" = "opens in {count} hour"; +"lng_info_hours_opens_in_hours#other" = "opens in {count} hours"; +"lng_info_hours_opens_in_days#one" = "opens in {count} day"; +"lng_info_hours_opens_in_days#other" = "opens in {count} days"; +"lng_info_hours_open_full" = "open 24 hours"; +"lng_info_hours_next_day" = "{time} (next day)"; +"lng_info_hours_local_time" = "local time"; +"lng_info_hours_my_time" = "my time"; "lng_info_user_title" = "User Info"; "lng_info_bot_title" = "Bot Info"; "lng_info_group_title" = "Group Info"; @@ -2190,7 +2204,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_hours_sunday" = "Sunday"; "lng_hours_closed" = "Closed"; "lng_hours_open_full" = "Open 24 hours"; -"lng_hours_next_day" = "Next day, {time}"; +"lng_hours_next_day" = "{time} (Next day)"; "lng_hours_time_zone_title" = "Choose Time Zone"; "lng_hours_add_button" = "Add a Set of Hours"; "lng_hours_opening" = "Opening Time"; diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 34a46f3cc..14b1b0903 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -106,6 +106,11 @@ WorkingIntervals ExtractDayIntervals( return result; } +bool IsFullOpen(const WorkingIntervals &extractedDay) { + return extractedDay + && (extractedDay.list.front() == WorkingInterval{ 0, kDay }); +} + WorkingIntervals RemoveDayIntervals( const WorkingIntervals &intervals, int dayIndex) { diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index a47dce3a2..86422bc98 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -138,6 +138,7 @@ struct WorkingHours { [[nodiscard]] WorkingIntervals ExtractDayIntervals( const WorkingIntervals &intervals, int dayIndex); +[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay); [[nodiscard]] WorkingIntervals RemoveDayIntervals( const WorkingIntervals &intervals, int dayIndex); @@ -148,7 +149,7 @@ struct WorkingHours { struct BusinessLocation { QString address; - LocationPoint point; + std::optional<LocationPoint> point; explicit operator bool() const { return !address.isEmpty(); diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 43ef345e6..d874e52b9 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_business_info.h" #include "apiwrap.h" +#include "base/unixtime.h" #include "data/business/data_business_common.h" #include "data/data_session.h" #include "data/data_user.h" @@ -270,4 +271,24 @@ rpl::producer<Timezones> BusinessInfo::timezonesValue() const { return _timezones.value(); } +QString FindClosestTimezoneId(const std::vector<Timezone> &list) { + const auto local = QDateTime::currentDateTime(); + const auto utc = QDateTime(local.date(), local.time(), Qt::UTC); + const auto shift = base::unixtime::now() - (TimeId)::time(nullptr); + const auto delta = int(utc.toSecsSinceEpoch()) + - int(local.toSecsSinceEpoch()) + - shift; + const auto proj = [&](const Timezone &value) { + auto distance = value.utcOffset - delta; + while (distance > 12 * 3600) { + distance -= 24 * 3600; + } + while (distance < -12 * 3600) { + distance += 24 * 3600; + } + return std::abs(distance); + }; + return ranges::min_element(list, ranges::less(), proj)->id; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index 3b747eb0f..adea50a17 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -55,4 +55,7 @@ private: }; +[[nodiscard]] QString FindClosestTimezoneId( + const std::vector<Timezone> &list); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index daa9bb593..d670d9308 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -280,6 +280,7 @@ const Data::BusinessDetails &UserData::businessDetails() const { } void UserData::setBusinessDetails(Data::BusinessDetails details) { + details.hours = details.hours.normalized(); if ((!details && !_businessDetails) || (details && _businessDetails && details == *_businessDetails)) { return; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index d53d12f94..19162c400 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1008,3 +1008,21 @@ similarChannelsLockAbout: FlatLabel(defaultFlatLabel) { minWidth: 128px; } similarChannelsLockAboutPadding: margins(12px, 12px, 12px, 12px); + +infoHoursState: FlatLabel(infoLabeled) { + minWidth: 0px; +} +infoHoursValue: FlatLabel(infoHoursState) { + textFg: windowSubTextFg; + align: align(topright); +} +infoHoursDayLabel: infoHoursState; +infoHoursOuter: RoundButton(defaultActiveButton) { + textBg: transparent; + textBgOver: transparent; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +infoHoursOuterMargin: margins(8px, 4px, 8px, 4px); +infoHoursDaySkip: 6px; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index eba90de15..f0d690b51 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "base/options.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_folder.h" @@ -71,6 +75,8 @@ namespace Info { namespace Profile { namespace { +constexpr auto kDay = Data::WorkingInterval::kDay; + base::options::toggle ShowPeerIdBelowAbout({ .id = kOptionShowPeerIdBelowAbout, .name = "Show Peer IDs in Profile", @@ -159,6 +165,435 @@ base::options::toggle ShowPeerIdBelowAbout({ }); } +[[nodiscard]] bool AreNonTrivialHours(const Data::WorkingHours &hours) { + if (!hours) { + return false; + } + const auto &intervals = hours.intervals.list; + for (auto i = 0; i != 7; ++i) { + const auto day = Data::WorkingInterval{ i * kDay, (i + 1) * kDay }; + for (const auto &interval : intervals) { + const auto intersection = interval.intersected(day); + if (intersection && intersection != day) { + return true; + } + } + } + return false; +} + +[[nodiscard]] TimeId OpensIn( + const Data::WorkingIntervals &intervals, + TimeId now) { + using namespace Data; + + while (now < 0) { + now += WorkingInterval::kWeek; + } + while (now > WorkingInterval::kWeek) { + now -= WorkingInterval::kWeek; + } + auto closest = WorkingInterval::kWeek; + for (const auto &interval : intervals.list) { + if (interval.start <= now && interval.end > now) { + return TimeId(0); + } else if (interval.start > now && interval.start - now < closest) { + closest = interval.start - now; + } else if (interval.start < now) { + const auto next = interval.start + WorkingInterval::kWeek - now; + if (next < closest) { + closest = next; + } + } + } + return closest; +} + +[[nodiscard]] rpl::producer<QString> OpensInText( + rpl::producer<TimeId> in, + rpl::producer<QString> fallback) { + return rpl::combine( + std::move(in), + std::move(fallback) + ) | rpl::map([](TimeId in, QString fallback) { + return !in + ? std::move(fallback) + : (in >= 86400) + ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400) + : (in >= 3600) + ? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, in / 3600) + : tr::lng_info_hours_opens_in_minutes( + tr::now, + lt_count, + std::max(in / 60, 1)); + }); +} + +[[nodiscard]] QString FormatDayTime(TimeId time) { + const auto wrap = [](TimeId value) { + const auto hours = value / 3600; + const auto minutes = (value % 3600) / 60; + return QString::number(hours).rightJustified(2, u'0') + + ':' + + QString::number(minutes).rightJustified(2, u'0'); + }; + return (time > kDay) + ? tr::lng_info_hours_next_day(tr::now, lt_time, wrap(time - kDay)) + : wrap(time == kDay ? 0 : time); +} + +[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) { + auto result = QStringList(); + result.reserve(data.list.size()); + for (const auto &interval : data.list) { + const auto start = FormatDayTime(interval.start); + const auto end = FormatDayTime(interval.end); + result.push_back(start + u" - "_q + end); + } + return result.join('\n'); +} + +[[nodiscard]] QString FormatDayHours( + const Data::WorkingHours &hours, + const Data::WorkingIntervals &mine, + bool my, + int day) { + using namespace Data; + + const auto local = ExtractDayIntervals(hours.intervals, day); + if (IsFullOpen(local)) { + return tr::lng_info_hours_open_full(tr::now); + } + const auto use = my ? ExtractDayIntervals(mine, day) : local; + if (!use) { + return tr::lng_info_hours_closed(tr::now); + } + return JoinIntervals(use); +} + +[[nodiscard]] Data::WorkingIntervals ShiftedIntervals( + Data::WorkingIntervals intervals, + int delta) { + auto &list = intervals.list; + if (!delta || list.empty()) { + return { std::move(list) }; + } + for (auto &interval : list) { + interval.start += delta; + interval.end += delta; + } + while (list.front().start < 0) { + constexpr auto kWeek = Data::WorkingInterval::kWeek; + const auto first = list.front(); + if (first.end > 0) { + list.push_back({ first.start + kWeek, kWeek }); + list.front().start = 0; + } else { + list.push_back(first.shifted(kWeek)); + list.erase(list.begin()); + } + } + return intervals.normalized(); +} + +[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateWorkingHours( + not_null<QWidget*> parent, + not_null<UserData*> user) { + using namespace Data; + + auto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>( + parent, + object_ptr<Ui::RoundButton>( + parent, + rpl::single(QString()), + st::infoHoursOuter), + st::infoProfileLabeledPadding - st::infoHoursOuterMargin); + const auto button = result->entity(); + const auto inner = Ui::CreateChild<Ui::VerticalLayout>(button); + button->widthValue() | rpl::start_with_next([=](int width) { + const auto margin = st::infoHoursOuterMargin; + inner->resizeToWidth(width - margin.left() - margin.right()); + inner->move(margin.left(), margin.top()); + }, inner->lifetime()); + inner->heightValue() | rpl::start_with_next([=](int height) { + const auto margin = st::infoHoursOuterMargin; + height += margin.top() + margin.bottom(); + button->resize(button->width(), height); + }, inner->lifetime()); + + const auto info = &user->owner().businessInfo(); + + struct State { + rpl::variable<WorkingHours> hours; + rpl::variable<TimeId> time; + rpl::variable<int> day; + rpl::variable<int> timezoneDelta; + + rpl::variable<WorkingIntervals> mine; + rpl::variable<WorkingIntervals> mineByDays; + rpl::variable<TimeId> opensIn; + rpl::variable<bool> opened; + rpl::variable<bool> expanded; + rpl::variable<bool> nonTrivial; + rpl::variable<bool> myTimezone; + + rpl::event_stream<> recounts; + }; + const auto state = inner->lifetime().make_state<State>(); + + auto recounts = state->recounts.events_starting_with_copy(rpl::empty); + const auto recount = [=] { + state->recounts.fire({}); + }; + + state->hours = user->session().changes().peerFlagsValue( + user, + PeerUpdate::Flag::BusinessDetails + ) | rpl::map([=] { + return user->businessDetails().hours; + }); + state->nonTrivial = state->hours.value() | rpl::map(AreNonTrivialHours); + + const auto seconds = QTime::currentTime().msecsSinceStartOfDay() / 1000; + const auto inMinute = seconds % 60; + const auto firstTick = inMinute ? (61 - inMinute) : 1; + state->time = rpl::single(rpl::empty) | rpl::then( + base::timer_once(firstTick * crl::time(1000)) + ) | rpl::then( + base::timer_each(60 * crl::time(1000)) + ) | rpl::map([] { + const auto local = QDateTime::currentDateTime(); + const auto day = local.date().dayOfWeek() - 1; + const auto seconds = local.time().msecsSinceStartOfDay() / 1000; + return day * kDay + seconds; + }); + + state->day = state->time.value() | rpl::map([](TimeId time) { + return time / kDay; + }); + state->timezoneDelta = rpl::combine( + state->hours.value(), + info->timezonesValue() + ) | rpl::filter([]( + const WorkingHours &hours, + const Timezones &timezones) { + return ranges::contains( + timezones.list, + hours.timezoneId, + &Timezone::id); + }) | rpl::map([](WorkingHours &&hours, const Timezones &timezones) { + const auto &list = timezones.list; + const auto closest = FindClosestTimezoneId(list); + const auto i = ranges::find(list, closest, &Timezone::id); + const auto j = ranges::find(list, hours.timezoneId, &Timezone::id); + Assert(i != end(list)); + Assert(j != end(list)); + return i->utcOffset - j->utcOffset; + }); + + state->mine = rpl::combine( + state->hours.value(), + state->timezoneDelta.value() + ) | rpl::map([](WorkingHours &&hours, int delta) { + return ShiftedIntervals(hours.intervals, delta); + }); + + state->opensIn = rpl::combine( + state->mine.value(), + state->time.value() + ) | rpl::map([](const WorkingIntervals &mine, TimeId time) { + return OpensIn(mine, time); + }); + state->opened = state->opensIn.value() | rpl::map(rpl::mappers::_1 == 0); + + state->mineByDays = rpl::combine( + state->hours.value(), + state->timezoneDelta.value() + ) | rpl::map([](WorkingHours &&hours, int delta) { + auto full = std::array<bool, 7>(); + auto withoutFullDays = hours.intervals; + for (auto i = 0; i != 7; ++i) { + if (IsFullOpen(ExtractDayIntervals(hours.intervals, i))) { + full[i] = true; + withoutFullDays = ReplaceDayIntervals( + withoutFullDays, + i, + Data::WorkingIntervals()); + } + } + auto result = ShiftedIntervals(withoutFullDays, delta); + for (auto i = 0; i != 7; ++i) { + if (full[i]) { + result = ReplaceDayIntervals( + result, + i, + Data::WorkingIntervals{ { { 0, kDay } } }); + } + } + return result; + }); + + const auto dayHoursText = [=](int day) { + return rpl::combine( + state->hours.value(), + state->mineByDays.value(), + state->myTimezone.value() + ) | rpl::map([=]( + const WorkingHours &hours, + const WorkingIntervals &mine, + bool my) { + return FormatDayHours(hours, mine, my, day); + }); + }; + const auto dayHoursTextValue = [=](rpl::producer<int> day) { + return std::move(day) + | rpl::map(dayHoursText) + | rpl::flatten_latest(); + }; + + const auto openedWrap = inner->add(object_ptr<Ui::RpWidget>(inner)); + const auto opened = Ui::CreateChild<Ui::FlatLabel>( + openedWrap, + rpl::conditional( + state->opened.value(), + tr::lng_info_work_open(), + tr::lng_info_work_closed() + ) | rpl::after_next(recount), + st::infoHoursState); + opened->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto timing = Ui::CreateChild<Ui::FlatLabel>( + openedWrap, + OpensInText( + state->opensIn.value(), + dayHoursTextValue(state->day.value()) + ) | rpl::after_next(recount), + st::infoHoursValue); + timing->setAttribute(Qt::WA_TransparentForMouseEvents); + state->opened.value() | rpl::start_with_next([=](bool value) { + opened->setTextColorOverride(value + ? st::boxTextFgGood->c + : st::boxTextFgError->c); + }, opened->lifetime()); + + rpl::combine( + openedWrap->widthValue(), + opened->heightValue(), + timing->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + opened->moveToLeft(0, 0, width); + timing->moveToRight(0, 0, width); + + const auto margins = opened->getMargins(); + const auto added = margins.top() + margins.bottom(); + openedWrap->resize(width, std::max(h1, size.height()) - added); + }, openedWrap->lifetime()); + + const auto labelWrap = inner->add(object_ptr<Ui::RpWidget>(inner)); + const auto label = Ui::CreateChild<Ui::FlatLabel>( + labelWrap, + tr::lng_info_hours_label(), + st::infoLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto link = Ui::CreateChild<Ui::LinkButton>( + labelWrap, + QString()); + rpl::combine( + state->nonTrivial.value(), + state->hours.value(), + state->mine.value(), + state->myTimezone.value() + ) | rpl::map([=]( + bool complex, + const WorkingHours &hours, + const WorkingIntervals &mine, + bool my) { + return (!complex || hours.intervals == mine) + ? rpl::single(QString()) + : my + ? tr::lng_info_hours_my_time() + : tr::lng_info_hours_local_time(); + }) | rpl::flatten_latest( + ) | rpl::start_with_next([=](const QString &text) { + link->setText(text); + }, link->lifetime()); + link->setClickedCallback([=] { + state->myTimezone = !state->myTimezone.current(); + }); + + rpl::combine( + labelWrap->widthValue(), + label->heightValue(), + link->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + label->moveToLeft(0, 0, width); + link->moveToRight(0, 0, width); + + const auto margins = label->getMargins(); + const auto added = margins.top() + margins.bottom(); + labelWrap->resize(width, std::max(h1, size.height()) - added); + }, labelWrap->lifetime()); + + const auto other = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + other->toggleOn(state->expanded.value(), anim::type::normal); + other->finishAnimating(); + const auto days = other->entity(); + + for (auto i = 1; i != 7; ++i) { + const auto dayWrap = days->add( + object_ptr<Ui::RpWidget>(other), + QMargins(0, st::infoHoursDaySkip, 0, 0)); + auto label = state->day.value() | rpl::map([=](int day) { + switch ((day + i) % 7) { + case 0: return tr::lng_hours_monday(); + case 1: return tr::lng_hours_tuesday(); + case 2: return tr::lng_hours_wednesday(); + case 3: return tr::lng_hours_thursday(); + case 4: return tr::lng_hours_friday(); + case 5: return tr::lng_hours_saturday(); + case 6: return tr::lng_hours_sunday(); + } + Unexpected("Index in working hours."); + }) | rpl::flatten_latest(); + const auto dayLabel = Ui::CreateChild<Ui::FlatLabel>( + dayWrap, + std::move(label), + st::infoHoursDayLabel); + dayLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto dayHours = Ui::CreateChild<Ui::FlatLabel>( + dayWrap, + dayHoursTextValue(state->day.value() + | rpl::map((rpl::mappers::_1 + i) % 7)), + st::infoHoursValue); + dayHours->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + dayWrap->widthValue(), + dayLabel->heightValue(), + dayHours->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + dayLabel->moveToLeft(0, 0, width); + dayHours->moveToRight(0, 0, width); + + const auto margins = dayLabel->getMargins(); + const auto added = margins.top() + margins.bottom(); + dayWrap->resize(width, std::max(h1, size.height()) - added); + }, dayWrap->lifetime()); + } + + button->setClickedCallback([=] { + state->expanded = !state->expanded.current(); + }); + + result->toggleOn(state->hours.value( + ) | rpl::map([](const WorkingHours &data) { + return bool(data); + })); + + return result; +} + template <typename Text, typename ToggleOn, typename Callback> auto AddActionButton( not_null<Ui::VerticalLayout*> parent, @@ -563,6 +998,28 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { } return false; }); + } else { + tracker.track(result->add(CreateWorkingHours(result, user))); + + auto locationText = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::BusinessDetails + ) | rpl::map([=] { + const auto &details = user->businessDetails(); + if (!details.location) { + return TextWithEntities(); + } else if (!details.location.point) { + return TextWithEntities{ details.location.address }; + } + return Ui::Text::Link( + TextUtilities::SingleLine(details.location.address), + LocationClickHandler::Url(*details.location.point)); + }); + addInfoOneLine( + tr::lng_info_location_label(), + std::move(locationText), + QString() + ).text->setLinksTrusted(); } AddMainButton( diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index dd6c54b66..bae1b6b3d 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -70,27 +70,6 @@ private: return prefix + ' ' + data.name; } -[[nodiscard]] QString FindClosestTimezoneId( - const std::vector<Data::Timezone> &list) { - const auto local = QDateTime::currentDateTime(); - const auto utc = QDateTime(local.date(), local.time(), Qt::UTC); - const auto shift = base::unixtime::now() - (TimeId)::time(nullptr); - const auto delta = int(utc.toSecsSinceEpoch()) - - int(local.toSecsSinceEpoch()) - - shift; - const auto proj = [&](const Data::Timezone &value) { - auto distance = value.utcOffset - delta; - while (distance > 12 * 3600) { - distance -= 24 * 3600; - } - while (distance < -12 * 3600) { - distance += 24 * 3600; - } - return std::abs(distance); - }; - return ranges::min_element(list, ranges::less(), proj)->id; -} - [[nodiscard]] QString FormatDayTime( TimeId time, bool showEndAsNextDay = false) { @@ -372,7 +351,7 @@ void ChooseTimezoneBox( }); if (!ranges::contains(list, id, &Data::Timezone::id)) { - id = FindClosestTimezoneId(list); + id = Data::FindClosestTimezoneId(list); } const auto i = ranges::find(list, id, &Data::Timezone::id); const auto value = int(i - begin(list)); @@ -472,7 +451,7 @@ void AddWeekButton( } if (!intervals) { return tr::lng_hours_closed(); - } else if (intervals.list.front() == WorkingInterval{ 0, kDay }) { + } else if (IsFullOpen(intervals)) { return tr::lng_hours_open_full(); } return rpl::single(JoinIntervals(intervals)); @@ -613,7 +592,7 @@ void WorkingHours::setupContent( const auto now = _hours.current().timezoneId; if (!ranges::contains(value.list, now, &Data::Timezone::id)) { auto copy = _hours.current(); - copy.timezoneId = FindClosestTimezoneId(value.list); + copy.timezoneId = Data::FindClosestTimezoneId(value.list); _hours = std::move(copy); } }, inner->lifetime()); From e3f6c189a72f61723d1cc9618597f670a5837391 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 5 Mar 2024 20:52:14 +0400 Subject: [PATCH 073/108] Implement preview and save of chatbots. --- Telegram/Resources/langs/lang.strings | 5 +- .../data/business/data_business_chatbots.cpp | 67 +++- .../data/business/data_business_chatbots.h | 16 +- .../data/business/data_business_common.cpp | 124 ++++++++ .../data/business/data_business_common.h | 20 ++ .../data/business/data_business_info.cpp | 51 --- Telegram/SourceFiles/data/data_user.cpp | 96 ------ .../info/profile/info_profile_actions.cpp | 8 +- .../business/settings_away_message.cpp | 4 +- .../settings/business/settings_chatbots.cpp | 297 +++++++++++++++++- .../settings/business/settings_greeting.cpp | 4 +- Telegram/SourceFiles/settings/settings.style | 8 +- .../settings/settings_business.cpp | 12 +- 13 files changed, 536 insertions(+), 176 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 96055ee66..e8366f8f7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2220,7 +2220,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_replies_add_placeholder" = "Shortcut"; "lng_replies_add_exists" = "This shortcut already exists."; "lng_replies_empty_title" = "New Quick Reply"; -"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing / or using Attachment menu."; +"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing /."; "lng_replies_remove_title" = "Remove Shortcut"; "lng_replies_remove_text" = "You didn't create a quick reply message. Do you want to remove the shortcut?"; "lng_replies_edit_title" = "Edit Shortcut"; @@ -2241,6 +2241,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_greeting_empty_about" = "Create greetings that will be automatically sent to new customers."; "lng_greeting_message_placeholder" = "Add a Greeting"; "lng_greeting_limit_reached" = "You have too many quick replies. Remove one to add a greeting message."; +"lng_greeting_recipients_empty" = "Please choose at least one recipient."; "lng_away_title" = "Away Message"; "lng_away_about" = "Automatically reply with a message when you are away."; @@ -2282,7 +2283,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_reply" = "Reply to Messages"; "lng_chatbots_reply_about" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; "lng_chatbots_remove" = "Remove Bot"; -"lng_chatbots_not_found" = "Chatbot not found"; +"lng_chatbots_not_found" = "Chatbot not found."; "lng_chatbots_add" = "Add"; "lng_boost_channel_button" = "Boost Channel"; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index 89215a2e0..8862c4e84 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -7,14 +7,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/business/data_business_chatbots.h" +#include "apiwrap.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + namespace Data { -Chatbots::Chatbots(not_null<Session*> session) -: _session(session) { +Chatbots::Chatbots(not_null<Session*> owner) +: _owner(owner) { } Chatbots::~Chatbots() = default; +void Chatbots::preload() { + if (_loaded || _requestId) { + return; + } + _requestId = _owner->session().api().request( + MTPaccount_GetConnectedBots() + ).done([=](const MTPaccount_ConnectedBots &result) { + _requestId = 0; + _loaded = true; + + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + const auto &list = data.vconnected_bots().v; + if (!list.isEmpty()) { + const auto &bot = list.front().data(); + const auto botId = bot.vbot_id().v; + _settings = ChatbotsSettings{ + .bot = _owner->session().data().user(botId), + .recipients = FromMTP(_owner, bot.vrecipients()), + .repliesAllowed = bot.is_can_reply(), + }; + } + }).send(); +} + const ChatbotsSettings &Chatbots::current() const { return _settings.current(); } @@ -27,7 +59,36 @@ rpl::producer<ChatbotsSettings> Chatbots::value() const { return _settings.value(); } -void Chatbots::save(ChatbotsSettings settings, Fn<void(QString)> fail) { +void Chatbots::save( + ChatbotsSettings settings, + Fn<void()> done, + Fn<void(QString)> fail) { + const auto was = _settings.current(); + if (was == settings) { + return; + } else if (was.bot || settings.bot) { + using Flag = MTPaccount_UpdateConnectedBot::Flag; + const auto api = &_owner->session().api(); + api->request(MTPaccount_UpdateConnectedBot( + MTP_flags(!settings.bot + ? Flag::f_deleted + : settings.repliesAllowed + ? Flag::f_can_reply + : Flag()), + (settings.bot ? settings.bot : was.bot)->inputUser, + ToMTP(settings.recipients) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + if (done) { + done(); + } + }).fail([=](const MTP::Error &error) { + _settings = was; + if (fail) { + fail(error.type()); + } + }).send(); + } _settings = settings; } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index da088c394..ca21baef6 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -19,23 +19,33 @@ struct ChatbotsSettings { UserData *bot = nullptr; BusinessRecipients recipients; bool repliesAllowed = false; + + friend inline bool operator==( + const ChatbotsSettings &, + const ChatbotsSettings &) = default; }; class Chatbots final { public: - explicit Chatbots(not_null<Session*> session); + explicit Chatbots(not_null<Session*> owner); ~Chatbots(); + void preload(); [[nodiscard]] const ChatbotsSettings ¤t() const; [[nodiscard]] rpl::producer<ChatbotsSettings> changes() const; [[nodiscard]] rpl::producer<ChatbotsSettings> value() const; - void save(ChatbotsSettings settings, Fn<void(QString)> fail); + void save( + ChatbotsSettings settings, + Fn<void()> done, + Fn<void(QString)> fail); private: - const not_null<Session*> _session; + const not_null<Session*> _owner; rpl::variable<ChatbotsSettings> _settings; + mtpRequestId _requestId = 0; + bool _loaded = false; }; diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 14b1b0903..7da2970db 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/business/data_business_common.h" +#include "data/data_session.h" +#include "data/data_user.h" + namespace Data { namespace { @@ -50,6 +53,127 @@ constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; } // namespace +MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data) { + using Flag = MTPDinputBusinessRecipients::Flag; + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + const auto flags = Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) + | (chats.list.empty() ? Flag() : Flag::f_users) + | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); + const auto &users = data.allButExcluded + ? data.excluded + : data.included; + return MTP_inputBusinessRecipients( + MTP_flags(flags), + MTP_vector_from_range(users.list + | ranges::views::transform(&UserData::inputUser))); +} + +BusinessRecipients FromMTP( + not_null<Session*> owner, + const MTPBusinessRecipients &recipients) { + using Type = BusinessChatType; + + const auto &data = recipients.data(); + auto result = BusinessRecipients{ + .allButExcluded = data.is_exclude_selected(), + }; + auto &chats = result.allButExcluded + ? result.excluded + : result.included; + chats.types = Type() + | (data.is_new_chats() ? Type::NewChats : Type()) + | (data.is_existing_chats() ? Type::ExistingChats : Type()) + | (data.is_contacts() ? Type::Contacts : Type()) + | (data.is_non_contacts() ? Type::NonContacts : Type()); + if (const auto users = data.vusers()) { + for (const auto &userId : users->v) { + chats.list.push_back(owner->user(UserId(userId.v))); + } + } + return result; +} + +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional<MTPBusinessWorkHours> &hours, + const tl::conditional<MTPBusinessLocation> &location) { + auto result = BusinessDetails(); + if (hours) { + const auto &data = hours->data(); + result.hours.timezoneId = qs(data.vtimezone_id()); + result.hours.intervals.list = ranges::views::all( + data.vweekly_open().v + ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { + const auto &data = open.data(); + return WorkingInterval{ + data.vstart_minute().v * 60, + data.vend_minute().v * 60, + }; + }) | ranges::to_vector; + } + if (location) { + const auto &data = location->data(); + result.location.address = qs(data.vaddress()); + if (const auto point = data.vgeo_point()) { + point->match([&](const MTPDgeoPoint &data) { + result.location.point = LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } + } + return result; +} + +[[nodiscard]] AwaySettings FromMTP( + not_null<Session*> owner, + const tl::conditional<MTPBusinessAwayMessage> &message) { + if (!message) { + return AwaySettings(); + } + const auto &data = message->data(); + auto result = AwaySettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .shortcutId = data.vshortcut_id().v, + .offlineOnly = data.is_offline_only(), + }; + data.vschedule().match([&]( + const MTPDbusinessAwayMessageScheduleAlways &) { + result.schedule.type = AwayScheduleType::Always; + }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { + result.schedule.type = AwayScheduleType::OutsideWorkingHours; + }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { + result.schedule.type = AwayScheduleType::Custom; + result.schedule.customInterval = WorkingInterval{ + data.vstart_date().v, + data.vend_date().v, + }; + }); + return result; +} + +[[nodiscard]] GreetingSettings FromMTP( + not_null<Session*> owner, + const tl::conditional<MTPBusinessGreetingMessage> &message) { + if (!message) { + return GreetingSettings(); + } + const auto &data = message->data(); + return GreetingSettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .noActivityDays = data.vno_activity_days().v, + .shortcutId = data.vshortcut_id().v, + }; +} + WorkingIntervals WorkingIntervals::normalized() const { return SortAndMerge(MoveTailToFront(SortAndMerge(*this))); } diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 86422bc98..dd66bb770 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -14,6 +14,8 @@ class UserData; namespace Data { +class Session; + enum class BusinessChatType { NewChats = (1 << 0), ExistingChats = (1 << 1), @@ -43,6 +45,12 @@ struct BusinessRecipients { const BusinessRecipients &b) = default; }; +[[nodiscard]] MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data); +[[nodiscard]] BusinessRecipients FromMTP( + not_null<Session*> owner, + const MTPBusinessRecipients &recipients); + struct Timezone { QString id; QString name; @@ -173,6 +181,10 @@ struct BusinessDetails { const BusinessDetails &b) = default; }; +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional<MTPBusinessWorkHours> &hours, + const tl::conditional<MTPBusinessLocation> &location); + enum class AwayScheduleType : uchar { Never = 0, Always = 1, @@ -204,6 +216,10 @@ struct AwaySettings { const AwaySettings &b) = default; }; +[[nodiscard]] AwaySettings FromMTP( + not_null<Session*> owner, + const tl::conditional<MTPBusinessAwayMessage> &message); + struct GreetingSettings { BusinessRecipients recipients; int noActivityDays = 0; @@ -218,4 +234,8 @@ struct GreetingSettings { const GreetingSettings &b) = default; }; +[[nodiscard]] GreetingSettings FromMTP( + not_null<Session*> owner, + const tl::conditional<MTPBusinessGreetingMessage> &message); + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index d874e52b9..b6cf5ac36 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -30,57 +30,6 @@ namespace { MTP_vector_from_range(list | ranges::views::transform(proj))); } -[[nodiscard]] MTPInputBusinessRecipients ToMTP( - const BusinessRecipients &data) { - //MTP_flags(RecipientsFlags(data.recipients, Flag())), - // MTP_vector_from_range( - // (data.recipients.allButExcluded - // ? data.recipients.excluded - // : data.recipients.included).list - // | ranges::views::transform(&UserData::inputUser)), - - using Flag = MTPDinputBusinessRecipients::Flag; - using Type = BusinessChatType; - const auto &chats = data.allButExcluded - ? data.excluded - : data.included; - const auto flags = Flag() - | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) - | ((chats.types & Type::ExistingChats) - ? Flag::f_existing_chats - : Flag()) - | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) - | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) - | (chats.list.empty() ? Flag() : Flag::f_users) - | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); - const auto &users = data.allButExcluded - ? data.excluded - : data.included; - return MTP_inputBusinessRecipients( - MTP_flags(flags), - MTP_vector_from_range(users.list - | ranges::views::transform(&UserData::inputUser))); -} - -template <typename Flag> -[[nodiscard]] auto RecipientsFlags( - const BusinessRecipients &data, - Flag) { - using Type = BusinessChatType; - const auto &chats = data.allButExcluded - ? data.excluded - : data.included; - return Flag() - | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) - | ((chats.types & Type::ExistingChats) - ? Flag::f_existing_chats - : Flag()) - | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) - | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) - | (chats.list.empty() ? Flag() : Flag::f_users) - | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); -} - [[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP( const AwaySchedule &data) { Expects(data.type != AwayScheduleType::Never); diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index d670d9308..a5c2a705d 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -32,102 +32,6 @@ constexpr auto kSetOnlineAfterActivity = TimeId(30); using UpdateFlag = Data::PeerUpdate::Flag; -[[nodiscard]] Data::BusinessDetails FromMTP( - const tl::conditional<MTPBusinessWorkHours> &hours, - const tl::conditional<MTPBusinessLocation> &location) { - auto result = Data::BusinessDetails(); - if (hours) { - const auto &data = hours->data(); - result.hours.timezoneId = qs(data.vtimezone_id()); - result.hours.intervals.list = ranges::views::all( - data.vweekly_open().v - ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { - const auto &data = open.data(); - return Data::WorkingInterval{ - data.vstart_minute().v * 60, - data.vend_minute().v * 60, - }; - }) | ranges::to_vector; - } - if (location) { - const auto &data = location->data(); - result.location.address = qs(data.vaddress()); - if (const auto point = data.vgeo_point()) { - point->match([&](const MTPDgeoPoint &data) { - result.location.point = Data::LocationPoint(data); - }, [&](const MTPDgeoPointEmpty &) { - }); - } - } - return result; -} - -Data::BusinessRecipients FromMTP( - not_null<Data::Session*> owner, - const MTPBusinessRecipients &recipients) { - using Type = Data::BusinessChatType; - - const auto &data = recipients.data(); - auto result = Data::BusinessRecipients{ - .allButExcluded = data.is_exclude_selected(), - }; - auto &chats = result.allButExcluded - ? result.excluded - : result.included; - chats.types = Type() - | (data.is_new_chats() ? Type::NewChats : Type()) - | (data.is_existing_chats() ? Type::ExistingChats : Type()) - | (data.is_contacts() ? Type::Contacts : Type()) - | (data.is_non_contacts() ? Type::NonContacts : Type()); - if (const auto users = data.vusers()) { - for (const auto &userId : users->v) { - chats.list.push_back(owner->user(UserId(userId.v))); - } - } - return result; -} - -[[nodiscard]] Data::AwaySettings FromMTP( - not_null<Data::Session*> owner, - const tl::conditional<MTPBusinessAwayMessage> &message) { - if (!message) { - return Data::AwaySettings(); - } - const auto &data = message->data(); - auto result = Data::AwaySettings{ - .recipients = FromMTP(owner, data.vrecipients()), - .shortcutId = data.vshortcut_id().v, - .offlineOnly = data.is_offline_only(), - }; - data.vschedule().match([&]( - const MTPDbusinessAwayMessageScheduleAlways &) { - result.schedule.type = Data::AwayScheduleType::Always; - }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { - result.schedule.type = Data::AwayScheduleType::OutsideWorkingHours; - }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { - result.schedule.type = Data::AwayScheduleType::Custom; - result.schedule.customInterval = Data::WorkingInterval{ - data.vstart_date().v, - data.vend_date().v, - }; - }); - return result; -} - -[[nodiscard]] Data::GreetingSettings FromMTP( - not_null<Data::Session*> owner, - const tl::conditional<MTPBusinessGreetingMessage> &message) { - if (!message) { - return Data::GreetingSettings(); - } - const auto &data = message->data(); - return Data::GreetingSettings{ - .recipients = FromMTP(owner, data.vrecipients()), - .noActivityDays = data.vno_activity_days().v, - .shortcutId = data.vshortcut_id().v, - }; -} - } // namespace BotInfo::BotInfo() = default; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index f0d690b51..ddcb93e45 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -211,12 +211,14 @@ base::options::toggle ShowPeerIdBelowAbout({ [[nodiscard]] rpl::producer<QString> OpensInText( rpl::producer<TimeId> in, + rpl::producer<bool> hoursExpanded, rpl::producer<QString> fallback) { return rpl::combine( std::move(in), + std::move(hoursExpanded), std::move(fallback) - ) | rpl::map([](TimeId in, QString fallback) { - return !in + ) | rpl::map([](TimeId in, bool hoursExpanded, QString fallback) { + return (!in || hoursExpanded) ? std::move(fallback) : (in >= 86400) ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400) @@ -465,6 +467,7 @@ base::options::toggle ShowPeerIdBelowAbout({ openedWrap, OpensInText( state->opensIn.value(), + state->expanded.value(), dayHoursTextValue(state->day.value()) ) | rpl::after_next(recount), st::infoHoursValue); @@ -518,6 +521,7 @@ base::options::toggle ShowPeerIdBelowAbout({ }, link->lifetime()); link->setClickedCallback([=] { state->myTimezone = !state->myTimezone.current(); + state->expanded = true; }); rpl::combine( diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 04dfe993a..a31e2958f 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -346,9 +346,7 @@ void AwayMessage::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } else if (error != u"SHORTCUT_INVALID"_q) { show->showToast(error); } diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d3f112483..d9b17f260 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" +#include "apiwrap.h" +#include "boxes/peers/prepare_short_info_box.h" +#include "boxes/peer_list_box.h" #include "core/application.h" #include "data/business/data_business_chatbots.h" #include "data/data_session.h" @@ -14,19 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { namespace { +constexpr auto kDebounceTimeout = crl::time(400); + enum class LookupState { Empty, Loading, @@ -64,22 +72,298 @@ private: }; +class PreviewController final : public PeerListController { +public: + PreviewController(not_null<PeerData*> peer, Fn<void()> resetBot); + + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null<PeerListRow*> row) override; + void rowRightActionClicked(not_null<PeerListRow*> row) override; + Main::Session &session() const override; + +private: + const not_null<PeerData*> _peer; + const Fn<void()> _resetBot; + rpl::lifetime _lifetime; + +}; + +class PreviewRow final : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + void rightActionAddRipple( + QPoint point, + Fn<void()> updateCallback) override; + void rightActionStopLastRipple() override; + +private: + std::unique_ptr<Ui::RippleAnimation> _actionRipple; + +}; + +QSize PreviewRow::rightActionSize() const { + return QSize( + st::settingsChatbotsDeleteIcon.width(), + st::settingsChatbotsDeleteIcon.height()) * 2; +} + +QMargins PreviewRow::rightActionMargins() const { + const auto itemHeight = st::peerListSingleRow.item.height; + const auto skip = (itemHeight - rightActionSize().height()) / 2; + return QMargins(0, skip, skip, 0); +} + +void PreviewRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionRipple) { + _actionRipple->paint( + p, + x, + y, + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + const auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize()); + (actionSelected + ? st::settingsChatbotsDeleteIconOver + : st::settingsChatbotsDeleteIcon).paintInCenter(p, rect); +} + +void PreviewRow::rightActionAddRipple( + QPoint point, + Fn<void()> updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize()); + _actionRipple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point); +} + +void PreviewRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +PreviewController::PreviewController( + not_null<PeerData*> peer, + Fn<void()> resetBot) +: _peer(peer) +, _resetBot(std::move(resetBot)) { +} + +void PreviewController::prepare() { + delegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer)); + delegate()->peerListRefreshRows(); +} + +void PreviewController::loadMoreRows() { +} + +void PreviewController::rowClicked(not_null<PeerListRow*> row) { +} + +void PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) { + _resetBot(); +} + +Main::Session &PreviewController::session() const { + return _peer->session(); +} + [[nodiscard]] rpl::producer<QString> DebouncedValue( not_null<Ui::InputField*> field) { - return rpl::single(field->getLastText()); + return [=](auto consumer) { + + auto result = rpl::lifetime(); + struct State { + base::Timer timer; + QString lastText; + }; + const auto state = result.make_state<State>(); + const auto push = [=] { + state->timer.cancel(); + consumer.put_next_copy(state->lastText); + }; + state->timer.setCallback(push); + state->lastText = field->getLastText(); + consumer.put_next_copy(field->getLastText()); + field->changes() | rpl::start_with_next([=] { + const auto &text = field->getLastText(); + const auto was = std::exchange(state->lastText, text); + if (std::abs(int(text.size()) - int(was.size())) == 1) { + state->timer.callOnce(kDebounceTimeout); + } else { + push(); + } + }, result); + return result; + }; +} + +[[nodiscard]] QString ExtractUsername(QString text) { + text = text.trimmed(); + static const auto expression = QRegularExpression( + "^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)"); + const auto match = expression.match(text); + return match.hasMatch() ? match.captured(3) : text; } [[nodiscard]] rpl::producer<BotState> LookupBot( not_null<Main::Session*> session, rpl::producer<QString> usernameChanges) { - return rpl::never<BotState>(); + using Cache = base::flat_map<QString, UserData*>; + const auto cache = std::make_shared<Cache>(); + return std::move( + usernameChanges + ) | rpl::map([=](const QString &username) -> rpl::producer<BotState> { + const auto extracted = ExtractUsername(username); + const auto owner = &session->data(); + static const auto expression = QRegularExpression( + "^[a-zA-Z0-9_\\.]+$"); + if (!expression.match(extracted).hasMatch()) { + return rpl::single(BotState()); + } else if (const auto peer = owner->peerByUsername(extracted)) { + if (const auto user = peer->asUser(); user && user->isBot()) { + return rpl::single(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + } + return rpl::single(BotState{ + .state = LookupState::Ready, + }); + } else if (const auto i = cache->find(extracted); i != end(*cache)) { + return rpl::single(BotState{ + .bot = i->second, + .state = LookupState::Ready, + }); + } + + return [=](auto consumer) { + auto result = rpl::lifetime(); + + const auto requestId = result.make_state<mtpRequestId>(); + *requestId = session->api().request(MTPcontacts_ResolveUsername( + MTP_string(extracted) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + const auto &data = result.data(); + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + const auto peerId = peerFromMTP(data.vpeer()); + const auto peer = session->data().peer(peerId); + if (const auto user = peer->asUser()) { + if (user->isBot()) { + cache->emplace(extracted, user); + consumer.put_next(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + return; + } + } + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).fail([=] { + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).send(); + + result.add([=] { + session->api().request(*requestId).cancel(); + }); + return result; + }; + }) | rpl::flatten_latest(); } [[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview( - not_null<QWidget*> parent, + not_null<Ui::RpWidget*> parent, rpl::producer<BotState> state, Fn<void()> resetBot) { - return object_ptr<Ui::RpWidget>(parent.get()); + auto result = object_ptr<Ui::SlideWrap<>>( + parent.get(), + object_ptr<Ui::RpWidget>(parent.get())); + const auto raw = result.data(); + const auto inner = raw->entity(); + raw->hide(anim::type::instant); + + const auto child = inner->lifetime().make_state<Ui::RpWidget*>(nullptr); + std::move(state) | rpl::filter([=](BotState state) { + return state.state != LookupState::Loading; + }) | rpl::start_with_next([=](BotState state) { + raw->toggle(state.state == LookupState::Ready, anim::type::normal); + if (state.bot) { + const auto delegate = parent->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = parent->lifetime().make_state< + PreviewController + >(state.bot, resetBot); + controller->setStyleOverrides(&st::peerListSingleRow); + const auto content = Ui::CreateChild<PeerListContent>( + inner, + controller); + delegate->setContent(content); + controller->setDelegate(delegate); + delete base::take(*child); + *child = content; + } else if (state.state == LookupState::Ready) { + const auto content = Ui::CreateChild<Ui::RpWidget>(inner); + const auto label = Ui::CreateChild<Ui::FlatLabel>( + content, + tr::lng_chatbots_not_found(), + st::settingsChatbotsNotFound); + content->resize( + inner->width(), + st::peerListSingleRow.item.height); + rpl::combine( + content->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](QSize size, QSize inner) { + label->move( + (size.width() - inner.width()) / 2, + (size.height() - inner.height()) / 2); + }, label->lifetime()); + delete base::take(*child); + *child = content; + } else { + return; + } + (*child)->show(); + + inner->widthValue() | rpl::start_with_next([=](int width) { + (*child)->resizeToWidth(width); + }, (*child)->lifetime()); + + (*child)->heightValue() | rpl::start_with_next([=](int height) { + inner->resize(inner->width(), height + st::contactSkip); + }, inner->lifetime()); + }, inner->lifetime()); + + raw->finishAnimating(); + return result; } Chatbots::Chatbots( @@ -193,15 +477,14 @@ void Chatbots::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } }; controller()->session().data().chatbots().save({ .bot = _botValue.current().bot, .recipients = _recipients.current(), .repliesAllowed = _repliesAllowed.current(), + }, [=] { }, [=](QString error) { show->showToast(error); }); } diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 3c73f391f..932c1d980 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -263,9 +263,7 @@ void Greeting::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } else if (error != u"SHORTCUT_INVALID"_q) { show->showToast(error); } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index fcdd0a7cd..00f1121d1 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -631,4 +631,10 @@ settingsAddReplyField: InputField(defaultInputField) { placeholderScale: 0.; heightMin: 36px; -} \ No newline at end of file +} +settingsChatbotsNotFound: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + align: align(top); +} +settingsChatbotsDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; +settingsChatbotsDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }}; diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index c0848e341..936f0c49f 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -9,10 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "core/click_handler_types.h" +#include "data/business/data_business_info.h" +#include "data/business/data_business_chatbots.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_peer_values.h" // AmPremiumValue. #include "data/data_session.h" -#include "data/business/data_business_info.h" -#include "data/business/data_shortcut_messages.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -224,9 +225,9 @@ void AddBusinessSummary( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = account.appConfig().get<Order>( + const auto mtpOrder = FallbackOrder(); AssertIsDebug();/* account.appConfig().get<Order>( "business_promo_order", - FallbackOrder()); + FallbackOrder());*/ const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); @@ -354,7 +355,8 @@ void Business::setStepDataReference(std::any &data) { void Business::setupContent() { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - _controller->session().data().businessInfo().preloadTimezones(); + _controller->session().data().chatbots().preload(); + _controller->session().data().businessInfo().preload(); _controller->session().data().shortcutMessages().preloadShortcuts(); Ui::AddSkip(content, st::settingsFromFileTop); From 00dcf11691da3dccbeaf2033f869a68d173ddba6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:02:32 +0400 Subject: [PATCH 074/108] Improve recipients selection in business features. --- Telegram/Resources/langs/lang.strings | 1 + .../SourceFiles/core/local_url_handlers.cpp | 15 ------ .../data/business/data_business_common.h | 6 ++- .../data/business/data_business_info.cpp | 52 +++++++++---------- .../SourceFiles/info/info_content_widget.h | 6 +++ .../SourceFiles/info/info_wrap_widget.cpp | 14 +++-- .../info/settings/info_settings_widget.cpp | 8 +++ .../info/settings/info_settings_widget.h | 2 + .../business/settings_away_message.cpp | 5 ++ .../settings/business/settings_chatbots.cpp | 9 +++- .../settings/business/settings_greeting.cpp | 5 ++ .../business/settings_recipients_helper.cpp | 24 ++++++++- .../business/settings_working_hours.cpp | 5 ++ .../SourceFiles/settings/settings_common.h | 6 +++ 14 files changed, 109 insertions(+), 49 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e8366f8f7..f13454d7d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2285,6 +2285,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_remove" = "Remove Bot"; "lng_chatbots_not_found" = "Chatbot not found."; "lng_chatbots_add" = "Add"; +"lng_chatbots_info_url" = "https://telegram.org/privacy"; "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 92ed6c9e8..d94187094 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -669,17 +669,6 @@ bool ShowSearchTagsPromo( return true; } -bool ShowAboutBusinessChatbots( - Window::SessionController *controller, - const Match &match, - const QVariant &context) { - if (!controller) { - return false; - } - controller->showToast(u"Cool feature, yeah.."_q); AssertIsDebug(); - return true; -} - void ExportTestChatTheme( not_null<Window::SessionController*> controller, not_null<const Data::CloudTheme*> theme) { @@ -1048,10 +1037,6 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() { u"about_tags"_q, ShowSearchTagsPromo }, - { - u"about_business_chatbots"_q, - ShowAboutBusinessChatbots - }, }; return Result; } diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index dd66bb770..151d534e1 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -30,6 +30,10 @@ struct BusinessChats { BusinessChatTypes types; std::vector<not_null<UserData*>> list; + [[nodiscard]] bool empty() const { + return !types && list.empty(); + } + friend inline bool operator==( const BusinessChats &a, const BusinessChats &b) = default; @@ -193,7 +197,7 @@ enum class AwayScheduleType : uchar { }; struct AwaySchedule { - AwayScheduleType type = AwayScheduleType::Always; + AwayScheduleType type = AwayScheduleType::Never; WorkingInterval customInterval; friend inline bool operator==( diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index b6cf5ac36..8a65201fe 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -109,20 +109,20 @@ void BusinessInfo::saveAwaySettings( const auto &was = _awaySettings; if (was == data) { return; + } else if (!data || data.shortcutId) { + using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; + const auto session = &_owner->session(); + session->api().request(MTPaccount_UpdateBusinessAwayMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessAwayMessage() + )).fail([=](const MTP::Error &error) { + _awaySettings = was; + _awaySettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); } - using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; - const auto session = &_owner->session(); - session->api().request(MTPaccount_UpdateBusinessAwayMessage( - MTP_flags(data ? Flag::f_message : Flag()), - data ? ToMTP(data) : MTPInputBusinessAwayMessage() - )).fail([=](const MTP::Error &error) { - _awaySettings = was; - _awaySettingsChanged.fire({}); - if (fail) { - fail(error.type()); - } - }).send(); - _awaySettings = std::move(data); _awaySettingsChanged.fire({}); } @@ -153,20 +153,20 @@ void BusinessInfo::saveGreetingSettings( const auto &was = _greetingSettings; if (was == data) { return; + } else if (!data || data.shortcutId) { + using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag; + _owner->session().api().request( + MTPaccount_UpdateBusinessGreetingMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessGreetingMessage()) + ).fail([=](const MTP::Error &error) { + _greetingSettings = was; + _greetingSettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); } - using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag; - _owner->session().api().request( - MTPaccount_UpdateBusinessGreetingMessage( - MTP_flags(data ? Flag::f_message : Flag()), - data ? ToMTP(data) : MTPInputBusinessGreetingMessage()) - ).fail([=](const MTP::Error &error) { - _greetingSettings = was; - _greetingSettingsChanged.fire({}); - if (fail) { - fail(error.type()); - } - }).send(); - _greetingSettings = std::move(data); _greetingSettingsChanged.fire({}); } diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 29d947c66..7f7cd0f2f 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -101,6 +101,12 @@ public: } virtual void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction); + [[nodiscard]] virtual bool closeByOutsideClick() const { + return true; + } + virtual void checkBeforeClose(Fn<void()> close) { + close(); + } [[nodiscard]] virtual rpl::producer<QString> title() = 0; [[nodiscard]] virtual rpl::producer<QString> subtitle() { return nullptr; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index a2c6a781c..50eb99333 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -410,8 +410,10 @@ void WrapWidget::setupTopBarMenuToggle() { } void WrapWidget::checkBeforeClose(Fn<void()> close) { - _controller->parentController()->hideLayer(); - close(); + _content->checkBeforeClose(crl::guard(this, [=] { + _controller->parentController()->hideLayer(); + close(); + })); } void WrapWidget::addTopBarMenuButton() { @@ -438,7 +440,7 @@ void WrapWidget::addTopBarMenuButton() { } bool WrapWidget::closeByOutsideClick() const { - return true; + return _content->closeByOutsideClick(); } void WrapWidget::addProfileCallsButton() { @@ -872,8 +874,12 @@ void WrapWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) { if (hasStackHistory() || wrap() != Wrap::Layer) { checkBeforeClose([=] { _controller->showBackFromStack(); }); - return; + } else { + checkBeforeClose([=] { + _controller->parentController()->hideSpecialLayer(); + }); } + return; } SectionWidget::keyPressEvent(e); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 5bc315aa1..bbc0ca717 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -232,6 +232,14 @@ rpl::producer<bool> Widget::desiredShadowVisibility() const { : rpl::single(true); } +bool Widget::closeByOutsideClick() const { + return _inner->closeByOutsideClick();; +} + +void Widget::checkBeforeClose(Fn<void()> close) { + _inner->checkBeforeClose(std::move(close)); +} + rpl::producer<QString> Widget::title() { return _inner->title(); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index 09fca4419..23b4e7c93 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -76,6 +76,8 @@ public: rpl::producer<bool> desiredShadowVisibility() const override; + bool closeByOutsideClick() const override; + void checkBeforeClose(Fn<void()> close) override; rpl::producer<QString> title() override; void enableBackButton() override; diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index a31e2958f..a5fef26a2 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -38,6 +38,7 @@ public: not_null<Window::SessionController*> controller); ~AwayMessage(); + [[nodiscard]] bool closeByOutsideClick() const override; [[nodiscard]] rpl::producer<QString> title() override; private: @@ -199,6 +200,10 @@ AwayMessage::~AwayMessage() { } } +bool AwayMessage::closeByOutsideClick() const { + return false; +} + rpl::producer<QString> AwayMessage::title() { return tr::lng_away_title(); } diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d9b17f260..78d446200 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -53,6 +53,7 @@ public: not_null<Window::SessionController*> controller); ~Chatbots(); + [[nodiscard]] bool closeByOutsideClick() const override; [[nodiscard]] rpl::producer<QString> title() override; const Ui::RoundRect *bottomSkipRounding() const override { @@ -380,6 +381,10 @@ Chatbots::~Chatbots() { } } +bool Chatbots::closeByOutsideClick() const { + return false; +} + rpl::producer<QString> Chatbots::title() { return tr::lng_chatbots_title(); } @@ -402,7 +407,7 @@ void Chatbots::setupContent( .about = tr::lng_chatbots_about( lt_link, tr::lng_chatbots_about_link( - ) | Ui::Text::ToLink(u"internal:about_business_chatbots"_q), + ) | Ui::Text::ToLink(tr::lng_chatbots_info_url(tr::now)), Ui::Text::WithEntities), .aboutMargins = st::peerAppearanceCoverLabelMargin, }); @@ -485,7 +490,7 @@ void Chatbots::save() { .recipients = _recipients.current(), .repliesAllowed = _repliesAllowed.current(), }, [=] { - }, [=](QString error) { show->showToast(error); }); + }, fail); } } // namespace diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 932c1d980..9809c1b71 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -42,6 +42,7 @@ public: not_null<Window::SessionController*> controller); ~Greeting(); + [[nodiscard]] bool closeByOutsideClick() const override; [[nodiscard]] rpl::producer<QString> title() override; const Ui::RoundRect *bottomSkipRounding() const override { @@ -105,6 +106,10 @@ Greeting::~Greeting() { } } +bool Greeting::closeByOutsideClick() const { + return false; +} + rpl::producer<QString> Greeting::title() { return tr::lng_greeting_title(); } diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index 960c354b2..fb2ac16e9 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -168,8 +168,10 @@ void AddBusinessRecipientsSelector( modify(now); *data = std::move(now); }; + const auto ¤t = data->current(); + const auto all = current.allButExcluded || current.included.empty(); const auto group = std::make_shared<Ui::RadiobuttonGroup>( - data->current().allButExcluded ? kAllExcept : kSelectedOnly); + all ? kAllExcept : kSelectedOnly); const auto everyone = container->add( object_ptr<Ui::Radiobutton>( container, @@ -281,6 +283,12 @@ void AddBusinessRecipientsSelector( }, lifetime); SetupBusinessChatsPreview(includeInner, included); + included->value( + ) | rpl::start_with_next([=](const Data::BusinessChats &value) { + if (value.empty() && group->current() == kSelectedOnly) { + group->setValue(kAllExcept); + } + }, lifetime); includeWrap->toggleOn(data->value( ) | rpl::map([](const Data::BusinessRecipients &value) { @@ -289,6 +297,20 @@ void AddBusinessRecipientsSelector( includeWrap->finishAnimating(); group->setChangedCallback([=](int value) { + if (value == kSelectedOnly && data->current().included.empty()) { + group->setValue(kAllExcept); + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + group->setValue(kSelectedOnly); + }; + EditBusinessChats(controller, { + .save = crl::guard(includeAdd, save), + .include = true, + }); + return; + } change([&](Data::BusinessRecipients &data) { data.allButExcluded = (value == kAllExcept); }); diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index bae1b6b3d..1d06c99ce 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -44,6 +44,7 @@ public: not_null<Window::SessionController*> controller); ~WorkingHours(); + [[nodiscard]] bool closeByOutsideClick() const override; [[nodiscard]] rpl::producer<QString> title() override; private: @@ -529,6 +530,10 @@ WorkingHours::~WorkingHours() { } } +bool WorkingHours::closeByOutsideClick() const { + return false; +} + rpl::producer<QString> WorkingHours::title() { return tr::lng_hours_title(); } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index c279640a5..c36927978 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -70,6 +70,12 @@ public: [[nodiscard]] virtual rpl::producer<std::vector<Type>> removeFromStack() { return nullptr; } + [[nodiscard]] virtual bool closeByOutsideClick() const { + return true; + } + virtual void checkBeforeClose(Fn<void()> close) { + close(); + } [[nodiscard]] virtual rpl::producer<QString> title() = 0; virtual void sectionSaveChanges(FnMut<void()> done) { done(); From d608bffecbd86bcdff540fc802436399f59991d6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:15:27 +0400 Subject: [PATCH 075/108] Fix limit in business features exception box. --- .../SourceFiles/boxes/filters/edit_filter_box.cpp | 12 +++++++++--- .../boxes/filters/edit_filter_chats_list.cpp | 12 ++++++------ .../boxes/filters/edit_filter_chats_list.h | 6 +++--- .../settings/business/settings_recipients_helper.cpp | 5 ++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 958d01436..ce23d235f 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_peer.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_user.h" #include "core/application.h" @@ -124,6 +125,12 @@ void EditExceptions( const auto include = (options & Flag::Contacts) != Flags(0); const auto rules = data->current(); const auto session = &window->session(); + const auto limit = Data::PremiumLimits( + session + ).dialogFiltersChatsCurrent(); + const auto showLimitReached = [=] { + window->show(Box(FilterChatsLimitBox, session, limit, include)); + }; auto controller = std::make_unique<EditFilterChatsListController>( session, (include @@ -132,9 +139,8 @@ void EditExceptions( options, rules.flags() & options, include ? rules.always() : rules.never(), - [=](int count) { - return Box(FilterChatsLimitBox, session, count, include); - }); + limit, + showLimitReached); const auto rawController = controller.get(); auto initBox = [=](not_null<PeerListBox*> box) { box->setCloseByOutsideClick(false); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 25463f1e2..0ee2bace0 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -333,17 +333,17 @@ EditFilterChatsListController::EditFilterChatsListController( Flags options, Flags selected, const base::flat_set<not_null<History*>> &peers, - LimitBoxFactory limitBox) + int limit, + Fn<void()> showLimitReached) : ChatsListBoxController(session) , _session(session) -, _limitBox(std::move(limitBox)) +, _showLimitReached(std::move(showLimitReached)) , _title(std::move(title)) , _peers(peers) , _options(options & ~Flag::Chatlist) , _selected(selected) -, _limit(Data::PremiumLimits(session).dialogFiltersChatsCurrent()) +, _limit(limit) , _chatlist(options & Flag::Chatlist) { - Expects(_limitBox != nullptr); } Main::Session &EditFilterChatsListController::session() const { @@ -371,8 +371,8 @@ void EditFilterChatsListController::rowClicked(not_null<PeerListRow*> row) { if (count < _limit || row->checked()) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); - } else { - delegate()->peerListUiShow()->showBox(_limitBox(count)); + } else if (const auto copy = _showLimitReached) { + copy(); } } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index a9dfd3fa2..26e0529c3 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -43,7 +43,6 @@ class EditFilterChatsListController final : public ChatsListBoxController { public: using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; - using LimitBoxFactory = Fn<object_ptr<Ui::BoxContent>(int)>; EditFilterChatsListController( not_null<Main::Session*> session, @@ -51,7 +50,8 @@ public: Flags options, Flags selected, const base::flat_set<not_null<History*>> &peers, - LimitBoxFactory limitBox); + int limit, + Fn<void()> showLimitReached); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -72,7 +72,7 @@ private: void updateTitle(); const not_null<Main::Session*> _session; - const LimitBoxFactory _limitBox; + const Fn<void()> _showLimitReached; rpl::producer<QString> _title; base::flat_set<not_null<History*>> _peers; Flags _options; diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index fb2ac16e9..7f7f2b715 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -72,9 +72,8 @@ void EditBusinessChats( options, TypesToFlags(descriptor.current.types) & options, base::flat_set<not_null<History*>>(begin(peers), end(peers)), - [=](int count) { - return nullptr; AssertIsDebug(); - }); + 100, + nullptr); const auto rawController = controller.get(); const auto save = descriptor.save; auto initBox = [=](not_null<PeerListBox*> box) { From d14f11bd8830dc685d918701c3185df1e9625735 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:16:08 +0400 Subject: [PATCH 076/108] fixup Implement preview and save of chatbots. --- Telegram/SourceFiles/settings/settings_business.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 936f0c49f..157438868 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -225,9 +225,9 @@ void AddBusinessSummary( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = FallbackOrder(); AssertIsDebug();/* account.appConfig().get<Order>( + const auto mtpOrder = account.appConfig().get<Order>( "business_promo_order", - FallbackOrder());*/ + FallbackOrder()); const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); From 6d352597b4f72cb6e1269e4b5052c713a51be6f0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:21:18 +0400 Subject: [PATCH 077/108] Disable quick replies in bot chats. --- Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 1cf45d762..08b5abf14 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -638,7 +638,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } } } - const auto shortcuts = _user + const auto shortcuts = (_user && !_user->isBot()) ? _user->owner().shortcutMessages().shortcuts().list : base::flat_map<BusinessShortcutId, Data::Shortcut>(); if (!hasUsername && !shortcuts.empty()) { From 0a8e96114209e2cb5d69ef2a076a4d062e5c2160 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:47:40 +0400 Subject: [PATCH 078/108] Suggest premium when sending existing quick replies. --- Telegram/SourceFiles/history/history_widget.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 043fd7b8c..3b6ab5dd5 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "boxes/edit_caption_box.h" #include "boxes/premium_limits_box.h" +#include "boxes/premium_preview_box.h" #include "boxes/peers/edit_peer_permissions_box.h" // ShowAboutGigagroup. #include "boxes/peers/edit_peer_requests_box.h" #include "core/file_utilities.h" @@ -439,12 +440,16 @@ HistoryWidget::HistoryWidget( && data.method != Method::ByTab) ? messages->lookupShortcutId(data.command.mid(1)) : BusinessShortcutId(); - if (shortcutId) { + if (!shortcutId) { + insertHashtagOrBotCommand(data.command, data.method); + } else if (!_peer->session().premium()) { + ShowPremiumPreviewToBuy( + controller, + PremiumFeature::QuickReplies); + } else { session().api().sendShortcutMessages(_peer, shortcutId); session().api().finishForwarding(prepareSendAction({})); setFieldText(_field->getTextWithTagsPart(_field->textCursor().position())); - } else { - insertHashtagOrBotCommand(data.command, data.method); } }, lifetime()); From 49ec0517605bf40086fe0f9efb6cc1167784be95 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 17:47:52 +0400 Subject: [PATCH 079/108] Make premium toast adaptive. --- Telegram/SourceFiles/window/window_session_controller.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 92727609d..15ea551e8 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1427,8 +1427,10 @@ void SessionController::setupPremiumToast() { session().mtp().requestConfig(); return premium; }) | rpl::start_with_next([=] { - MainWindowShow(this).showToast( - { tr::lng_premium_success(tr::now) }); + MainWindowShow(this).showToast({ + .text = { tr::lng_premium_success(tr::now) }, + .adaptive = true, + }); }, _lifetime); } From 288979d8e7bc2eb2326c0cd9a811237e5fabfec4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 20:55:04 +0400 Subject: [PATCH 080/108] Allow editing quick replies from the suggestions. --- Telegram/Resources/langs/lang.strings | 1 + .../chat_helpers/field_autocomplete.cpp | 23 ++++++++-- .../SourceFiles/history/history_widget.cpp | 19 ++++++--- .../business/settings_quick_replies.cpp | 7 ++++ .../business/settings_shortcut_messages.cpp | 42 +++++++++++++++++++ Telegram/SourceFiles/ui/chat/chat.style | 3 ++ 6 files changed, 87 insertions(+), 8 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f13454d7d..2489020b8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2228,6 +2228,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_replies_message_placeholder" = "Add a Quick Reply"; "lng_replies_delete_sure" = "Are you sure you want to delete this quick reply with all its messages?"; "lng_replies_error_occupied" = "This shortcut is already used."; +"lng_replies_edit_button" = "Edit Quick Replies"; "lng_greeting_title" = "Greeting Message"; "lng_greeting_about" = "Greet customers when they message you the first time or after a period of no activity."; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 08b5abf14..d5aba296b 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -641,7 +641,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { const auto shortcuts = (_user && !_user->isBot()) ? _user->owner().shortcutMessages().shortcuts().list : base::flat_map<BusinessShortcutId, Data::Shortcut>(); - if (!hasUsername && !shortcuts.empty()) { + if (!hasUsername && brows.empty() && !shortcuts.empty()) { const auto self = _user->session().user(); for (const auto &[id, shortcut] : shortcuts) { if (shortcut.count < 1) { @@ -658,6 +658,9 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { self->activeUserpicView() }); } + if (!brows.empty()) { + brows.insert(begin(brows), BotCommandRow{ self }); // Edit. + } } } rowsUpdated( @@ -1096,6 +1099,15 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { } else { auto &row = _brows->at(i); const auto user = row.user; + if (user->isSelf() && row.command.isEmpty()) { + p.setPen(st::windowActiveTextFg); + p.setFont(st::semiboldFont); + p.drawText( + QRect(0, i * st::mentionHeight, width(), st::mentionHeight), + tr::lng_replies_edit_button(tr::now), + style::al_center); + continue; + } auto toHighlight = row.command; int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); @@ -1163,7 +1175,13 @@ void FieldAutocomplete::Inner::clearSel(bool hidden) { _overDelete = false; _mouseSelection = false; _lastMousePosition = std::nullopt; - setSel((_mrows->empty() && _brows->empty() && _hrows->empty()) ? -1 : 0); + setSel((_mrows->empty() && _brows->empty() && _hrows->empty()) + ? -1 + : (_brows->size() > 1 + && _brows->front().user->isSelf() + && _brows->front().command.isEmpty()) + ? 1 + : 0); if (hidden) { _down = -1; _previewShown = false; @@ -1269,7 +1287,6 @@ bool FieldAutocomplete::Inner::chooseAtIndex( const auto commandString = QString("/%1%2").arg( command, insertUsername ? ('@' + PrimaryUsername(user)) : QString()); - _botCommandChosen.fire({ user, commandString, method }); return true; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 3b6ab5dd5..8a5fd3dfa 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -123,6 +123,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwidget.h" #include "mainwindow.h" +#include "settings/business/settings_quick_replies.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/file_upload.h" @@ -378,6 +379,11 @@ HistoryWidget::HistoryWidget( checkFieldAutocomplete(); }, Qt::QueuedConnection); + controller->session().data().shortcutMessages().shortcutsChanged( + ) | rpl::start_with_next([=] { + checkFieldAutocomplete(); + }, lifetime()); + _fieldBarCancel->hide(); _topBar->hide(); @@ -435,12 +441,15 @@ HistoryWidget::HistoryWidget( ) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) { using Method = FieldAutocomplete::ChooseMethod; const auto messages = &data.user->owner().shortcutMessages(); - const auto shortcutId = (_peer - && data.user->isSelf() - && data.method != Method::ByTab) - ? messages->lookupShortcutId(data.command.mid(1)) + const auto shortcut = data.user->isSelf(); + const auto command = data.command.mid(1); + const auto byTab = (data.method == Method::ByTab); + const auto shortcutId = (_peer && shortcut && !byTab) + ? messages->lookupShortcutId(command) : BusinessShortcutId(); - if (!shortcutId) { + if (shortcut && command.isEmpty()) { + controller->showSettings(Settings::QuickRepliesId()); + } else if (!shortcutId) { insertHashtagOrBotCommand(data.command, data.method); } else if (!_peer->session().premium()) { ShowPremiumPreviewToBuy( diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index d45d94750..cc55987cb 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_quick_replies.h" +#include "boxes/premium_preview_box.h" #include "core/application.h" #include "data/business/data_shortcut_messages.h" #include "data/data_session.h" @@ -96,6 +97,12 @@ void QuickReplies::setupContent( )); add->setClickedCallback([=] { + if (!controller->session().premium()) { + ShowPremiumPreviewToBuy( + controller, + PremiumFeature::QuickReplies); + return; + } const auto submit = [=](QString name, Fn<void()> close) { const auto id = messages->emplaceShortcut(name); showOther(ShortcutMessagesId(id)); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 91ba0c63f..2e1f577ca 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "boxes/delete_messages_box.h" #include "boxes/premium_limits_box.h" +#include "boxes/premium_preview_box.h" #include "boxes/send_files_box.h" #include "chat_helpers/tabbed_selector.h" #include "core/file_utilities.h" @@ -253,6 +254,7 @@ private: void showAtEnd(); void finishSending(); void refreshEmptyText(); + bool showPremiumRequired() const; const not_null<Window::SessionController*> _controller; const not_null<Main::Session*> _session; @@ -508,6 +510,12 @@ void ShortcutMessages::fillTopBarMenu( const auto messages = &owner->shortcutMessages(); addAction(tr::lng_context_edit_shortcut(tr::now), [=] { + if (!_controller->session().premium()) { + ShowPremiumPreviewToBuy( + _controller, + PremiumFeature::QuickReplies); + return; + } const auto submit = [=](QString name, Fn<void()> close) { const auto id = _shortcutId.current(); const auto error = [=](QString text) { @@ -773,6 +781,7 @@ QPointer<Ui::RpWidget> ShortcutMessages::createPinnedToBottom( _composeControls = std::make_unique<ComposeControls>( dynamic_cast<Ui::RpWidget*>(_scroll->parentWidget()), ComposeControlsDescriptor{ + .stOverride = &st::repliesComposeControls, .show = _controller->uiShow(), .unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); @@ -1127,6 +1136,9 @@ bool ShortcutMessages::showSendingFilesError( bool ShortcutMessages::showSendingFilesError( const Ui::PreparedList &list, std::optional<bool> compress) const { + if (showPremiumRequired()) { + return true; + } const auto text = [&] { using Error = Ui::PreparedList::Error; switch (list.error) { @@ -1171,6 +1183,9 @@ void ShortcutMessages::send() { } void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { + if (showPremiumRequired()) { + return; + } auto action = prepareSendAction(data.options); _session->api().sendVoiceMessage( data.bytes, @@ -1184,6 +1199,9 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { } void ShortcutMessages::send(Api::SendOptions options) { + if (showPremiumRequired()) { + return; + } _cornerButtons.clearReplyReturns(); auto message = Api::MessageToSend(prepareSendAction(options)); @@ -1409,6 +1427,9 @@ void ShortcutMessages::sendingFilesConfirmed( void ShortcutMessages::chooseAttach( std::optional<bool> overrideSendImagesAsPhotos) { + if (showPremiumRequired()) { + return; + } _choosingAttach = false; const auto filter = (overrideSendImagesAsPhotos == true) @@ -1472,6 +1493,10 @@ bool ShortcutMessages::sendExistingDocument( not_null<DocumentData*> document, Api::SendOptions options, std::optional<MsgId> localId) { + if (showPremiumRequired()) { + return false; + } + Api::SendExistingDocument( Api::MessageToSend(prepareSendAction(options)), document, @@ -1489,6 +1514,9 @@ void ShortcutMessages::sendExistingPhoto(not_null<PhotoData*> photo) { bool ShortcutMessages::sendExistingPhoto( not_null<PhotoData*> photo, Api::SendOptions options) { + if (showPremiumRequired()) { + return false; + } Api::SendExistingPhoto( Api::MessageToSend(prepareSendAction(options)), photo); @@ -1501,6 +1529,9 @@ bool ShortcutMessages::sendExistingPhoto( void ShortcutMessages::sendInlineResult( not_null<InlineBots::Result*> result, not_null<UserData*> bot) { + if (showPremiumRequired()) { + return; + } const auto errorText = result->getErrorOnSend(_history); if (!errorText.isEmpty()) { _controller->showToast(errorText); @@ -1520,6 +1551,9 @@ void ShortcutMessages::sendInlineResult( not_null<UserData*> bot, Api::SendOptions options, std::optional<MsgId> localMessageId) { + if (showPremiumRequired()) { + return; + } auto action = prepareSendAction(options); action.generateLocal = true; _session->api().sendInlineResult(bot, result, action, localMessageId); @@ -1564,6 +1598,14 @@ FullReplyTo ShortcutMessages::replyTo() const { return _composeControls->replyingToMessage(); } +bool ShortcutMessages::showPremiumRequired() const { + if (!_controller->session().premium()) { + ShowPremiumPreviewToBuy(_controller, PremiumFeature::QuickReplies); + return true; + } + return false; +} + } // namespace Type ShortcutMessagesId(int shortcutId) { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index b4217d85b..655ea9896 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1050,6 +1050,9 @@ awayEmptyIcon: icon{{ "chat/large_away", msgServiceFg }}; repliesEmptyWidth: 264px; repliesEmptySkip: 16px; repliesEmptyPadding: margins(10px, 20px, 10px, 16px); +repliesComposeControls: ComposeControls(defaultComposeControls) { + tabbedHeightMin: 220px; +} boostMessageIcon: icon {{ "stories/boost_mini", windowFg }}; boostMessageIconPadding: margins(0px, 2px, 0px, 0px); From 9483d17fc8a236bc051baa5ce913b0be7f2c7b25 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 21:24:32 +0400 Subject: [PATCH 081/108] Validate quick reply name. --- .../business/settings_quick_replies.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index cc55987cb..97b0c9a61 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -162,6 +162,22 @@ void QuickReplies::setupContent( Ui::ResizeFitChild(this, content); } +[[nodiscard]] bool ValidShortcutName(const QString &name) { + if (name.isEmpty() || name.size() > 32) { + return false; + } + for (const auto &ch : name) { + if (!ch.isLetterOrNumber() + && (ch != '_') + && (ch != 0x200c) + && (ch != 0x00b7) + && (ch < 0x0d80 || ch > 0x0dff)) { + return false; + } + } + return true; +} + } // namespace Type QuickRepliesId() { @@ -195,7 +211,7 @@ void EditShortcutNameBox( const auto callback = [=] { const auto name = field->getLastText().trimmed(); - if (name.isEmpty()) { + if (!ValidShortcutName(name)) { field->showError(); } else { submit(name, [weak = Ui::MakeWeak(box)] { From bef26cf9d2e4dec17940ca6963a0165e1fd3d234 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 21:24:46 +0400 Subject: [PATCH 082/108] Force right-alignment in quick replies editing. --- .../SourceFiles/history/view/history_view_list_widget.cpp | 6 +++++- .../SourceFiles/history/view/history_view_list_widget.h | 2 ++ .../settings/business/settings_shortcut_messages.cpp | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index c37342fa2..9c2f94086 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1734,7 +1734,7 @@ void ListWidget::elementHandleViaClick(not_null<UserData*> bot) { } bool ListWidget::elementIsChatWide() { - return _isChatWide; + return _overrideIsChatWide.value_or(_isChatWide); } not_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() { @@ -3963,6 +3963,10 @@ void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) { } } +void ListWidget::overrideIsChatWide(bool isWide) { + _overrideIsChatWide = isWide; +} + ListWidget::~ListWidget() { // Destroy child widgets first, because they may invoke leaveEvent-s. _emptyInfo = nullptr; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index c15a041b8..12e09accf 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -343,6 +343,7 @@ public: QString elementAuthorRank(not_null<const Element*> view) override; void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w); + void overrideIsChatWide(bool isWide); ~ListWidget(); @@ -725,6 +726,7 @@ private: bool _refreshingViewer = false; bool _showFinished = false; bool _resizePending = false; + std::optional<bool> _overrideIsChatWide; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 2e1f577ca..3b1143126 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -372,6 +372,7 @@ ShortcutMessages::ShortcutMessages( this, controller, static_cast<ListDelegate*>(this)); + _inner->overrideIsChatWide(false); _scroll->sizeValue() | rpl::filter([](QSize size) { return !size.isEmpty(); From 4975cf2ec1e07140078ad598d0f33501057a8ec1 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 7 Mar 2024 22:59:44 +0400 Subject: [PATCH 083/108] Implement double-drumroll time picker. --- .../inline_bots/bot_attach_web_view.cpp | 2 +- .../business/settings_working_hours.cpp | 162 +++++++++++++----- .../ui/widgets/vertical_drum_picker.cpp | 17 +- .../ui/widgets/vertical_drum_picker.h | 3 + 4 files changed, 136 insertions(+), 48 deletions(-) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 39c48f7ad..056aa224e 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -571,7 +571,7 @@ bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) { Core::App().domain().activate(&bot->session().account()); } const auto window = !bot->session().windows().empty() - ? bot->session().windows().front() + ? bot->session().windows().front().get() : nullptr; const auto variant = QVariant::fromValue(ClickHandlerContext{ .attachBotWebviewUrl = shownUrl, diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 1d06c99ce..db30dd847 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -86,6 +86,34 @@ private: : wrap(time == kDay ? 0 : time); } +[[nodiscard]] QString FormatTimeHour(TimeId time) { + const auto wrap = [](TimeId value) { + return QString::number(value / 3600).rightJustified(2, u'0'); + }; + if (time < kDay) { + return wrap(time); + } + const auto wrapped = wrap(time - kDay); + const auto result = tr::lng_hours_next_day(tr::now, lt_time, wrapped); + const auto i = result.indexOf(wrapped); + return (i >= 0) ? (result.left(i) + wrapped) : result; +} + +[[nodiscard]] QString FormatTimeMinute(TimeId time) { + const auto wrap = [](TimeId value) { + return QString::number(value / 60).rightJustified(2, u'0'); + }; + if (time < kDay) { + return wrap(time); + } + const auto wrapped = wrap(time - kDay); + const auto result = tr::lng_hours_next_day(tr::now, lt_time, wrapped); + const auto i = result.indexOf(wrapped); + return (i >= 0) + ? (wrapped + result.right(result.size() - i - wrapped.size())) + : result; +} + [[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) { auto result = QStringList(); result.reserve(data.list.size()); @@ -105,49 +133,97 @@ void EditTimeBox( Fn<void(TimeId)> save) { Expects(low <= high); - const auto values = (high - low + 60) / 60; - const auto startIndex = (value - low) / 60; - const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>( box, st::settingsWorkingHoursPicker)); const auto font = st::boxTextFont; const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; - auto paintCallback = [=]( - QPainter &p, - int index, - float64 y, - float64 distanceFromCenter, - int outerWidth) { - const auto r = QRectF(0, y, outerWidth, itemHeight); - const auto progress = std::abs(distanceFromCenter); - const auto revProgress = 1. - progress; - p.save(); - p.translate(r.center()); - constexpr auto kMinYScale = 0.2; - const auto yScale = kMinYScale - + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); - p.scale(1., yScale); - p.translate(-r.center()); - p.setOpacity(revProgress); - p.setFont(font); - p.setPen(st::defaultFlatLabel.textFg); - p.drawText(r, FormatDayTime(low + index * 60, true), style::al_center); - p.restore(); + const auto picker = [=]( + int count, + int startIndex, + Fn<void(QPainter &p, QRectF rect, int index)> paint) { + auto paintCallback = [=]( + QPainter &p, + int index, + float64 y, + float64 distanceFromCenter, + int outerWidth) { + const auto r = QRectF(0, y, outerWidth, itemHeight); + const auto progress = std::abs(distanceFromCenter); + const auto revProgress = 1. - progress; + p.save(); + p.translate(r.center()); + constexpr auto kMinYScale = 0.2; + const auto yScale = kMinYScale + + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); + p.scale(1., yScale); + p.translate(-r.center()); + p.setOpacity(revProgress); + p.setFont(font); + p.setPen(st::defaultFlatLabel.textFg); + paint(p, r, index); + p.restore(); + }; + return Ui::CreateChild<Ui::VerticalDrumPicker>( + content, + std::move(paintCallback), + count, + itemHeight, + startIndex); }; - const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>( - content, - std::move(paintCallback), - values, - itemHeight, - startIndex); + const auto hoursCount = (high - low + 3600) / 3600; + const auto hoursStartIndex = (value - low) / 3600; + const auto hoursPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + FormatTimeHour(((low / 3600) + index) * 3600), + style::al_right); + }; + const auto hours = picker(hoursCount, hoursStartIndex, hoursPaint); + const auto minutes = content->lifetime().make_state< + rpl::variable<Ui::VerticalDrumPicker*> + >(nullptr); + const auto minutesStart = content->lifetime().make_state<TimeId>(); + hours->value() | rpl::start_with_next([=](int hoursIndex) { + const auto start = std::max(low, (hoursIndex + (low / 3600)) * 3600); + const auto end = std::min(high, ((start / 3600) * 60 + 59) * 60); + const auto minutesCount = (end - start + 60) / 60; + const auto minutesStartIndex = minutes->current() + ? std::clamp( + ((((*minutesStart) / 60 + minutes->current()->index()) % 60) + - ((start / 60) % 60)), + 0, + (minutesCount - 1)) + : std::clamp((value - start) / 60, 0, minutesCount - 1); + *minutesStart = start; - content->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - picker->resize(s.width(), s.height()); - picker->moveToLeft((s.width() - picker->width()) / 2, 0); + const auto minutesPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + FormatTimeMinute((((start / 60) + index) % 60) * 60), + style::al_left); + }; + const auto updated = picker( + minutesCount, + minutesStartIndex, + minutesPaint); + delete minutes->current(); + *minutes = updated; + minutes->current()->show(); + }, hours->lifetime()); + + const auto separator = u":"_q; + const auto separatorWidth = st::boxTextFont->width(separator); + + rpl::combine( + content->sizeValue(), + minutes->value() + ) | rpl::start_with_next([=](QSize s, Ui::VerticalDrumPicker *minutes) { + const auto half = (s.width() - separatorWidth) / 2; + hours->setGeometry(0, 0, half, s.height()); + minutes->setGeometry(half + separatorWidth, 0, half, s.height()); }, content->lifetime()); content->paintRequest( @@ -163,28 +239,22 @@ void EditTimeBox( st::defaultInputField.borderActive); p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); + p.drawText(QRectF(content->rect()), separator, style::al_center); }, content->lifetime()); - base::install_event_filter(content, [=](not_null<QEvent*> e) { - if ((e->type() == QEvent::MouseButtonPress) - || (e->type() == QEvent::MouseButtonRelease) - || (e->type() == QEvent::MouseMove)) { - picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get())); - } else if (e->type() == QEvent::Wheel) { - picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get())); - } - return base::EventFilterResult::Continue; - }); base::install_event_filter(box, [=](not_null<QEvent*> e) { if (e->type() == QEvent::KeyPress) { - picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); + hours->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); } return base::EventFilterResult::Continue; }); box->addButton(tr::lng_settings_save(), [=] { const auto weak = Ui::MakeWeak(box); - save(std::clamp(low + picker->index() * 60, low, high)); + save(std::clamp( + ((*minutesStart) / 60 + minutes->current()->index()) * 60, + low, + high)); if (const auto strong = weak.data()) { strong->closeBox(); } diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp index 58fd123ea..73f2ecf8c 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp @@ -92,6 +92,8 @@ VerticalDrumPicker::VerticalDrumPicker( _loopData.minIndex = -_itemsVisible.centerOffset; _loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset; } + + _changes.fire({}); }, lifetime()); paintRequest( @@ -144,7 +146,9 @@ void VerticalDrumPicker::increaseShift(float64 by) { index++; index = normalizedIndex(index); } - if (!_loopData.looped && (index <= _loopData.minIndex)) { + if (_loopData.minIndex == _loopData.maxIndex) { + _shift = 0.; + } else if (!_loopData.looped && (index <= _loopData.minIndex)) { _shift = std::min(0., shift); _index = _loopData.minIndex; } else if (!_loopData.looped && (index >= _loopData.maxIndex)) { @@ -154,6 +158,7 @@ void VerticalDrumPicker::increaseShift(float64 by) { _shift = shift; _index = index; } + _changes.fire({}); update(); } @@ -270,4 +275,14 @@ int VerticalDrumPicker::index() const { return normalizedIndex(_index + _itemsVisible.centerOffset); } +rpl::producer<int> VerticalDrumPicker::changes() const { + return _changes.events() | rpl::map([=] { return index(); }); +} + +rpl::producer<int> VerticalDrumPicker::value() const { + return rpl::single(index()) + | rpl::then(changes()) + | rpl::distinct_until_changed(); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h index 4140d3397..63c0cbc2a 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h @@ -52,6 +52,8 @@ public: bool looped = false); [[nodiscard]] int index() const; + [[nodiscard]] rpl::producer<int> changes() const; + [[nodiscard]] rpl::producer<int> value() const; void handleWheelEvent(not_null<QWheelEvent*> e); void handleMouseEvent(not_null<QMouseEvent*> e); @@ -84,6 +86,7 @@ private: int _index = 0; float64 _shift = 0.; + rpl::event_stream<> _changes; struct { const bool looped; From 5397f64b23df621b025c3b36906d6de5324cb6be Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 10:52:58 +0400 Subject: [PATCH 084/108] Add Telegram Business icon to Premium promo. --- .../Resources/icons/settings/premium/market.png | Bin 0 -> 396 bytes .../icons/settings/premium/market@2x.png | Bin 0 -> 607 bytes .../icons/settings/premium/market@3x.png | Bin 0 -> 955 bytes Telegram/SourceFiles/settings/settings.style | 2 +- .../SourceFiles/settings/settings_premium.cpp | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 Telegram/Resources/icons/settings/premium/market.png create mode 100644 Telegram/Resources/icons/settings/premium/market@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/market@3x.png diff --git a/Telegram/Resources/icons/settings/premium/market.png b/Telegram/Resources/icons/settings/premium/market.png new file mode 100644 index 0000000000000000000000000000000000000000..3c2b9cd2da695598c4671a97feeded862d19a48e GIT binary patch literal 396 zcmV;70dxL|P)<h;3K|Lk000e1NJLTq000;O000;W0ssI2ZxyPq00009a7bBm000XU z000XU0RWnu7ytkPK1oDDR5*?8lfR0CFdWBo!GDWGap((lDSZTY#|-Wr`W!-E!50u* zyA`^3EI5craC38W(4i;|iD_;Sjyp}`aopI?_WSwGhcA%;@a7MkPAA3~eF5hjV?00g zA$1)NhhZ3`pEOOoTrN%1EQsPb-fp*)QblD5VcWI{p_hp?O~tRN>soG!qG(J|O25*C z5XCajbBSoT+Yv$-V+f)1`7DG;l4P^lbX~_e-|zQv919_eqHr9iZCfG4@px3+!2b~h zfjZ#(zU#URmM{z%W9kgXILorhQk%?#kiPHLqOR+n&*x+bCo|{VvMjY|y<X2n(?*n1 z(=_k*`{VJDT6A4styX1OE{H~K+qP|6-D^Q)7zO}f-uycd!ZG$^Ihn85>ljr40GR(Z q$?JC<$MZZzqpGTIx0@pHM$RWs|MSec)(l$!0000<MNUMnLSTZQ`>eA7 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/market@2x.png b/Telegram/Resources/icons/settings/premium/market@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1ae9e950a0de624e05f578c59051806177afc232 GIT binary patch literal 607 zcmV-l0-*hgP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00009a7bBm000XU z000XU0RWnu7ytkQ5lKWrR9J=Wm%obYKorK$Jzj01=u=pTm5-4pFomrt0v3WG*!d87 z0`UPvEbS$oNFW9YA{GI$(4dmU%q_wVS+9FD&SZC4HowB+bLKnWOium`Kt@JJ#@F(T zW>BNin9t{hLV>xA@B7VWb2ghXJG@~$9usaRlL<{NsvQD=>2zADRDvL2SPvqePA5?m z8RmXH$vdQ~YOPjd+nYFuxL&W@?KZoGqA0HG@^?=h&+~e{9%Gu&;+`s@XTRTnEsZ2e zACV@WNtWfOrJwhZ>$>@Tp1*J20}+ED$mMcR{`yxEMe!jGB5IoUkPi_Tiv=PkiU6Q# zT3if*01;cQ7VQdb+m5amA!M`J0Kj&;jf=b84gf66ii`XG9so?!jElBy1AwmUaq)0C z(549qO$i~ou1BI_7@=qwMkKCQt57se^Z(*<xeP_ivR+G*@gkj0hu=g|6n+zczYRVb zjd)B{RV9QRkH^H3Pj34M%H=WuRI62<K=pbZ07|7&3W0pzkA9g6rxQX_2=vL&2I72; z-0yeFk+gw|#Ug7M002P{c&5L>aU4R(?RM*SyKD}!EMG1cLWt|SiG!2B7#+tE1R;DK zvIUCpJTLL+@|)g{vyCX%^KI|4PvaW~;_u~`NG^6hpVKPlI8F*Kk|ae(1y0^OX~dyD t+Ybf<o`?1IdL0gj%+oV6GBSQLe*iBkxyY_Q*-iie002ovPDHLkV1h+s1gQW3 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/premium/market@3x.png b/Telegram/Resources/icons/settings/premium/market@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5d132577d05a27bc5b568750b2d6e326d23c6005 GIT binary patch literal 955 zcmV;s14R6ZP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00009a7bBm000XU z000XU0RWnu7ytkRZAnByRA_<in!ii)Kp4lL_k2YiyGZR^p&+7FEMf;c1Q#=Oa4Wbt zcM=5ug@R*2gitz_Y+c;CI7mT4tEC{tu}GyuLm^;G?d2T~LwK8?mt58Nz0Xv7&-b3^ z+Z>lCmjZA&91e%W;cz(C{GwLYYPG||LyU2V)tEsD-QC>@g1~5gYx4QLuIsw4PnkjH z&*$@W6(Ty>Xf&b|`eibi&<W|6;_dA%>vdAA)sByknWo@;8)%l|`T6<!`ugJHV(8V( zGMP+XUS6)QuGprLWm#2KeeS5LD$BC3Qhi*p$H&L5tt|jxcki+TAp`(VPfy$1+a4ue zjWrsLWHLE_OJ)#4&1Q3FXU9{pmmoz^zT6c6V2qQ=WTVmW66|j5{{DV<clXO(5s0^X zz3v+6VoZ{xgM))_;);MV-rwJsB*{6<#h7z%@%HxSFgh3v#^bTw=y*IH3<lJb9~~Vz zjDo8%yK99I>i7GlQi<$WmI38*x!dhp1|WpG-EO&Dc954!rGCF}7x3}%;as^3k|bHS z@Vc&FUS0x#rfIVQx~}(nJpd>a3U=~Bp#T8AUe8jlX&L}rTwK`6U6-OetyYWNCCyJ0 zilUIwTrOvPY5s6}dP)M??e?s^-EKR}bGaM|P!wfW-fFdix*}AoRdQuW6HuvC%%j<C z)~KFj6-Ch;Aj|S_I3%OP;qb2>LKH>gw<DfUPEO1Ll}g2EcQj3_R;%H-GRR~ysZ`2t zG!}~qf)I&B>;ls1bUK~38;wLFf*{0VF}r|NDwWA(JQvIPUszdL`Nt9g0CYN?Ft;Xq z$fnZibXX?>0Jy%sHg>RLvB)k_u~;+`-Q3*xmHR%WibkXG*^#WRt@)LEXLWVeNJKvS z!nZN=dDwJ1^(*&|xoM&}grhuHoC9`UqE~rXb$mCtF)NrpfCijAkg=tur66*qlLs=k zv9S?E?#$-qW)L~OF^q9M9)EavNF)+LDwrjaNYv}~csw3tHU17#0mgW5Z}0i}Imjdq zlLwIpG8Q~NJNS10)8NK_8r;|?3kwS@>IgllZ(~fK!9q;x+nC4KQ{h69l;-97`}_Oy z^74Fc%?P2<Xf&s@Dp3^Y_KICq)oeCPQ6&98b0kTMMx#vklGoSQv$HdH^K&>H4u`|x da5$Wwz#kqakS`xh^u+)G002ovPDHLkV1idz$Rz*( literal 0 HcmV?d00001 diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 00f1121d1..9dcb8e191 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -94,7 +94,7 @@ settingsPremiumIconTranslations: icon {{ "settings/premium/translations", settin settingsPremiumIconTags: icon {{ "settings/premium/tags", settingsIconFg }}; settingsPremiumIconLastSeen: icon {{ "settings/premium/lastseen", settingsIconFg }}; settingsPremiumIconPrivacy: icon {{ "settings/premium/privacy", settingsIconFg }}; -settingsPremiumIconBusiness: icon {{ "settings/premium/privacy", settingsIconFg }}; +settingsPremiumIconBusiness: icon {{ "settings/premium/market", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 2b9d5e35a..4e81d19c6 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -371,7 +371,7 @@ using Order = std::vector<QString>; { u"business"_q, Entry{ - &st::settingsPremiumIconPlay, AssertIsDebug() + &st::settingsPremiumIconBusiness, tr::lng_premium_summary_subtitle_business(), tr::lng_premium_summary_about_business(), PremiumFeature::Business, From 5ebd5852ba7c223528341737f9bbba30dad9876d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 10:55:50 +0400 Subject: [PATCH 085/108] Update lib_ui submodule. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 7eaf7f8aa..edfcac751 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 7eaf7f8aaa5c7aac9cbc6e5dc92dea7944003eba +Subproject commit edfcac751dbdd93fcabac4ebf0ff731ceab8af0f From 2c03d90fc896761cdb137ddb058c5c2a2a05612e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 11:47:29 +0400 Subject: [PATCH 086/108] Fix the new time picker. --- Telegram/Resources/langs/lang.strings | 1 + .../business/settings_working_hours.cpp | 41 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2489020b8..2805ebaf6 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2205,6 +2205,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_hours_closed" = "Closed"; "lng_hours_open_full" = "Open 24 hours"; "lng_hours_next_day" = "{time} (Next day)"; +"lng_hours_on_next_day" = "Next day {time}"; "lng_hours_time_zone_title" = "Choose Time Zone"; "lng_hours_add_button" = "Add a Set of Hours"; "lng_hours_opening" = "Opening Time"; diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index db30dd847..525d069a4 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -94,20 +94,20 @@ private: return wrap(time); } const auto wrapped = wrap(time - kDay); - const auto result = tr::lng_hours_next_day(tr::now, lt_time, wrapped); + const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped); const auto i = result.indexOf(wrapped); return (i >= 0) ? (result.left(i) + wrapped) : result; } [[nodiscard]] QString FormatTimeMinute(TimeId time) { const auto wrap = [](TimeId value) { - return QString::number(value / 60).rightJustified(2, u'0'); + return QString::number((value / 60) % 60).rightJustified(2, u'0'); }; if (time < kDay) { return wrap(time); } const auto wrapped = wrap(time - kDay); - const auto result = tr::lng_hours_next_day(tr::now, lt_time, wrapped); + const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped); const auto i = result.indexOf(wrapped); return (i >= 0) ? (wrapped + result.right(result.size() - i - wrapped.size())) @@ -174,7 +174,7 @@ void EditTimeBox( }; const auto hoursCount = (high - low + 3600) / 3600; - const auto hoursStartIndex = (value - low) / 3600; + const auto hoursStartIndex = (value / 3600) - (low / 3600); const auto hoursPaint = [=](QPainter &p, QRectF rect, int index) { p.drawText( rect, @@ -185,6 +185,23 @@ void EditTimeBox( const auto minutes = content->lifetime().make_state< rpl::variable<Ui::VerticalDrumPicker*> >(nullptr); + + // hours->value() is valid only after size is set. + const auto separator = u":"_q; + const auto separatorWidth = st::boxTextFont->width(separator); + rpl::combine( + content->sizeValue(), + minutes->value() + ) | rpl::start_with_next([=](QSize s, Ui::VerticalDrumPicker *minutes) { + const auto half = (s.width() - separatorWidth) / 2; + hours->setGeometry(0, 0, half, s.height()); + if (minutes) { + minutes->setGeometry(half + separatorWidth, 0, half, s.height()); + } + }, content->lifetime()); + + Ui::SendPendingMoveResizeEvents(hours); + const auto minutesStart = content->lifetime().make_state<TimeId>(); hours->value() | rpl::start_with_next([=](int hoursIndex) { const auto start = std::max(low, (hoursIndex + (low / 3600)) * 3600); @@ -196,13 +213,13 @@ void EditTimeBox( - ((start / 60) % 60)), 0, (minutesCount - 1)) - : std::clamp((value - start) / 60, 0, minutesCount - 1); + : std::clamp((value / 60) - (start / 60), 0, minutesCount - 1); *minutesStart = start; const auto minutesPaint = [=](QPainter &p, QRectF rect, int index) { p.drawText( rect, - FormatTimeMinute((((start / 60) + index) % 60) * 60), + FormatTimeMinute(((start / 60) + index) * 60), style::al_left); }; const auto updated = picker( @@ -214,18 +231,6 @@ void EditTimeBox( minutes->current()->show(); }, hours->lifetime()); - const auto separator = u":"_q; - const auto separatorWidth = st::boxTextFont->width(separator); - - rpl::combine( - content->sizeValue(), - minutes->value() - ) | rpl::start_with_next([=](QSize s, Ui::VerticalDrumPicker *minutes) { - const auto half = (s.width() - separatorWidth) / 2; - hours->setGeometry(0, 0, half, s.height()); - minutes->setGeometry(half + separatorWidth, 0, half, s.height()); - }, content->lifetime()); - content->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(content); From d729e625e672ed1a4bfd373519a28f77663be729 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 12:28:53 +0400 Subject: [PATCH 087/108] Show business section settings only when loaded. --- .../data/business/data_business_chatbots.cpp | 11 +++ .../data/business/data_business_chatbots.h | 1 + .../data/business/data_business_info.cpp | 4 + .../data/business/data_business_info.h | 1 + .../settings/settings_business.cpp | 74 ++++++++++++++++--- 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index 8862c4e84..5c8c9f895 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -43,10 +43,21 @@ void Chatbots::preload() { .recipients = FromMTP(_owner, bot.vrecipients()), .repliesAllowed = bot.is_can_reply(), }; + } else { + _settings.force_assign(ChatbotsSettings()); } + }).fail([=](const MTP::Error &error) { + _requestId = 0; + LOG(("API Error: Could not get connected bots %1 (%2)" + ).arg(error.code() + ).arg(error.type())); }).send(); } +bool Chatbots::loaded() const { + return _loaded; +} + const ChatbotsSettings &Chatbots::current() const { return _settings.current(); } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index ca21baef6..6328b487d 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -31,6 +31,7 @@ public: ~Chatbots(); void preload(); + [[nodiscard]] bool loaded() const; [[nodiscard]] const ChatbotsSettings ¤t() const; [[nodiscard]] rpl::producer<ChatbotsSettings> changes() const; [[nodiscard]] rpl::producer<ChatbotsSettings> value() const; diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 8a65201fe..151e36dcb 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -220,6 +220,10 @@ rpl::producer<Timezones> BusinessInfo::timezonesValue() const { return _timezones.value(); } +bool BusinessInfo::timezonesLoaded() const { + return !_timezones.current().list.empty(); +} + QString FindClosestTimezoneId(const std::vector<Timezone> &list) { const auto local = QDateTime::currentDateTime(); const auto utc = QDateTime(local.date(), local.time(), Qt::UTC); diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index adea50a17..933d86910 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -37,6 +37,7 @@ public: [[nodiscard]] rpl::producer<> greetingSettingsChanged() const; void preloadTimezones(); + [[nodiscard]] bool timezonesLoaded() const; [[nodiscard]] rpl::producer<Timezones> timezonesValue() const; private: diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 157438868..c9123c528 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_business_info.h" #include "data/business/data_business_chatbots.h" #include "data/business/data_shortcut_messages.h" +#include "data/data_changes.h" #include "data/data_peer_values.h" // AmPremiumValue. #include "data/data_session.h" +#include "data/data_user.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -317,6 +319,8 @@ private: rpl::event_stream<> _showFinished; rpl::variable<QString> _buttonText; + PremiumFeature _waitingToShow = PremiumFeature::Business; + }; Business::Business( @@ -355,20 +359,14 @@ void Business::setStepDataReference(std::any &data) { void Business::setupContent() { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - _controller->session().data().chatbots().preload(); - _controller->session().data().businessInfo().preload(); - _controller->session().data().shortcutMessages().preloadShortcuts(); + const auto owner = &_controller->session().data(); + owner->chatbots().preload(); + owner->businessInfo().preload(); + owner->shortcutMessages().preloadShortcuts(); Ui::AddSkip(content, st::settingsFromFileTop); - AddBusinessSummary(content, _controller, [=](PremiumFeature feature) { - if (!_controller->session().premium()) { - _setPaused(true); - const auto hidden = crl::guard(this, [=] { _setPaused(false); }); - - ShowPremiumPreviewToBuy(_controller, feature, hidden); - return; - } + const auto showFeature = [=](PremiumFeature feature) { showOther([&] { switch (feature) { case PremiumFeature::AwayMessage: return AwayMessageId(); @@ -378,8 +376,60 @@ void Business::setupContent() { case PremiumFeature::QuickReplies: return QuickRepliesId(); case PremiumFeature::BusinessBots: return ChatbotsId(); } - Unexpected("Feature in Business::setupContent."); + Unexpected("Feature in showFeature."); }()); + }; + const auto isReady = [=](PremiumFeature feature) { + switch (feature) { + case PremiumFeature::AwayMessage: + return owner->businessInfo().awaySettingsLoaded() + && owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::BusinessHours: + return owner->session().user()->isFullLoaded() + && owner->businessInfo().timezonesLoaded(); + case PremiumFeature::BusinessLocation: + return owner->session().user()->isFullLoaded(); + case PremiumFeature::GreetingMessage: + return owner->businessInfo().greetingSettingsLoaded() + && owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::QuickReplies: + return owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::BusinessBots: + return owner->chatbots().loaded(); + } + Unexpected("Feature in isReady."); + }; + const auto check = [=] { + if (_waitingToShow != PremiumFeature::Business + && isReady(_waitingToShow)) { + showFeature( + std::exchange(_waitingToShow, PremiumFeature::Business)); + } + }; + + rpl::merge( + owner->businessInfo().awaySettingsChanged(), + owner->businessInfo().greetingSettingsChanged(), + owner->businessInfo().timezonesValue() | rpl::to_empty, + owner->shortcutMessages().shortcutsChanged(), + owner->chatbots().changes() | rpl::to_empty, + owner->session().changes().peerUpdates( + owner->session().user(), + Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty + ) | rpl::start_with_next(check, content->lifetime()); + + AddBusinessSummary(content, _controller, [=](PremiumFeature feature) { + if (!_controller->session().premium()) { + _setPaused(true); + const auto hidden = crl::guard(this, [=] { _setPaused(false); }); + + ShowPremiumPreviewToBuy(_controller, feature, hidden); + return; + } else if (!isReady(feature)) { + _waitingToShow = feature; + } else { + showFeature(feature); + } }); Ui::ResizeFitChild(this, content); From c345b50ab74e36f54ac43ccb46b58fcff50e7c1e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 12:55:20 +0400 Subject: [PATCH 088/108] Version 4.15.1. Telegram Business features. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 4 ++++ 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 54c27edef..dee90b06c 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="4.15.0.0" /> + Version="4.15.1.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 20b938e47..b8aba7a6f 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,0,0 - PRODUCTVERSION 4,15,0,0 + FILEVERSION 4,15,1,0 + PRODUCTVERSION 4,15,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "4.15.0.0" + VALUE "FileVersion", "4.15.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.15.0.0" + VALUE "ProductVersion", "4.15.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index f4f694d5b..93f0a6769 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,0,0 - PRODUCTVERSION 4,15,0,0 + FILEVERSION 4,15,1,0 + PRODUCTVERSION 4,15,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "4.15.0.0" + VALUE "FileVersion", "4.15.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.15.0.0" + VALUE "ProductVersion", "4.15.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 0f3431150..9bd491610 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 4015000; -constexpr auto AppVersionStr = "4.15"; +constexpr auto AppVersion = 4015001; +constexpr auto AppVersionStr = "4.15.1"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/build/version b/Telegram/build/version index cb2635869..60c27dece 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4015000 +AppVersion 4015001 AppVersionStrMajor 4.15 -AppVersionStrSmall 4.15 -AppVersionStr 4.15.0 +AppVersionStrSmall 4.15.1 +AppVersionStr 4.15.1 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.15 +AppVersionOriginal 4.15.1 diff --git a/changelog.txt b/changelog.txt index 4453ccb54..1d80bf48b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +4.15.1 (08.03.24) + +- Telegram Business features. + 4.15 (18.02.24) - Stories from groups. From 7c002cf8be1af0b5423e2c6f9570bfc8254239f8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 15:26:14 +0400 Subject: [PATCH 089/108] Version 4.15.1: Fix sending media albums. --- Telegram/SourceFiles/api/api_sending.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 982992ce3..495b9467e 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -511,6 +511,7 @@ void SendConfirmedFile( .date = HistoryItem::NewMessageDate(file->to.options), .shortcutId = file->to.options.shortcutId, .postAuthor = messagePostAuthor, + .groupedId = groupId, }, caption, media); } From 0df8864ae0604418014bd61b84eeffd085a7a4d9 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 8 Mar 2024 12:25:13 +0400 Subject: [PATCH 090/108] Port specific_linux to cppgir --- Telegram/CMakeLists.txt | 3 + .../platform/linux/specific_linux.cpp | 407 ++++++++++-------- 2 files changed, 224 insertions(+), 186 deletions(-) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 76e06c8e1..9285d5ad2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1679,6 +1679,9 @@ else() desktop-app::external_glibmm ) + include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake) + generate_dbus(Telegram org.freedesktop.portal. XdpBackground ${third_party_loc}/xdg-desktop-portal/data/org.freedesktop.portal.Background.xml) + if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) target_link_libraries(Telegram PRIVATE diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index c0cbed82e..c892885d0 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -38,6 +38,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <glibmm.h> #include <giomm.h> +#include <xdgdbus/xdgdbus.hpp> +#include <xdpbackground/xdpbackground.hpp> +#include <xdprequest/xdprequest.hpp> + #include <sys/stat.h> #include <sys/types.h> #include <sys/un.h> @@ -48,12 +52,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <iostream> +namespace { + +using namespace gi::repository; +namespace Gio = gi::repository::Gio; using namespace Platform; using Platform::internal::WaylandIntegration; -namespace Platform { -namespace { - void PortalAutostart(bool enabled, Fn<void(bool)> done) { if (cExeName().isEmpty()) { if (done) { @@ -62,127 +67,141 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) { return; } - const auto connection = [&] { - try { - return Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); + XdpBackground::BackgroundProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + [=](GObject::Object, Gio::AsyncResult res) { + auto proxy = XdpBackground::BackgroundProxy::new_for_bus_finish( + res); + + if (!proxy) { + if (done) { + LOG(("Portal Autostart Error: %1").arg( + proxy.error().what())); + done(false); + } + return; } - return Glib::RefPtr<Gio::DBus::Connection>(); - } - }(); - if (!connection) { - if (done) { - done(false); - } - return; - } + auto interface = XdpBackground::Background(*proxy); - const auto handleToken = Glib::ustring("tdesktop") - + std::to_string(base::RandomValue<uint>()); + const auto handleToken = "tdesktop" + + std::to_string(base::RandomValue<uint>()); - std::vector<Glib::ustring> commandline; - commandline.push_back(cExeName().toStdString()); - if (Core::Launcher::Instance().customWorkingDir()) { - commandline.push_back("-workdir"); - commandline.push_back(cWorkingDir().toStdString()); - } - commandline.push_back("-autostart"); + auto uniqueName = std::string( + proxy->get_connection().get_unique_name()); + uniqueName.erase(0, 1); + uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - std::map<Glib::ustring, Glib::VariantBase> options; - options["handle_token"] = Glib::create_variant(handleToken); - options["reason"] = Glib::create_variant( - Glib::ustring( - tr::lng_settings_auto_start(tr::now).toStdString())); - options["autostart"] = Glib::create_variant(enabled); - options["commandline"] = Glib::create_variant(commandline); - options["dbus-activatable"] = Glib::create_variant(false); + const auto window = std::make_shared<QWidget>(); + window->setAttribute(Qt::WA_DontShowOnScreen); + window->setWindowModality(Qt::ApplicationModal); + window->show(); - auto uniqueName = connection->get_unique_name(); - uniqueName.erase(0, 1); - uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); + XdpRequest::RequestProxy::new_( + proxy->get_connection(), + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath + + std::string("/request/") + + uniqueName + + '/' + + handleToken, + nullptr, + [=](GObject::Object, Gio::AsyncResult res) mutable { + auto requestProxy = XdpRequest::RequestProxy::new_finish( + res); - const auto requestPath = base::Platform::XDP::kObjectPath - + Glib::ustring("/request/") - + uniqueName - + '/' - + handleToken; - - const auto window = std::make_shared<QWidget>(); - window->setAttribute(Qt::WA_DontShowOnScreen); - window->setWindowModality(Qt::ApplicationModal); - window->show(); - - const auto signalId = std::make_shared<uint>(); - *signalId = connection->signal_subscribe( - [=]( - const Glib::RefPtr<Gio::DBus::Connection> &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - (void)window; // don't destroy until finish - - try { - const auto response = parameters.get_child( - 0 - ).get_dynamic<uint>(); - - if (response) { + if (!requestProxy) { if (done) { - LOG(("Portal Autostart Error: Request denied")); + LOG(("Portal Autostart Error: %1").arg( + requestProxy.error().what())); done(false); } - } else if (done) { - done(enabled); - } - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); - done(false); - } - } - - if (*signalId) { - connection->signal_unsubscribe(*signalId); - } - }); - }, - base::Platform::XDP::kService, - base::Platform::XDP::kRequestInterface, - "Response", - requestPath); - - connection->call( - base::Platform::XDP::kObjectPath, - "org.freedesktop.portal.Background", - "RequestBackground", - Glib::create_variant(std::tuple{ - base::Platform::XDP::ParentWindowID(), - options, - }), - [=](const Glib::RefPtr<Gio::AsyncResult> &result) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - try { - connection->call_finish(result); - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); - done(false); + return; } - if (*signalId) { - connection->signal_unsubscribe(*signalId); + auto request = XdpRequest::Request(*requestProxy); + const auto signalId = std::make_shared<ulong>(); + *signalId = request.signal_response().connect([=]( + XdpRequest::Request, + guint response, + GLib::Variant) mutable { + auto &sandbox = Core::Sandbox::Instance(); + sandbox.customEnterFromEventLoop([&] { + (void)window; // don't destroy until finish + + if (response) { + if (done) { + LOG(("Portal Autostart Error: " + "Request denied")); + done(false); + } + } else if (done) { + done(enabled); + } + + request.disconnect(*signalId); + }); + }); + + + std::vector<std::string> commandline; + commandline.push_back(cExeName().toStdString()); + if (Core::Launcher::Instance().customWorkingDir()) { + commandline.push_back("-workdir"); + commandline.push_back(cWorkingDir().toStdString()); } - } - }); - }, - base::Platform::XDP::kService); + commandline.push_back("-autostart"); + + interface.call_request_background( + std::string(base::Platform::XDP::ParentWindowID()), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(handleToken))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("reason"), + GLib::Variant::new_variant( + GLib::Variant::new_string( + tr::lng_settings_auto_start(tr::now) + .toStdString()))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("autostart"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(enabled))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("commandline"), + GLib::Variant::new_variant( + GLib::Variant::new_strv(commandline))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("dbus-activatable"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(false))), + }), + [=](GObject::Object, Gio::AsyncResult res) mutable { + auto &sandbox = Core::Sandbox::Instance(); + sandbox.customEnterFromEventLoop([&] { + const auto result = + interface.call_request_background_finish( + res); + + if (!result) { + if (done) { + LOG(("Portal Autostart Error: %1") + .arg(result.error().what())); + done(false); + } + + request.disconnect(*signalId); + } + }); + }); + }); + }); } bool GenerateDesktopFile( @@ -218,77 +237,89 @@ bool GenerateDesktopFile( return false; } - try { - const auto target = Glib::KeyFile::create(); - target->load_from_data( - sourceText, - Glib::KeyFile::Flags::KEEP_COMMENTS - | Glib::KeyFile::Flags::KEEP_TRANSLATIONS); + auto target = GLib::KeyFile::new_(); + const auto loaded = target.load_from_data( + sourceText, + -1, + GLib::KeyFileFlags::KEEP_COMMENTS_ + | GLib::KeyFileFlags::KEEP_TRANSLATIONS_); + + if (!loaded) { + if (!silent) { + LOG(("App Error: %1").arg(loaded.error().what())); + } + return false; + } - for (const auto &group : target->get_groups()) { - if (onlyMainGroup && group != "Desktop Entry") { - target->remove_group(group); - continue; + for (const auto &group : target.get_groups(nullptr)) { + if (onlyMainGroup && group != "Desktop Entry") { + const auto removed = target.remove_group(group); + if (!removed) { + if (!silent) { + LOG(("App Error: %1").arg(removed.error().what())); + } + return false; } + continue; + } - if (target->has_key(group, "TryExec")) { - target->set_string( + if (target.has_key(group, "TryExec", nullptr)) { + target.set_string( + group, + "TryExec", + KShell::joinArgs({ executable }).replace( + '\\', + qstr("\\\\")).toStdString()); + } + + if (target.has_key(group, "Exec", nullptr)) { + if (group == "Desktop Entry" && !args.isEmpty()) { + QStringList exec; + exec.append(executable); + if (Core::Launcher::Instance().customWorkingDir()) { + exec.append(u"-workdir"_q); + exec.append(cWorkingDir()); + } + exec.append(args); + target.set_string( group, - "TryExec", - KShell::joinArgs({ executable }).replace( + "Exec", + KShell::joinArgs(exec).replace( '\\', qstr("\\\\")).toStdString()); - } + } else { + auto exec = KShell::splitArgs( + QString::fromStdString( + target.get_string(group, "Exec", nullptr) + ).replace( + qstr("\\\\"), + qstr("\\"))); - if (target->has_key(group, "Exec")) { - if (group == "Desktop Entry" && !args.isEmpty()) { - QStringList exec; - exec.append(executable); + if (!exec.isEmpty()) { + exec[0] = executable; if (Core::Launcher::Instance().customWorkingDir()) { - exec.append(u"-workdir"_q); - exec.append(cWorkingDir()); + exec.insert(1, u"-workdir"_q); + exec.insert(2, cWorkingDir()); } - exec.append(args); - target->set_string( + target.set_string( group, "Exec", KShell::joinArgs(exec).replace( '\\', qstr("\\\\")).toStdString()); - } else { - auto exec = KShell::splitArgs( - QString::fromStdString( - target->get_string(group, "Exec") - ).replace( - qstr("\\\\"), - qstr("\\"))); - - if (!exec.isEmpty()) { - exec[0] = executable; - if (Core::Launcher::Instance().customWorkingDir()) { - exec.insert(1, u"-workdir"_q); - exec.insert(2, cWorkingDir()); - } - target->set_string( - group, - "Exec", - KShell::joinArgs(exec).replace( - '\\', - qstr("\\\\")).toStdString()); - } } } } + } - if (!args.isEmpty() - && target->has_key("Desktop Entry", "DBusActivatable")) { - target->remove_key("Desktop Entry", "DBusActivatable"); - } + if (!args.isEmpty()) { + target.remove_key("Desktop Entry", "DBusActivatable"); + } - target->save_to_file(targetFile.toStdString()); - } catch (const std::exception &e) { + const auto saved = target.save_to_file(targetFile.toStdString()); + if (!saved) { if (!silent) { - LOG(("App Error: %1").arg(e.what())); + LOG(("App Error: %1").arg(saved.error().what())); } return false; } @@ -357,10 +388,10 @@ bool GenerateServiceFile(bool silent = false) { DEBUG_LOG(("App Info: placing D-Bus service file to %1").arg(targetPath)); if (!QDir(targetPath).exists()) QDir().mkpath(targetPath); - const auto target = Glib::KeyFile::create(); + auto target = GLib::KeyFile::new_(); constexpr auto group = "D-BUS Service"; - target->set_string( + target.set_string( group, "Name", QGuiApplication::desktopFileName().toStdString()); @@ -371,21 +402,21 @@ bool GenerateServiceFile(bool silent = false) { exec.append(u"-workdir"_q); exec.append(cWorkingDir()); } - target->set_string( + target.set_string( group, "Exec", KShell::joinArgs(exec).toStdString()); - try { - target->save_to_file(targetFile.toStdString()); - } catch (const std::exception &e) { + const auto saved = target.save_to_file(targetFile.toStdString()); + if (!saved) { if (!silent) { - LOG(("App Error: %1").arg(e.what())); + LOG(("App Error: %1").arg(saved.error().what())); } return false; } - if (!Core::UpdaterDisabled() && !Core::Launcher::Instance().customWorkingDir()) { + if (!Core::UpdaterDisabled() + && !Core::Launcher::Instance().customWorkingDir()) { DEBUG_LOG(("App Info: removing old D-Bus service files")); char md5Hash[33] = { 0 }; @@ -397,19 +428,21 @@ bool GenerateServiceFile(bool silent = false) { md5Hash)); } - try { - Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION - )->call( - base::Platform::DBus::kObjectPath, - base::Platform::DBus::kInterface, - "ReloadConfig", - {}, - {}, - base::Platform::DBus::kService - ); - } catch (...) { - } + XdgDBus::DBusProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::DBus::kService, + base::Platform::DBus::kObjectPath, + [=](GObject::Object, Gio::AsyncResult res) { + auto interface = XdgDBus::DBus( + XdgDBus::DBusProxy::new_for_bus_finish(res, nullptr)); + + if (!interface) { + return; + } + + interface.call_reload_config(nullptr); + }); return true; } @@ -447,6 +480,8 @@ void InstallLauncher() { } // namespace +namespace Platform { + void SetApplicationIcon(const QIcon &icon) { QApplication::setWindowIcon(icon); } @@ -648,11 +683,11 @@ void start() { qputenv("PULSE_PROP_application.name", AppName.utf8()); qputenv("PULSE_PROP_application.icon_name", base::IconName().toLatin1()); - Glib::set_prgname(cExeName().toStdString()); - Glib::set_application_name(AppName.data()); + GLib::set_prgname(cExeName().toStdString()); + GLib::set_application_name(AppName.data()); Glib::init(); - Gio::init(); + ::Gio::init(); Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-%4"_q.arg( QDir::tempPath(), From 296e8c1ab1590f49f5b6c70daa5d2639e712318e Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 8 Mar 2024 15:03:47 +0400 Subject: [PATCH 091/108] Use ExecutablePathForShortcuts in PortalAutostart --- Telegram/SourceFiles/platform/linux/specific_linux.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index c892885d0..4122097bc 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -60,7 +60,8 @@ using namespace Platform; using Platform::internal::WaylandIntegration; void PortalAutostart(bool enabled, Fn<void(bool)> done) { - if (cExeName().isEmpty()) { + const auto executable = ExecutablePathForShortcuts(); + if (executable.isEmpty()) { if (done) { done(false); } @@ -149,7 +150,7 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) { std::vector<std::string> commandline; - commandline.push_back(cExeName().toStdString()); + commandline.push_back(executable.toStdString()); if (Core::Launcher::Instance().customWorkingDir()) { commandline.push_back("-workdir"); commandline.push_back(cWorkingDir().toStdString()); From 3d5092f7ad58311fc31424f1aadae0be6134f8c1 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 8 Mar 2024 15:09:21 +0400 Subject: [PATCH 092/108] Update cmake_helpers --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index b699c232d..f1628c260 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit b699c232d57d50070a7b1b861809e206624f48d4 +Subproject commit f1628c260b17c55996740fc00b00a7c227b61de2 From 990ae11f6291aa0a9d71007c0cee5e28b68c0c0a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 8 Mar 2024 15:49:30 +0400 Subject: [PATCH 093/108] Version 4.15.1: Fix build with GCC. --- .../boxes/peers/replace_boost_box.cpp | 7 +---- .../data/business/data_shortcut_messages.cpp | 1 - .../view/media/history_view_contact.cpp | 3 -- .../SourceFiles/info/info_content_widget.cpp | 5 ---- .../SourceFiles/info/info_layer_widget.cpp | 30 ------------------- .../settings/business/settings_chatbots.cpp | 1 - .../business/settings_quick_replies.cpp | 1 - .../business/settings_recipients_helper.cpp | 4 +-- .../business/settings_shortcut_messages.cpp | 8 ----- .../business/settings_working_hours.cpp | 2 -- 10 files changed, 3 insertions(+), 59 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 36f18ab23..f57573d99 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -425,14 +425,9 @@ Ui::BoostCounters ParseBoostCounters( } Ui::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) { - const auto group = channel->isMegagroup(); - const auto appConfig = &channel->session().account().appConfig(); - const auto get = [&](const QString &key, int fallback, bool ok = true) { - return ok ? appConfig->get<int>(key, fallback) : 0; - }; - auto nameColorsByLevel = base::flat_map<int, int>(); auto linkStylesByLevel = base::flat_map<int, int>(); + const auto group = channel->isMegagroup(); const auto peerColors = &channel->session().api().peerColors(); const auto &list = group ? peerColors->requiredLevelsGroup() diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index df2f5f880..75b27a25b 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -262,7 +262,6 @@ void ShortcutMessages::scheduleShortcutsReload() { } void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) { - const auto selfId = _session->userPeerId(); const auto &reply = update.vquick_reply(); auto foundId = BusinessShortcutId(); const auto shortcut = parseShortcut(reply); diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 1a3e35716..1e6654abd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -244,10 +244,8 @@ void Contact::draw(Painter &p, const PaintContext &context) const { } const auto st = context.st; - const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); - const auto bubble = st::msgPadding; const auto full = Rect(currentSize()); const auto outer = full - inBubblePadding(); const auto inner = outer - innerMargin(); @@ -438,7 +436,6 @@ TextState Contact::textState(QPoint point, StateRequest request) const { if (_buttons.size() > 1) { const auto end = rect::bottom(inner) + _st.padding.bottom(); - const auto line = st::historyPageButtonLine; const auto bWidth = inner.width() / float64(_buttons.size()); const auto bHeight = rect::bottom(outer) - end; for (auto i = 0; i < _buttons.size(); i++) { diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 440c125b2..b2908c3ac 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -167,12 +167,7 @@ Ui::RpWidget *ContentWidget::doSetInnerWidget( const auto bottom = top + height; _innerDesiredHeight = desired; _innerWrap->setVisibleTopBottom(top, bottom); - LOG(("TOP: %1, HEIGHT: %2, DESIRED: %3, TILL: %4").arg(top).arg(height).arg(desired).arg(std::max(desired - bottom, 0))); _scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0)); - //const auto bottom = _scroll->scrollTop() + _scroll->height(); - //_innerDesiredHeight = desired; - //_innerWrap->setVisibleTopBottom(_scroll->scrollTop(), bottom); - //_scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0)); }, _innerWrap->lifetime()); return _innerWrap->entity(); diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index 50eca4456..3404cf932 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -138,7 +138,6 @@ void LayerWidget::setContentHeight(int height) { if (_contentWrapHeight == height) { return; } - LOG(("CONTENT WRAP HEIGHT: %1 -> %2").arg(_contentWrapHeight).arg(height)); _contentWrapHeight = height; if (_inResize) { _pendingResize = true; @@ -261,35 +260,6 @@ int LayerWidget::resizeGetHeight(int newWidth) { auto attempts = 0; while (true) { _inResize = true; - { - const auto &parentSize = parentWidget()->size(); - const auto windowWidth = parentSize.width(); - const auto windowHeight = parentSize.height(); - const auto newLeft = (windowWidth - newWidth) / 2; - const auto newTop = std::clamp( - windowHeight / 24, - st::infoLayerTopMinimal, - st::infoLayerTopMaximal); - const auto newBottom = newTop; - - const auto bottomRadius = st::boxRadius; - const auto maxVisibleHeight = windowHeight - newTop; - // Top rounding is included in _contentWrapHeight. - auto desiredHeight = _contentWrapHeight + bottomRadius; - accumulate_min(desiredHeight, maxVisibleHeight - newBottom); - - // First resize content to new width and get the new desired height. - const auto contentLeft = 0; - const auto contentTop = 0; - const auto contentBottom = bottomRadius; - const auto contentWidth = newWidth; - auto contentHeight = desiredHeight - contentTop - contentBottom; - LOG(("ATTEMPT %1: WIDTH %2, WRAP HEIGHT %3, SCROLL TILL BOTTOM: %4" - ).arg(attempts + 1 - ).arg(newWidth - ).arg(_contentWrapHeight - ).arg(_contentWrap->scrollTillBottom(contentHeight))); - } const auto newGeometry = countGeometry(newWidth); _inResize = false; if (!_pendingResize) { diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 78d446200..656d49958 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -479,7 +479,6 @@ void Chatbots::setupContent( void Chatbots::save() { const auto show = controller()->uiShow(); - const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { show->showToast(tr::lng_greeting_recipients_empty(tr::now)); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index 97b0c9a61..d451feb1b 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -130,7 +130,6 @@ void QuickReplies::setupContent( auto old = inner->count(); const auto &shortcuts = messages->shortcuts(); - auto i = 0; for (const auto &[_, shortcut] : shortcuts.list | ranges::views::reverse) { if (!shortcut.count) { diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index 7f7f2b715..160b1d83b 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -171,7 +171,7 @@ void AddBusinessRecipientsSelector( const auto all = current.allButExcluded || current.included.empty(); const auto group = std::make_shared<Ui::RadiobuttonGroup>( all ? kAllExcept : kSelectedOnly); - const auto everyone = container->add( + container->add( object_ptr<Ui::Radiobutton>( container, group, @@ -179,7 +179,7 @@ void AddBusinessRecipientsSelector( tr::lng_chatbots_all_except(tr::now), st::settingsChatbotsAccess), st::settingsChatbotsAccessMargins); - const auto selected = container->add( + container->add( object_ptr<Ui::Radiobutton>( container, group, diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 3b1143126..e1fcec944 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -181,7 +181,6 @@ private: void processScroll(); void updateInnerVisibleArea(); - void pushReplyReturn(not_null<HistoryItem*> item); void checkReplyReturns(); void confirmDeleteSelected(); void clearSelected(); @@ -565,7 +564,6 @@ bool ShortcutMessages::paintOuter( not_null<QWidget*> outer, int maxVisibleHeight, QRect clip) { - const auto window = outer->window()->height(); Window::SectionWidget::PaintBackground( _theme.get(), outer, @@ -1092,12 +1090,6 @@ bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { return (type == CornerButtonType::Down); } -void ShortcutMessages::pushReplyReturn(not_null<HistoryItem*> item) { - if (item->shortcutId() == _shortcutId.current()) { - _cornerButtons.pushReplyReturn(item); - } -} - void ShortcutMessages::checkReplyReturns() { const auto currentTop = _scroll->scrollTop(); const auto shortcutId = _shortcutId.current(); diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 525d069a4..fe80e5c73 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -60,7 +60,6 @@ private: const auto abs = std::abs(data.utcOffset); const auto hours = abs / 3600; const auto minutes = (abs % 3600) / 60; - const auto seconds = abs % 60; const auto sign = (data.utcOffset < 0) ? '-' : '+'; const auto prefix = u"(UTC"_q + sign @@ -366,7 +365,6 @@ void EditDayBox( const auto from = std::max( std::min(last + 30 * 60, kDay - 30 * 60), last + 60); - const auto till = std::min(from + 4 * 3600, kDay + 30 * 60); now.list.push_back({ from, from + 4 * 3600 }); } state->data = std::move(now); From d3b1abb61ed14f288de2491f3504fae4b973d873 Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:13:44 -0800 Subject: [PATCH 094/108] fix windows build --- Telegram/build/prepare/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index c3a566796..cf604e292 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -205,7 +205,7 @@ def removeDir(folder): def setVar(key, multilineValue): singlelineValue = ' '.join(multilineValue.replace('\n', '').split()); if win: - return 'SET ' + key + '="' + singlelineValue + '"'; + return 'SET "' + key + '=' + singlelineValue + '"'; return key + '="' + singlelineValue + '"'; def filterByPlatform(commands): From 506b8fd4f13af996b7be9b97bb6da88e534d6654 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Sat, 9 Mar 2024 10:56:58 +0400 Subject: [PATCH 095/108] Fix saving of empty working intervals. --- Telegram/SourceFiles/data/business/data_business_common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 151d534e1..af3429421 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -139,7 +139,7 @@ struct WorkingHours { } explicit operator bool() const { - return !timezoneId.isEmpty(); + return !timezoneId.isEmpty() && !intervals.list.empty(); } friend inline bool operator==( From 626b3395ab48c279001fc9688d83563f4289c9fc Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Sat, 9 Mar 2024 10:57:17 +0400 Subject: [PATCH 096/108] Show 00:00-23:59 as "open 24 hours". --- Telegram/SourceFiles/data/business/data_business_common.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 7da2970db..06cb17e91 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -231,8 +231,9 @@ WorkingIntervals ExtractDayIntervals( } bool IsFullOpen(const WorkingIntervals &extractedDay) { - return extractedDay - && (extractedDay.list.front() == WorkingInterval{ 0, kDay }); + return extractedDay // 00:00-23:59 or 00:00-00:00 (next day) + && (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 } + || extractedDay.list.front() == WorkingInterval{ 0, kDay }); } WorkingIntervals RemoveDayIntervals( From 77dcbaf00cad934eaea53357d290f1485d97a1a1 Mon Sep 17 00:00:00 2001 From: Kolya <142352140+agl-1984@users.noreply.github.com> Date: Sun, 10 Mar 2024 01:51:42 +0000 Subject: [PATCH 097/108] don't use brotli (built by other dependencies) --- Telegram/build/prepare/prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index cf604e292..727e8521b 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1512,6 +1512,7 @@ mac: -system-webp \ -I "$USED_PREFIX/include" \ -no-feature-futimens \ + -no-feature-brotli \ -nomake examples \ -nomake tests \ -platform macx-clang -- \ From fc0cfbf003269679963e2babbdf94a63d4ae7cc0 Mon Sep 17 00:00:00 2001 From: prawwtocol <142227259+prawwtocol@users.noreply.github.com> Date: Sun, 10 Mar 2024 11:58:26 +0300 Subject: [PATCH 098/108] Update mac build instructions with up-to-date homebrew link --- docs/building-mac.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building-mac.md b/docs/building-mac.md index 11435a8dd..605a5b519 100644 --- a/docs/building-mac.md +++ b/docs/building-mac.md @@ -12,7 +12,7 @@ You will require **api_id** and **api_hash** to access the Telegram API servers. Go to ***BuildPath*** and run - ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install git automake cmake wget pkg-config gnu-tar ninja nasm meson sudo xcode-select -s /Applications/Xcode.app/Contents/Developer From 12356b16177c002d2a52de443b3055cb366de2a8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 13:00:06 +0400 Subject: [PATCH 099/108] Fix possible crash in WebView2 destruction. --- Telegram/lib_webview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 27af88195..fbf9dd547 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 27af88195bca687e9d2a52b4fcd4e83ef5476be9 +Subproject commit fbf9dd54787df90c98cf230cb53323527e0b0639 From 68bb0a17447d1dad6b70948970cb118381f9a5af Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 13:12:49 +0400 Subject: [PATCH 100/108] Fix recent actions date marks. Regression was introduced in 7f3ebde252. --- .../SourceFiles/history/admin_log/history_admin_log_item.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index c54093dc3..14f9718fc 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -829,6 +829,7 @@ void GenerateItems( .id = history->nextNonHistoryEntryId(), .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, .from = from->id, + .date = date, }, std::move(text), MTP_messageMediaEmpty()); }; From b4993453c01381aac3dd9da41d7bf1df0abab2cb Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 13:16:50 +0400 Subject: [PATCH 101/108] Update submodules. --- Telegram/lib_base | 2 +- Telegram/lib_spellcheck | 2 +- cmake | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/lib_base b/Telegram/lib_base index cee9211bd..5b9556fdd 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit cee9211bd58e054f24ad5e7f122037f71a44b237 +Subproject commit 5b9556fddb9a67e514d0bed2c123e18cbe1663b7 diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index 96543c171..9b52030bf 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit 96543c1716d3790ef12bdec6b113958427710441 +Subproject commit 9b52030bfcd7e90e3e550231a3783ad1982fda78 diff --git a/cmake b/cmake index f1628c260..5a61112d6 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit f1628c260b17c55996740fc00b00a7c227b61de2 +Subproject commit 5a61112d6d025b56573ad48bcc1331ac65c4a927 From 1647991f6a55c8a3ff834ef3f6a621e0ec56ab4b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Sat, 9 Mar 2024 11:06:43 +0400 Subject: [PATCH 102/108] Fix autologin token account selection. --- Telegram/SourceFiles/core/ui_integration.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index ba2593e7b..e6b62203d 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -50,8 +50,13 @@ const auto kBadPrefix = u"http://"_q; [[nodiscard]] QString UrlWithAutoLoginToken( const QString &url, QUrl parsed, - const QString &domain) { - const auto &active = Core::App().activeAccount(); + const QString &domain, + QVariant context) { + const auto my = context.value<ClickHandlerContext>(); + const auto window = my.sessionWindow.get(); + const auto &active = window + ? window->session().account() + : Core::App().activeAccount(); const auto token = active.mtp().configValues().autologinToken; const auto domains = active.appConfig().get<std::vector<QString>>( "autologin_domains", @@ -238,7 +243,8 @@ bool UiIntegration::handleUrlClick( const auto domain = DomainForAutoLogin(parsed); const auto skip = context.value<ClickHandlerContext>().skipBotAutoLogin; if (skip || !BotAutoLogin(url, domain, context)) { - File::OpenUrl(UrlWithAutoLoginToken(url, std::move(parsed), domain)); + File::OpenUrl( + UrlWithAutoLoginToken(url, std::move(parsed), domain, context)); } return true; } From cf6d13acc2e797700cf79170b5a9d2b34d69d7ab Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 17:08:27 +0400 Subject: [PATCH 103/108] Add fast Ctrl/Shift scroll to ElasticScroll. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index edfcac751..fb1716c91 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit edfcac751dbdd93fcabac4ebf0ff731ceab8af0f +Subproject commit fb1716c91f80baf482a51b86a9a92cb1df2819b0 From c6f49486ee7d983cab5096b1ef16b6469f53e114 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 18:31:48 +0400 Subject: [PATCH 104/108] Use regular good-green color in unmute. --- Telegram/SourceFiles/menu/menu_mute.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/menu/menu_mute.cpp b/Telegram/SourceFiles/menu/menu_mute.cpp index dd58f6fc0..661ef0672 100644 --- a/Telegram/SourceFiles/menu/menu_mute.cpp +++ b/Telegram/SourceFiles/menu/menu_mute.cpp @@ -111,6 +111,7 @@ MuteItem::MuteItem( isMuted ? 1. : 0., st::defaultPopupMenu.showDuration); }, lifetime()); + _animation.stop(); setClickedCallback([=] { descriptor.updateMutePeriod(_isMuted ? 0 : kMuteForeverValue); @@ -123,7 +124,7 @@ void MuteItem::paintEvent(QPaintEvent *e) { const auto progress = _animation.value(_isMuted ? 1. : 0.); const auto color = anim::color( st::menuIconAttentionColor, - st::settingsIconBg2, + st::boxTextFgGood, progress); p.setPen(color); From 8c5db25476b08f5b1e2562ac8eb99895880ee801 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 23:19:59 +0400 Subject: [PATCH 105/108] Fix a crash in main settings destructor. Fixes #27544. --- .../info/profile/info_profile_emoji_status_panel.cpp | 4 ++++ .../info/profile/info_profile_emoji_status_panel.h | 1 + Telegram/SourceFiles/settings/settings_main.cpp | 11 ++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index c7f26b4ef..c4cce455f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -159,6 +159,10 @@ void EmojiStatusPanel::show(Descriptor &&descriptor) { _panel->toggleAnimated(); } +bool EmojiStatusPanel::hasFocus() const { + return _panel && Ui::InFocusChain(_panel.get()); +} + void EmojiStatusPanel::repaint() { _panel->selector()->update(); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h index a373c904c..9777cfcfa 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h @@ -46,6 +46,7 @@ public: not_null<Window::SessionController*> controller, not_null<QWidget*> button, Data::CustomEmojiSizeTag animationSizeTag = {}); + [[nodiscard]] bool hasFocus() const; struct Descriptor { not_null<Window::SessionController*> controller; diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index c746a881a..33b175520 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -174,7 +174,16 @@ Cover::Cover( }, _name->lifetime()); } -Cover::~Cover() = default; +Cover::~Cover() { + if (_emojiStatusPanel.hasFocus()) { + // Panel will try to return focus to the layer widget, the problem is + // we are destroying the layer widget probably right now and focusing + // it will lead to a crash, because it destroys its children (how we + // got here) after it clears focus out of itself. So if you return + // the focus inside a child destructor, it won't be cleared at all. + window()->setFocus(); + } +} void Cover::setupChildGeometry() { using namespace rpl::mappers; From 5573bbc77609bac20ade4f57ac86a66186f822b8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 12 Mar 2024 23:30:49 +0400 Subject: [PATCH 106/108] Version 4.15.2. - Telegram Business: Greeting Message. - Telegram Business: Away Message. - Telegram Business: Quick Replies. - Telegram Business: Working Hours. - Close the ongoing call window without hanging up the call. - Fast scroll through chats list with Ctrl or Shift pressed. - Several bugfixes. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 10 ++++++++++ 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index dee90b06c..b41618532 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="4.15.1.0" /> + Version="4.15.2.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index b8aba7a6f..035ae8fde 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,1,0 - PRODUCTVERSION 4,15,1,0 + FILEVERSION 4,15,2,0 + PRODUCTVERSION 4,15,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "4.15.1.0" + VALUE "FileVersion", "4.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.15.1.0" + VALUE "ProductVersion", "4.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 93f0a6769..97cf094de 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,1,0 - PRODUCTVERSION 4,15,1,0 + FILEVERSION 4,15,2,0 + PRODUCTVERSION 4,15,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "4.15.1.0" + VALUE "FileVersion", "4.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.15.1.0" + VALUE "ProductVersion", "4.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 9bd491610..34fc3f789 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 4015001; -constexpr auto AppVersionStr = "4.15.1"; +constexpr auto AppVersion = 4015002; +constexpr auto AppVersionStr = "4.15.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/build/version b/Telegram/build/version index 60c27dece..e540466fc 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4015001 +AppVersion 4015002 AppVersionStrMajor 4.15 -AppVersionStrSmall 4.15.1 -AppVersionStr 4.15.1 +AppVersionStrSmall 4.15.2 +AppVersionStr 4.15.2 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.15.1 +AppVersionOriginal 4.15.2 diff --git a/changelog.txt b/changelog.txt index 1d80bf48b..f1b710187 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +4.15.2 (12.03.24) + +- Telegram Business: Greeting Message. +- Telegram Business: Away Message. +- Telegram Business: Quick Replies. +- Telegram Business: Working Hours. +- Close the ongoing call window without hanging up the call. +- Fast scroll through chats list with Ctrl or Shift pressed. +- Several bugfixes. + 4.15.1 (08.03.24) - Telegram Business features. From f13971dce177c5049b95e0977a74509e280f8754 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 13 Mar 2024 09:20:12 +0400 Subject: [PATCH 107/108] Use line-tables-only debug information format on macOS. Otherwise linking fails on x86_64 in Release mode. --- Telegram/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 9285d5ad2..723c837a2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1754,6 +1754,7 @@ set_target_properties(Telegram PROPERTIES XCODE_ATTRIBUTE_ALWAYS_SEARCH_USER_PATHS NO XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY libc++ XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS --deep + XCODE_ATTRIBUTE_CLANG_DEBUG_INFORMATION_LEVEL $<IF:$<CONFIG:Debug>,default,line-tables-only> ) set(entitlement_sources "${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram.entitlements" From bf1b3dc8f6a8b638e4735b07c08bcef30ad13819 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 13 Mar 2024 09:21:35 +0400 Subject: [PATCH 108/108] Version 4.15.2: Update fcitx-qt5. I hope this fixes #27573. --- Telegram/ThirdParty/fcitx5-qt | 2 +- Telegram/lib_ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/ThirdParty/fcitx5-qt b/Telegram/ThirdParty/fcitx5-qt index 413747e76..cc77e32c0 160000 --- a/Telegram/ThirdParty/fcitx5-qt +++ b/Telegram/ThirdParty/fcitx5-qt @@ -1 +1 @@ -Subproject commit 413747e761b13bacc5ebd01e20810c64c2f3b6dc +Subproject commit cc77e32c0ab675a663a7c019b3bb8cfcc60c5ec3 diff --git a/Telegram/lib_ui b/Telegram/lib_ui index fb1716c91..6bce49302 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit fb1716c91f80baf482a51b86a9a92cb1df2819b0 +Subproject commit 6bce493029a08b460db88cbaf528e49450751d1f