diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index d98bfc07d..7586a3241 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -31,8 +31,9 @@ jobs:
CMAKE_VER: "3.16.3"
UPLOAD_ARTIFACT: "false"
ONLY_CACHE: "false"
- MANUAL_CACHING: "2"
+ MANUAL_CACHING: "6"
DOC_PATH: "docs/building-cmake.md"
+ AUTO_CACHING: "1"
steps:
- name: Get repository name.
@@ -43,7 +44,20 @@ jobs:
with:
submodules: recursive
+ - name: Disable man for further package installs.
+ run: |
+ cfgFile="/etc/dpkg/dpkg.cfg.d/no_man"
+ sudo touch $cfgFile
+ p() {
+ sudo echo "path-exclude=/usr/share/$1/*" >> $cfgFile
+ }
+
+ p man
+ p locale
+ p doc
+
- name: First set up.
+ shell: bash
run: |
cd ..
mv $REPO_NAME temp
@@ -55,13 +69,13 @@ jobs:
sudo apt-get install software-properties-common -y && \
sudo apt-get install git libexif-dev liblzma-dev libz-dev libssl-dev \
libgtk2.0-dev libice-dev libsm-dev libicu-dev libdrm-dev dh-autoreconf \
- autoconf automake build-essential libass-dev libfreetype6-dev \
+ autoconf automake build-essential libxml2-dev libass-dev libfreetype6-dev \
libgpac-dev libsdl1.2-dev libtheora-dev libtool libva-dev libvdpau-dev \
libvorbis-dev libenchant-dev libxcb1-dev libxcb-image0-dev libxcb-shm0-dev \
libxcb-xfixes0-dev libxcb-keysyms1-dev libxcb-icccm4-dev libatspi2.0-dev \
libxcb-render-util0-dev libxcb-util0-dev libxcb-xkb-dev libxrender-dev \
- libasound-dev libpulse-dev libxcb-sync0-dev libxcb-randr0-dev bison \
- libx11-xcb-dev libffi-dev libncurses5-dev pkg-config texi2html yasm \
+ libasound-dev libpulse-dev libxcb-sync0-dev libxcb-randr0-dev libegl1-mesa-dev \
+ libx11-xcb-dev libffi-dev libncurses5-dev pkg-config texi2html bison yasm \
zlib1g-dev xutils-dev python-xcbgen chrpath gperf wget -y --force-yes && \
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y && \
sudo apt-get update && \
@@ -75,6 +89,10 @@ jobs:
gcc --version > CACHE_KEY.txt
echo $MANUAL_CACHING >> CACHE_KEY.txt
+ if [ "$AUTO_CACHING" == "1" ]; then
+ thisFile=$REPO_NAME/.github/workflows/linux.yml
+ echo `md5sum $thisFile | cut -c -32` >> CACHE_KEY.txt
+ fi
md5cache=$(md5sum CACHE_KEY.txt | cut -c -32)
echo ::set-env name=CACHE_KEY::$md5cache
@@ -111,16 +129,6 @@ jobs:
cmake --version
- - name: Zlib.
- run: |
- cd $LibrariesPath
-
- git clone $GIT/madler/zlib.git
- cd zlib
- ./configure
- make -j$(nproc)
- sudo make install
-
- name: Opus cache.
id: cache-opus
uses: actions/cache@v1
@@ -132,9 +140,8 @@ jobs:
run: |
cd $LibrariesPath
- git clone $GIT/xiph/opus
+ git clone -b v1.3 --depth=1 $GIT/xiph/opus
cd opus
- git checkout v1.3
./autogen.sh
./configure
make -j$(nproc)
@@ -152,17 +159,20 @@ jobs:
./autogen.sh --enable-static
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf libva
- name: Libvdpau.
run: |
cd $LibrariesPath
- git clone git://anongit.freedesktop.org/vdpau/libvdpau
+ git clone -b libvdpau-1.2 --depth=1 https://gitlab.freedesktop.org/vdpau/libvdpau.git
cd libvdpau
- git checkout libvdpau-1.2
./autogen.sh --enable-static
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf libvdpau
- name: FFmpeg cache.
id: cache-ffmpeg
@@ -278,6 +288,8 @@ jobs:
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf ffmpeg
- name: FFmpeg install.
run: |
cd $LibrariesPath
@@ -304,18 +316,20 @@ jobs:
./configure
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf portaudio
- name: OpenAL Soft.
run: |
cd $LibrariesPath
- git clone $GIT/kcat/openal-soft.git
- cd openal-soft
- git checkout openal-soft-1.19.1
- cd build
+ git clone -b openal-soft-1.19.1 --depth=1 $GIT/kcat/openal-soft.git
+ cd openal-soft/build
cmake -D LIBTYPE:STRING=STATIC ..
make -j$(nproc)
sudo make install
+ cd -
+ rm -rf openal-soft
- name: OpenSSL cache.
id: cache-openssl
@@ -328,12 +342,14 @@ jobs:
run: |
cd $LibrariesPath
- git clone $GIT/openssl/openssl openssl_$OPENSSL_VER
- cd openssl_$OPENSSL_VER
- git checkout OpenSSL_1_1_1-stable
+ git clone -b OpenSSL_${OPENSSL_VER}-stable --depth=1 \
+ $GIT/openssl/openssl openssl_${OPENSSL_VER}
+ cd openssl_${OPENSSL_VER}
./config --prefix=$LibrariesPath/openssl-cache
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf openssl_${OPENSSL_VER}
- name: OpenSSL install.
run: |
cd $LibrariesPath
@@ -344,12 +360,25 @@ jobs:
run: |
cd $LibrariesPath
- git clone $GIT/xkbcommon/libxkbcommon.git
+ git clone -b xkbcommon-0.8.4 --depth=1 $GIT/xkbcommon/libxkbcommon.git
cd libxkbcommon
- git checkout xkbcommon-0.8.4
./autogen.sh
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf libxkbcommon
+
+ - name: Libwayland.
+ run: |
+ cd $LibrariesPath
+
+ git clone -b 1.16 https://gitlab.freedesktop.org/wayland/wayland
+ cd wayland
+ ./autogen.sh --enable-static --disable-documentation
+ make -j$(nproc)
+ sudo make install
+ cd ..
+ rm -rf wayland
- name: Qt 5.12.5 cache.
id: cache-qt
@@ -362,14 +391,12 @@ jobs:
run: |
cd $LibrariesPath
- git clone git://code.qt.io/qt/qt5.git qt_$QT
- cd qt_$QT
- perl init-repository --module-subset=qtbase,qtimageformats
- git checkout v5.12.5
- git submodule update qtbase
- git submodule update qtimageformats
+ git clone -b v5.12.5 --depth=1 git://code.qt.io/qt/qt5.git qt_${QT}
+ cd qt_${QT}
+ perl init-repository --module-subset=qtbase,qtwayland,qtimageformats,qtsvg
+ git submodule update qtbase qtwayland qtimageformats qtsvg
cd qtbase
- git apply ../../patches/qtbase_$QT.diff
+ git apply ../../patches/qtbase_${QT}.diff
cd src/plugins/platforminputcontexts
git clone $GIT/desktop-app/fcitx.git
git clone $GIT/desktop-app/hime.git
@@ -389,10 +416,9 @@ jobs:
-qt-xcb \
-system-freetype \
-fontconfig \
- -no-opengl \
- -no-glib \
-no-gtk \
-static \
+ -dbus-runtime \
-openssl-linked \
-I "$OPENSSL_PREFIX/include" OPENSSL_LIBS="$OPENSSL_PREFIX/lib/libssl.a $OPENSSL_PREFIX/lib/libcrypto.a -ldl -lpthread" \
-nomake examples \
@@ -400,6 +426,8 @@ jobs:
make -j$(nproc)
sudo make install
+ cd ..
+ rm -rf qt_${QT}
- name: Qt 5.12.5 install.
run: |
cd $LibrariesPath
@@ -450,6 +478,8 @@ jobs:
make -j$(nproc) dump_syms
mv dump_syms $BreakpadCache/
+ cd ..
+ rm -rf gyp breakpad
- name: Breakpad install.
run: |
cd $LibrariesPath
diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml
index 6ca226e38..53beba810 100644
--- a/.github/workflows/mac.yml
+++ b/.github/workflows/mac.yml
@@ -35,6 +35,8 @@ jobs:
ONLY_CACHE: "false"
MANUAL_CACHING: "2"
DOC_PATH: "docs/building-xcode.md"
+ AUTO_CACHING: "1"
+
steps:
- name: Get repository name.
run: echo ::set-env name=REPO_NAME::${GITHUB_REPOSITORY##*/}
@@ -61,6 +63,10 @@ jobs:
echo $MIN_MAC >> CACHE_KEY.txt
echo $PREFIX >> CACHE_KEY.txt
echo $MANUAL_CACHING >> CACHE_KEY.txt
+ if [ "$AUTO_CACHING" == "1" ]; then
+ thisFile=$REPO_NAME/.github/workflows/mac.yml
+ echo `md5 -q $thisFile` >> CACHE_KEY.txt
+ fi
echo ::set-env name=CACHE_KEY::`md5 -q CACHE_KEY.txt`
echo ::add-path::$PWD/Libraries/depot_tools
diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml
index 6949d72ec..ec73a14d8 100644
--- a/.github/workflows/snap.yml
+++ b/.github/workflows/snap.yml
@@ -47,10 +47,6 @@ jobs:
md5cache=$(md5sum CMAKE_CACHE_KEY.txt | cut -c -32)
echo ::set-env name=CMAKE_CACHE_KEY::$md5cache
- awk -v RS="" -v ORS="\n\n" '/^ enchant:/' snap/snapcraft.yaml > ENCHANT_CACHE_KEY.txt
- md5cache=$(md5sum ENCHANT_CACHE_KEY.txt | cut -c -32)
- echo ::set-env name=ENCHANT_CACHE_KEY::$md5cache
-
- name: CMake cache.
id: cache-cmake
uses: actions/cache@v1
@@ -62,17 +58,6 @@ jobs:
if: steps.cache-cmake.outputs.cache-hit != 'true'
run: snapcraft build --destructive-mode cmake
- - name: Enchant cache.
- id: cache-enchant
- uses: actions/cache@v1
- with:
- path: parts/enchant
- key: ${{ runner.OS }}-enchant-${{ env.CACHE_KEY }}-${{ env.ENCHANT_CACHE_KEY }}
-
- - name: Enchant build.
- if: steps.cache-enchant.outputs.cache-hit != 'true'
- run: snapcraft build --destructive-mode enchant
-
- name: Kotatogram Desktop snap build.
if: env.ONLY_CACHE == 'false'
run: snapcraft --destructive-mode
@@ -95,5 +80,5 @@ jobs:
- name: Remove unneeded directories for cache.
run: |
- rm -rf parts/{cmake,enchant}/{build,src,ubuntu}
- rm -rf parts/{cmake,enchant}/state/{stage,prime}
+ rm -rf parts/cmake/{build,src,ubuntu}
+ rm -rf parts/cmake/state/{stage,prime}
diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml
index 28d457ba0..1cb303ffa 100644
--- a/.github/workflows/win.yml
+++ b/.github/workflows/win.yml
@@ -30,6 +30,8 @@ jobs:
ONLY_CACHE: "false"
MANUAL_CACHING: "2"
DOC_PATH: "docs/building-msvc.md"
+ AUTO_CACHING: "1"
+
steps:
- name: Get repository name.
shell: bash
@@ -69,6 +71,10 @@ jobs:
- name: Generate cache key.
shell: bash
run: |
+ if [ "$AUTO_CACHING" == "1" ]; then
+ thisFile=$REPO_NAME/.github/workflows/win.yml
+ echo `md5sum $thisFile | awk '{ print $1 }'` >> CACHE_KEY.txt
+ fi
echo ::set-env name=CACHE_KEY::`md5sum CACHE_KEY.txt | awk '{ print $1 }'`
- name: Choco installs.
diff --git a/.gitmodules b/.gitmodules
index c5f0782d7..1ef8dddfd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -64,3 +64,6 @@
[submodule "Telegram/ThirdParty/libdbusmenu-qt"]
path = Telegram/ThirdParty/libdbusmenu-qt
url = https://github.com/desktop-app/libdbusmenu-qt.git
+[submodule "Telegram/ThirdParty/hunspell"]
+ path = Telegram/ThirdParty/hunspell
+ url = https://github.com/hunspell/hunspell
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index c08709ddf..4ae453987 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -66,18 +66,7 @@ generate_numbers(Telegram ${res_loc}/numbers.txt)
set_target_properties(Telegram PROPERTIES AUTOMOC ON AUTORCC ON)
-if (DESKTOP_APP_USE_PACKAGED)
- set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
- find_package(Threads)
-
- target_link_libraries(Telegram
- PRIVATE
- ${CMAKE_DL_LIBS}
- Threads::Threads
- )
-endif()
-
-if (LINUX AND NOT TDESKTOP_DISABLE_DBUS_INTEGRATION)
+if (LINUX AND NOT DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
target_link_libraries(Telegram
PRIVATE
desktop-app::external_statusnotifieritem
@@ -85,6 +74,10 @@ if (LINUX AND NOT TDESKTOP_DISABLE_DBUS_INTEGRATION)
)
endif()
+if (add_hunspell_library)
+ target_link_libraries(Telegram PRIVATE desktop-app::external_hunspell)
+endif()
+
target_link_libraries(Telegram
PRIVATE
tdesktop::lib_mtproto
@@ -124,6 +117,17 @@ if (NOT HAVE_LONG_ATOMIC_WITHOUT_LIB)
target_link_libraries(Telegram PRIVATE atomic)
endif()
+if (DESKTOP_APP_USE_PACKAGED)
+ set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
+ find_package(Threads)
+
+ target_link_libraries(Telegram
+ PRIVATE
+ ${CMAKE_DL_LIBS}
+ Threads::Threads
+ )
+endif()
+
target_precompile_headers(Telegram PRIVATE ${src_loc}/stdafx.h)
nice_target_sources(Telegram ${src_loc}
PRIVATE
@@ -185,6 +189,8 @@ PRIVATE
boxes/connection_box.h
boxes/create_poll_box.cpp
boxes/create_poll_box.h
+ boxes/dictionaries_manager.cpp
+ boxes/dictionaries_manager.h
boxes/download_path_box.cpp
boxes/download_path_box.h
boxes/edit_caption_box.cpp
@@ -261,6 +267,8 @@ PRIVATE
chat_helpers/gifs_list_widget.h
chat_helpers/message_field.cpp
chat_helpers/message_field.h
+ chat_helpers/spellchecker_common.cpp
+ chat_helpers/spellchecker_common.h
chat_helpers/stickers.cpp
chat_helpers/stickers.h
chat_helpers/stickers_emoji_pack.cpp
@@ -341,6 +349,8 @@ PRIVATE
data/data_game.h
data/data_groups.cpp
data/data_groups.h
+ data/data_histories.cpp
+ data/data_histories.h
data/data_location.cpp
data/data_location.h
data/data_media_rotation.cpp
@@ -854,6 +864,8 @@ PRIVATE
storage/serialize_common.h
storage/serialize_document.cpp
storage/serialize_document.h
+ storage/storage_cloud_blob.cpp
+ storage/storage_cloud_blob.h
storage/storage_facade.cpp
storage/storage_facade.h
# storage/storage_feed_messages.cpp
@@ -1084,8 +1096,8 @@ endif()
if (build_macstore)
set(bundle_identifier "io.github.kotatogram")
- set(bundle_entitlements "Telegram Desktop.entitlements")
- set(output_name "Kotatogram Desktop")
+ set(bundle_entitlements "Telegram Lite.entitlements")
+ set(output_name "Kotatogram Lite")
set_target_properties(Telegram PROPERTIES
XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${libs_loc}/breakpad/src/client/mac/build/Release
)
@@ -1129,7 +1141,7 @@ set_target_properties(Telegram PROPERTIES
)
set(entitlement_sources
"${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram.entitlements"
- "${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram Desktop.entitlements"
+ "${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram Lite.entitlements"
)
target_sources(Telegram PRIVATE ${entitlement_sources})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/Telegram PREFIX Resources FILES ${entitlement_sources})
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index dd036799f..8b58fe5c7 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -422,6 +422,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_spellchecker" = "Spell checker";
"lng_settings_system_spellchecker" = "Use system spell checker";
+"lng_settings_custom_spellchecker" = "Use spell checker";
+"lng_settings_auto_download_dictionaries" = "Automatic dictionaries download";
+"lng_settings_manage_dictionaries" = "Manage dictionaries";
+"lng_settings_manage_enabled_dictionary" = "Dictionary is enabled";
+"lng_settings_manage_remove_dictionary" = "Remove Dictionary";
"lng_backgrounds_header" = "Choose your new chat background";
"lng_theme_sure_keep" = "Keep this theme?";
@@ -1369,7 +1374,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_user_action_upload_file" = "{user} is sending a file";
"lng_unread_bar#one" = "{count} unread message";
"lng_unread_bar#other" = "{count} unread messages";
-//"lng_unread_bar_some" = "Unread messages";
+"lng_unread_bar_some" = "Unread messages";
"lng_maps_point" = "Location";
"lng_save_photo" = "Save image";
@@ -1581,6 +1586,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_video_loading" = "Loading - {percent}";
"lng_mediaview_playback_speed" = "Playback speed";
"lng_mediaview_playback_speed_normal" = "Normal";
+"lng_mediaview_rotate_video" = "Rotate video";
"lng_theme_preview_title" = "Theme Preview";
"lng_theme_preview_generating" = "Generating color theme preview...";
@@ -1610,6 +1616,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_formatting_link_url" = "URL";
"lng_formatting_link_create" = "Create";
+"lng_spellchecker_submenu" = "Spelling";
"lng_spellchecker_add" = "Add to Dictionary";
"lng_spellchecker_remove" = "Remove from Dictionary";
"lng_spellchecker_ignore" = "Ignore word";
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 1cc443d70..474eae12b 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -9,7 +9,7 @@
+ Version="1.9.19.0" />
Telegram Desktop
Telegram FZ-LLC
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 45476bfd5..082826664 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h" // UserData::name
#include "data/data_session.h"
#include "data/data_file_origin.h"
+#include "data/data_histories.h"
#include "history/history.h"
#include "history/history_message.h" // NewMessageFlags.
#include "chat_helpers/message_field.h" // ConvertTextTagsToEntities.
@@ -110,23 +111,30 @@ void SendExistingMedia(
auto failHandler = std::make_shared>();
auto performRequest = [=] {
- const auto usedFileReference = media->fileReference();
- history->sendRequestId = api->request(MTPmessages_SendMedia(
- MTP_flags(sendFlags),
- peer->input,
- MTP_int(replyTo),
- inputMedia(),
- MTP_string(captionText),
- MTP_long(randomId),
- MTPReplyMarkup(),
- sentEntities,
- MTP_int(message.action.options.scheduled)
- )).done([=](const MTPUpdates &result) {
- api->applyUpdates(result, randomId);
- }).fail([=](const RPCError &error) {
- (*failHandler)(error, usedFileReference);
- }).afterRequest(history->sendRequestId
- ).send();
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ const auto usedFileReference = media->fileReference();
+ history->sendRequestId = api->request(MTPmessages_SendMedia(
+ MTP_flags(sendFlags),
+ peer->input,
+ MTP_int(replyTo),
+ inputMedia(),
+ MTP_string(captionText),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(message.action.options.scheduled)
+ )).done([=](const MTPUpdates &result) {
+ api->applyUpdates(result, randomId);
+ finish();
+ }).fail([=](const RPCError &error) {
+ (*failHandler)(error, usedFileReference);
+ finish();
+ }).afterRequest(history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
};
*failHandler = [=](const RPCError &error, QByteArray usedFileReference) {
if (error.code() == 400
@@ -169,7 +177,7 @@ void SendExistingDocument(
if (document->sticker()) {
if (const auto main = App::main()) {
main->incrementSticker(document);
- document->session().data().notifyRecentStickersUpdated();
+ document->owner().notifyRecentStickersUpdated();
}
}
}
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 121dccca2..a0feaa15b 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_cloud_themes.h"
+#include "data/data_histories.h"
#include "dialogs/dialogs_key.h"
#include "core/core_cloud_password.h"
#include "core/application.h"
@@ -343,7 +344,7 @@ void ApiWrap::proxyPromotionDone(const MTPhelp_ProxyData &proxy) {
const auto peer = _session->data().peer(peerId);
_session->data().setProxyPromoted(peer);
if (const auto history = _session->data().historyLoaded(peer)) {
- requestDialogEntry(history);
+ history->owner().histories().requestDialogEntry(history);
}
});
}
@@ -1011,162 +1012,6 @@ rpl::producer ApiWrap::dialogsLoadBlockedByDate() const {
return _dialogsLoadBlockedByDate.value();
}
-void ApiWrap::requestDialogEntry(not_null folder) {
- if (_dialogFolderRequests.contains(folder)) {
- return;
- }
- _dialogFolderRequests.emplace(folder);
-
- auto peers = QVector(
- 1,
- MTP_inputDialogPeerFolder(MTP_int(folder->id())));
- request(MTPmessages_GetPeerDialogs(
- MTP_vector(std::move(peers))
- )).done([=](const MTPmessages_PeerDialogs &result) {
- applyPeerDialogs(result);
- _dialogFolderRequests.remove(folder);
- }).fail([=](const RPCError &error) {
- _dialogFolderRequests.remove(folder);
- }).send();
-}
-
-void ApiWrap::requestDialogEntry(
- not_null history,
- Fn callback) {
- const auto i = _dialogRequests.find(history);
- if (i != end(_dialogRequests)) {
- if (callback) {
- i->second.push_back(std::move(callback));
- }
- return;
- }
-
- const auto [j, ok] = _dialogRequestsPending.try_emplace(history);
- if (callback) {
- j->second.push_back(std::move(callback));
- }
- if (!ok) {
- return;
- }
- if (_dialogRequestsPending.size() > 1) {
- return;
- }
- Core::App().postponeCall(crl::guard(_session, [=] {
- sendDialogRequests();
- }));
-}
-
-void ApiWrap::sendDialogRequests() {
- if (_dialogRequestsPending.empty()) {
- return;
- }
- auto histories = std::vector>();
- ranges::transform(
- _dialogRequestsPending,
- ranges::back_inserter(histories),
- [](const auto &pair) { return pair.first; });
- auto peers = QVector();
- const auto dialogPeer = [](not_null history) {
- return MTP_inputDialogPeer(history->peer->input);
- };
- ranges::transform(
- histories,
- ranges::back_inserter(peers),
- dialogPeer);
- for (auto &[history, callbacks] : base::take(_dialogRequestsPending)) {
- _dialogRequests.emplace(history, std::move(callbacks));
- }
-
- const auto finalize = [=] {
- for (const auto history : histories) {
- dialogEntryApplied(history);
- history->updateChatListExistence();
- }
- };
- request(MTPmessages_GetPeerDialogs(
- MTP_vector(std::move(peers))
- )).done([=](const MTPmessages_PeerDialogs &result) {
- applyPeerDialogs(result);
- finalize();
- }).fail([=](const RPCError &error) {
- finalize();
- }).send();
-}
-
-void ApiWrap::dialogEntryApplied(not_null history) {
- history->dialogEntryApplied();
- if (const auto callbacks = _dialogRequestsPending.take(history)) {
- for (const auto &callback : *callbacks) {
- callback();
- }
- }
- if (const auto callbacks = _dialogRequests.take(history)) {
- for (const auto &callback : *callbacks) {
- callback();
- }
- }
-}
-
-void ApiWrap::applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs) {
- Expects(dialogs.type() == mtpc_messages_peerDialogs);
-
- const auto &data = dialogs.c_messages_peerDialogs();
- _session->data().processUsers(data.vusers());
- _session->data().processChats(data.vchats());
- _session->data().processMessages(data.vmessages(), NewMessageType::Last);
- for (const auto &dialog : data.vdialogs().v) {
- dialog.match([&](const MTPDdialog &data) {
- if (const auto peerId = peerFromMTP(data.vpeer())) {
- _session->data().history(peerId)->applyDialog(nullptr, data);
- }
- }, [&](const MTPDdialogFolder &data) {
- const auto folder = _session->data().processFolder(data.vfolder());
- folder->applyDialog(data);
- });
- }
- _session->data().sendHistoryChangeNotifications();
-}
-
-void ApiWrap::changeDialogUnreadMark(
- not_null history,
- bool unread) {
- history->setUnreadMark(unread);
-
- using Flag = MTPmessages_MarkDialogUnread::Flag;
- request(MTPmessages_MarkDialogUnread(
- MTP_flags(unread ? Flag::f_unread : Flag(0)),
- MTP_inputDialogPeer(history->peer->input)
- )).send();
-}
-
-void ApiWrap::requestFakeChatListMessage(
- not_null history) {
- if (_fakeChatListRequests.contains(history)) {
- return;
- }
-
- _fakeChatListRequests.emplace(history);
- request(MTPmessages_GetHistory(
- history->peer->input,
- MTP_int(0), // offset_id
- MTP_int(0), // offset_date
- MTP_int(0), // add_offset
- MTP_int(2), // limit
- MTP_int(0), // max_id
- MTP_int(0), // min_id
- MTP_int(0)
- )).done([=](const MTPmessages_Messages &result) {
- _fakeChatListRequests.erase(history);
- history->setFakeChatListMessageFrom(result);
- }).fail([=](const RPCError &error) {
- _fakeChatListRequests.erase(history);
- history->setFakeChatListMessageFrom(MTP_messages_messages(
- MTP_vector(0),
- MTP_vector(0),
- MTP_vector(0)));
- }).send();
-}
-
void ApiWrap::requestWallPaper(
const QString &slug,
Fn done,
@@ -1786,7 +1631,7 @@ void ApiWrap::requestSelfParticipant(not_null channel) {
history->checkLocalMessages();
history->owner().sendHistoryChangeNotifications();
} else {
- requestDialogEntry(history);
+ history->owner().histories().requestDialogEntry(history);
}
}
};
@@ -2422,44 +2267,7 @@ int ApiWrap::OnlineTillFromStatus(
}
void ApiWrap::clearHistory(not_null peer, bool revoke) {
- auto deleteTillId = MsgId(0);
- if (const auto history = _session->data().historyLoaded(peer)) {
- while (history->lastMessageKnown()) {
- const auto last = history->lastMessage();
- if (!last) {
- // History is empty.
- return;
- } else if (!IsServerMsgId(last->id)) {
- // Destroy client-side message locally.
- last->destroy();
- } else {
- break;
- }
- }
- if (!history->lastMessageKnown()) {
- requestDialogEntry(history, [=] {
- Expects(history->lastMessageKnown());
-
- clearHistory(peer, revoke);
- });
- return;
- }
- deleteTillId = history->lastMessage()->id;
- history->clear(History::ClearType::ClearHistory);
- }
- if (const auto channel = peer->asChannel()) {
- if (const auto migrated = peer->migrateFrom()) {
- clearHistory(migrated, revoke);
- }
- if (IsServerMsgId(deleteTillId)) {
- request(MTPchannels_DeleteHistory(
- channel->inputChannel,
- MTP_int(deleteTillId)
- )).send();
- }
- } else {
- deleteHistory(peer, true, revoke);
- }
+ deleteHistory(peer, true, revoke);
}
void ApiWrap::deleteConversation(not_null peer, bool revoke) {
@@ -2473,30 +2281,69 @@ void ApiWrap::deleteConversation(not_null peer, bool revoke) {
}).fail([=](const RPCError &error) {
deleteHistory(peer, false, revoke);
}).send();
- } else if (const auto channel = peer->asChannel()) {
- channel->ptsWaitingForShortPoll(-1);
- leaveChannel(channel);
} else {
deleteHistory(peer, false, revoke);
}
- _session->data().deleteConversationLocally(peer);
}
-void ApiWrap::deleteHistory(not_null peer, bool justClear, bool revoke) {
- using Flag = MTPmessages_DeleteHistory::Flag;
- const auto flags = Flag(0)
- | (justClear ? Flag::f_just_clear : Flag(0))
- | ((peer->isUser() && revoke) ? Flag::f_revoke : Flag(0));
- request(MTPmessages_DeleteHistory(
- MTP_flags(flags),
- peer->input,
- MTP_int(0)
- )).done([=](const MTPmessages_AffectedHistory &result) {
- const auto offset = applyAffectedHistory(peer, result);
- if (offset > 0) {
- deleteHistory(peer, justClear, revoke);
+void ApiWrap::deleteHistory(
+ not_null peer,
+ bool justClear,
+ bool revoke) {
+ auto deleteTillId = MsgId(0);
+ const auto history = _session->data().history(peer);
+ if (justClear) {
+ // In case of clear history we need to know the last server message.
+ while (history->lastMessageKnown()) {
+ const auto last = history->lastMessage();
+ if (!last) {
+ // History is empty.
+ return;
+ } else if (!IsServerMsgId(last->id)) {
+ // Destroy client-side message locally.
+ last->destroy();
+ } else {
+ break;
+ }
}
- }).send();
+ if (!history->lastMessageKnown()) {
+ history->owner().histories().requestDialogEntry(history, [=] {
+ Expects(history->lastMessageKnown());
+
+ deleteHistory(peer, justClear, revoke);
+ });
+ return;
+ }
+ deleteTillId = history->lastMessage()->id;
+ }
+ if (const auto channel = peer->asChannel()) {
+ if (!justClear) {
+ channel->ptsWaitingForShortPoll(-1);
+ leaveChannel(channel);
+ } else {
+ if (const auto migrated = peer->migrateFrom()) {
+ deleteHistory(migrated, justClear, revoke);
+ }
+ if (IsServerMsgId(deleteTillId)) {
+ history->owner().histories().deleteAllMessages(
+ history,
+ deleteTillId,
+ justClear,
+ revoke);
+ }
+ }
+ } else {
+ history->owner().histories().deleteAllMessages(
+ history,
+ deleteTillId,
+ justClear,
+ revoke);
+ }
+ if (!justClear) {
+ _session->data().deleteConversationLocally(peer);
+ } else if (history) {
+ history->clear(History::ClearType::ClearHistory);
+ }
}
int ApiWrap::applyAffectedHistory(
@@ -2528,30 +2375,6 @@ void ApiWrap::applyAffectedMessages(
App::main()->ptsUpdateAndApply(data.vpts().v, data.vpts_count().v);
}
-void ApiWrap::deleteMessages(
- not_null peer,
- const QVector &ids,
- bool revoke) {
- const auto done = [=](const MTPmessages_AffectedMessages & result) {
- applyAffectedMessages(peer, result);
- if (const auto history = peer->owner().historyLoaded(peer)) {
- history->requestChatListMessage();
- }
- };
- if (const auto channel = peer->asChannel()) {
- request(MTPchannels_DeleteMessages(
- channel->inputChannel,
- MTP_vector(ids)
- )).done(done).send();
- } else {
- using Flag = MTPmessages_DeleteMessages::Flag;
- request(MTPmessages_DeleteMessages(
- MTP_flags(revoke ? Flag::f_revoke : Flag(0)),
- MTP_vector(ids)
- )).done(done).send();
- }
-}
-
void ApiWrap::saveDraftsToCloud() {
for (auto i = _draftsSaveRequestIds.begin(), e = _draftsSaveRequestIds.end(); i != e; ++i) {
if (i->second) continue; // sent already
@@ -4057,12 +3880,12 @@ void ApiWrap::requestSharedMedia(
SharedMediaType type,
MsgId messageId,
SliceType slice) {
- auto key = std::make_tuple(peer, type, messageId, slice);
+ const auto key = std::make_tuple(peer, type, messageId, slice);
if (_sharedMediaRequests.contains(key)) {
return;
}
- auto prepared = Api::PrepareSearchRequest(
+ const auto prepared = Api::PrepareSearchRequest(
peer,
type,
QString(),
@@ -4072,17 +3895,23 @@ void ApiWrap::requestSharedMedia(
return;
}
- auto requestId = request(
- std::move(*prepared)
- ).done([this, peer, type, messageId, slice](
- const MTPmessages_Messages &result) {
- auto key = std::make_tuple(peer, type, messageId, slice);
- _sharedMediaRequests.remove(key);
- sharedMediaDone(peer, type, messageId, slice, result);
- }).fail([this, key](const RPCError &error) {
- _sharedMediaRequests.remove(key);
- }).send();
- _sharedMediaRequests.emplace(key, requestId);
+ const auto history = session().data().history(peer);
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::History;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ return request(
+ std::move(*prepared)
+ ).done([=](const MTPmessages_Messages &result) {
+ const auto key = std::make_tuple(peer, type, messageId, slice);
+ _sharedMediaRequests.remove(key);
+ sharedMediaDone(peer, type, messageId, slice, result);
+ finish();
+ }).fail([=](const RPCError &error) {
+ _sharedMediaRequests.remove(key);
+ finish();
+ }).send();
+ });
+ _sharedMediaRequests.emplace(key);
}
void ApiWrap::sharedMediaDone(
@@ -4432,7 +4261,7 @@ void ApiWrap::userPhotosDone(
//}
void ApiWrap::sendAction(const SendAction &action) {
- readServerHistory(action.history);
+ session().data().histories().readInbox(action.history);
action.history->getReadyFor(ShowAtTheEndMsgId);
_sendActions.fire_copy(action);
}
@@ -4443,6 +4272,8 @@ void ApiWrap::forwardMessages(
FnMut &&successCallback) {
Expects(!items.empty());
+ auto &histories = session().data().histories();
+
struct SharedCallback {
int requestsLeft = 0;
FnMut callback;
@@ -4459,7 +4290,7 @@ void ApiWrap::forwardMessages(
const auto history = action.history;
const auto peer = history->peer;
- readServerHistory(history);
+ histories.readInbox(history);
const auto channelPost = peer->isChannel() && !peer->isMegagroup();
const auto silentPost = action.options.silent
@@ -4491,7 +4322,7 @@ void ApiWrap::forwardMessages(
auto currentGroupId = items.front()->groupId();
auto ids = QVector();
auto randomIds = QVector();
- auto localIds = std::unique_ptr>();
+ auto localIds = std::shared_ptr>();
const auto sendAccumulated = [&] {
if (shared) {
@@ -4501,30 +4332,36 @@ void ApiWrap::forwardMessages(
| (currentGroupId == MessageGroupId()
? MTPmessages_ForwardMessages::Flag(0)
: MTPmessages_ForwardMessages::Flag::f_grouped);
- history->sendRequestId = request(MTPmessages_ForwardMessages(
- MTP_flags(finalFlags),
- forwardFrom->input,
- MTP_vector(ids),
- MTP_vector(randomIds),
- peer->input,
- MTP_int(action.options.scheduled)
- )).done([=, callback = std::move(successCallback)](
- const MTPUpdates &updates) {
- applyUpdates(updates);
- if (shared && !--shared->requestsLeft) {
- shared->callback();
- }
- }).fail([=, ids = std::move(localIds)](const RPCError &error) {
- if (ids) {
- for (const auto &[randomId, itemId] : *ids) {
- sendMessageFail(error, peer, randomId, itemId);
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ history->sendRequestId = request(MTPmessages_ForwardMessages(
+ MTP_flags(finalFlags),
+ forwardFrom->input,
+ MTP_vector(ids),
+ MTP_vector(randomIds),
+ peer->input,
+ MTP_int(action.options.scheduled)
+ )).done([=](
+ const MTPUpdates &updates) {
+ applyUpdates(updates);
+ if (shared && !--shared->requestsLeft) {
+ shared->callback();
}
- } else {
- sendMessageFail(error, peer);
- }
- }).afterRequest(
- history->sendRequestId
- ).send();
+ finish();
+ }).fail([=, ids = localIds](const RPCError &error) {
+ if (ids) {
+ for (const auto &[randomId, itemId] : *ids) {
+ sendMessageFail(error, peer, randomId, itemId);
+ }
+ } else {
+ sendMessageFail(error, peer);
+ }
+ finish();
+ }).afterRequest(
+ history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
ids.resize(0);
randomIds.resize(0);
@@ -4557,7 +4394,7 @@ void ApiWrap::forwardMessages(
message);
_session->data().registerMessageRandomId(randomId, newId);
if (!localIds) {
- localIds = std::make_unique>();
+ localIds = std::make_shared>();
}
localIds->emplace(randomId, newId);
}
@@ -4972,6 +4809,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
HistoryItem *lastMessage = nullptr;
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+
while (TextUtilities::CutPart(sending, left, MaxMessageSize)) {
auto newId = FullMsgId(
peerToChannel(peer->id),
@@ -5062,27 +4902,32 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTPVector()),
clientFlags,
NewMessageType::Unread);
- history->sendRequestId = request(MTPmessages_SendMessage(
- MTP_flags(sendFlags),
- peer->input,
- MTP_int(action.replyTo),
- msgText,
- MTP_long(randomId),
- MTPReplyMarkup(),
- sentEntities,
- MTP_int(action.options.scheduled)
- )).done([=](const MTPUpdates &result) {
- applyUpdates(result, randomId);
- history->clearSentDraftText(QString());
- }).fail([=](const RPCError &error) {
- if (error.type() == qstr("MESSAGE_EMPTY")) {
- lastMessage->destroy();
- } else {
- sendMessageFail(error, peer, randomId, newId);
- }
- history->clearSentDraftText(QString());
- }).afterRequest(history->sendRequestId
- ).send();
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ history->sendRequestId = request(MTPmessages_SendMessage(
+ MTP_flags(sendFlags),
+ peer->input,
+ MTP_int(action.replyTo),
+ msgText,
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(action.options.scheduled)
+ )).done([=](const MTPUpdates &result) {
+ applyUpdates(result, randomId);
+ history->clearSentDraftText(QString());
+ finish();
+ }).fail([=](const RPCError &error) {
+ if (error.type() == qstr("MESSAGE_EMPTY")) {
+ lastMessage->destroy();
+ } else {
+ sendMessageFail(error, peer, randomId, newId);
+ }
+ history->clearSentDraftText(QString());
+ finish();
+ }).afterRequest(history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
}
if (const auto main = App::main()) {
@@ -5188,23 +5033,29 @@ void ApiWrap::sendInlineResult(
history->clearCloudDraft();
history->setSentDraftText(QString());
- history->sendRequestId = request(MTPmessages_SendInlineBotResult(
- MTP_flags(sendFlags),
- peer->input,
- MTP_int(action.replyTo),
- MTP_long(randomId),
- MTP_long(data->getQueryId()),
- MTP_string(data->getId()),
- MTP_int(action.options.scheduled)
- )).done([=](const MTPUpdates &result) {
- applyUpdates(result, randomId);
- history->clearSentDraftText(QString());
- }).fail([=](const RPCError &error) {
- sendMessageFail(error, peer, randomId, newId);
- history->clearSentDraftText(QString());
- }).afterRequest(history->sendRequestId
- ).send();
-
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ history->sendRequestId = request(MTPmessages_SendInlineBotResult(
+ MTP_flags(sendFlags),
+ peer->input,
+ MTP_int(action.replyTo),
+ MTP_long(randomId),
+ MTP_long(data->getQueryId()),
+ MTP_string(data->getId()),
+ MTP_int(action.options.scheduled)
+ )).done([=](const MTPUpdates &result) {
+ applyUpdates(result, randomId);
+ history->clearSentDraftText(QString());
+ finish();
+ }).fail([=](const RPCError &error) {
+ sendMessageFail(error, peer, randomId, newId);
+ history->clearSentDraftText(QString());
+ finish();
+ }).afterRequest(history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
if (const auto main = App::main()) {
main->finishForwarding(action);
}
@@ -5323,25 +5174,32 @@ void ApiWrap::sendMediaWithRandomId(
? MTPmessages_SendMedia::Flag::f_schedule_date
: MTPmessages_SendMedia::Flag(0));
- const auto peer = history->peer;
- const auto itemId = item->fullId();
- history->sendRequestId = request(MTPmessages_SendMedia(
- MTP_flags(flags),
- peer->input,
- MTP_int(replyTo),
- media,
- MTP_string(caption.text),
- MTP_long(randomId),
- MTPReplyMarkup(),
- sentEntities,
- MTP_int(options.scheduled)
- )).done([=](const MTPUpdates &result) {
- applyUpdates(result);
- }).fail([=](const RPCError &error) {
- sendMessageFail(error, peer, randomId, itemId);
- }).afterRequest(
- history->sendRequestId
- ).send();
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ const auto peer = history->peer;
+ const auto itemId = item->fullId();
+ history->sendRequestId = request(MTPmessages_SendMedia(
+ MTP_flags(flags),
+ peer->input,
+ MTP_int(replyTo),
+ media,
+ MTP_string(caption.text),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(options.scheduled)
+ )).done([=](const MTPUpdates &result) {
+ applyUpdates(result);
+ finish();
+ }).fail([=](const RPCError &error) {
+ sendMessageFail(error, peer, randomId, itemId);
+ finish();
+ }).afterRequest(
+ history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
}
void ApiWrap::sendAlbumWithUploaded(
@@ -5419,27 +5277,34 @@ void ApiWrap::sendAlbumIfReady(not_null album) {
| (album->options.scheduled
? MTPmessages_SendMultiMedia::Flag::f_schedule_date
: MTPmessages_SendMultiMedia::Flag(0));
- const auto peer = history->peer;
- history->sendRequestId = request(MTPmessages_SendMultiMedia(
- MTP_flags(flags),
- peer->input,
- MTP_int(replyTo),
- MTP_vector(medias),
- MTP_int(album->options.scheduled)
- )).done([=](const MTPUpdates &result) {
- _sendingAlbums.remove(groupId);
- applyUpdates(result);
- }).fail([=](const RPCError &error) {
- if (const auto album = _sendingAlbums.take(groupId)) {
- for (const auto &item : (*album)->items) {
- sendMessageFail(error, peer, item.randomId, item.msgId);
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ const auto peer = history->peer;
+ history->sendRequestId = request(MTPmessages_SendMultiMedia(
+ MTP_flags(flags),
+ peer->input,
+ MTP_int(replyTo),
+ MTP_vector(medias),
+ MTP_int(album->options.scheduled)
+ )).done([=](const MTPUpdates &result) {
+ _sendingAlbums.remove(groupId);
+ applyUpdates(result);
+ finish();
+ }).fail([=](const RPCError &error) {
+ if (const auto album = _sendingAlbums.take(groupId)) {
+ for (const auto &item : (*album)->items) {
+ sendMessageFail(error, peer, item.randomId, item.msgId);
+ }
+ } else {
+ sendMessageFail(error, peer);
}
- } else {
- sendMessageFail(error, peer);
- }
- }).afterRequest(
- history->sendRequestId
- ).send();
+ finish();
+ }).afterRequest(
+ history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
}
FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const {
@@ -5836,8 +5701,8 @@ Api::SensitiveContent &ApiWrap::sensitiveContent() {
void ApiWrap::createPoll(
const PollData &data,
const SendAction &action,
- FnMut done,
- FnMut fail) {
+ Fn done,
+ Fn fail) {
sendAction(action);
const auto history = action.history;
@@ -5870,27 +5735,34 @@ void ApiWrap::createPoll(
correct.push_back(MTP_bytes(answer.option));
}
}
- const auto replyTo = action.replyTo;
- history->sendRequestId = request(MTPmessages_SendMedia(
- MTP_flags(sendFlags),
- peer->input,
- MTP_int(replyTo),
- MTP_inputMediaPoll(
- MTP_flags(inputFlags),
- PollDataToMTP(&data),
- MTP_vector(correct)),
- MTP_string(),
- MTP_long(rand_value()),
- MTPReplyMarkup(),
- MTPVector(),
- MTP_int(action.options.scheduled)
- )).done([=, done = std::move(done)](const MTPUpdates &result) mutable {
- applyUpdates(result);
- done();
- }).fail([=, fail = std::move(fail)](const RPCError &error) mutable {
- fail(error);
- }).afterRequest(history->sendRequestId
- ).send();
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ const auto replyTo = action.replyTo;
+ history->sendRequestId = request(MTPmessages_SendMedia(
+ MTP_flags(sendFlags),
+ peer->input,
+ MTP_int(replyTo),
+ MTP_inputMediaPoll(
+ MTP_flags(inputFlags),
+ PollDataToMTP(&data),
+ MTP_vector(correct)),
+ MTP_string(),
+ MTP_long(rand_value()),
+ MTPReplyMarkup(),
+ MTPVector(),
+ MTP_int(action.options.scheduled)
+ )).done([=](const MTPUpdates &result) mutable {
+ applyUpdates(result);
+ done();
+ finish();
+ }).fail([=](const RPCError &error) mutable {
+ fail(error);
+ finish();
+ }).afterRequest(history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
}
void ApiWrap::sendPollVotes(
@@ -6000,43 +5872,6 @@ void ApiWrap::reloadPollResults(not_null item) {
_pollReloadRequestIds.emplace(itemId, requestId);
}
-void ApiWrap::readServerHistory(not_null history) {
- if (history->unreadCount()) {
- readServerHistoryForce(history);
- }
- if (history->unreadMark()) {
- changeDialogUnreadMark(history, false);
- }
-}
-
-void ApiWrap::readServerHistoryForce(not_null history) {
- const auto peer = history->peer;
- const auto upTo = history->readInbox();
- if (!upTo) {
- return;
- }
-
- if (const auto channel = peer->asChannel()) {
- if (!channel->amIn()) {
- return; // no read request for channels that I didn't join
- } else if (const auto migrateFrom = channel->migrateFrom()) {
- if (const auto migrated = _session->data().historyLoaded(migrateFrom)) {
- readServerHistory(migrated);
- }
- }
- }
-
- if (_readRequests.contains(peer)) {
- const auto i = _readRequestsPending.find(peer);
- if (i == _readRequestsPending.cend()) {
- _readRequestsPending.emplace(peer, upTo);
- } else if (i->second < upTo) {
- i->second = upTo;
- }
- } else {
- sendReadRequest(peer, upTo);
- }
-}
// // #feed
//void ApiWrap::readFeed(
// not_null feed,
@@ -6090,39 +5925,3 @@ void ApiWrap::readServerHistoryForce(not_null history) {
// _feedReadTimer.callOnce(delay);
// }
//}
-
-void ApiWrap::sendReadRequest(not_null peer, MsgId upTo) {
- const auto requestId = [&] {
- const auto finished = [=] {
- _readRequests.remove(peer);
- if (const auto next = _readRequestsPending.take(peer)) {
- sendReadRequest(peer, *next);
- } else if (const auto history
- = _session->data().historyLoaded(peer)) {
- if (!history->unreadCountKnown()) {
- requestDialogEntry(history);
- }
- }
- };
- if (const auto channel = peer->asChannel()) {
- return request(MTPchannels_ReadHistory(
- channel->inputChannel,
- MTP_int(upTo)
- )).done([=](const MTPBool &result) {
- finished();
- }).fail([=](const RPCError &error) {
- finished();
- }).send();
- }
- return request(MTPmessages_ReadHistory(
- peer->input,
- MTP_int(upTo)
- )).done([=](const MTPmessages_AffectedMessages &result) {
- applyAffectedMessages(peer, result);
- finished();
- }).fail([=](const RPCError &error) {
- finished();
- }).send();
- }();
- _readRequests.emplace(peer, requestId, upTo);
-}
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 0a57a619b..c48ef2b01 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -141,6 +141,9 @@ public:
void applyUpdates(
const MTPUpdates &updates,
uint64 sentMessageRandomId = 0);
+ int applyAffectedHistory(
+ not_null peer,
+ const MTPmessages_AffectedHistory &result);
void registerModifyRequest(const QString &key, mtpRequestId requestId);
void clearModifyRequest(const QString &key);
@@ -170,18 +173,10 @@ public:
rpl::producer dialogsLoadMayBlockByDate() const;
rpl::producer dialogsLoadBlockedByDate() const;
- void requestDialogEntry(not_null folder);
- void requestDialogEntry(
- not_null history,
- Fn callback = nullptr);
- void dialogEntryApplied(not_null history);
//void applyFeedSources(const MTPDchannels_feedSources &data); // #feed
//void setFeedChannels(
// not_null feed,
// const std::vector> &channels);
- void changeDialogUnreadMark(not_null history, bool unread);
- //void changeDialogUnreadMark(not_null feed, bool unread); // #feed
- void requestFakeChatListMessage(not_null history);
void requestWallPaper(
const QString &slug,
@@ -305,10 +300,6 @@ public:
void clearHistory(not_null peer, bool revoke);
void deleteConversation(not_null peer, bool revoke);
- void deleteMessages(
- not_null peer,
- const QVector &ids,
- bool revoke);
base::Observable &fullPeerUpdated() {
return _fullPeerUpdated;
@@ -389,11 +380,12 @@ public:
const QString &lastName,
const SendAction &action);
void shareContact(not_null user, const SendAction &action);
- void readServerHistory(not_null history);
- void readServerHistoryForce(not_null history);
//void readFeed( // #feed
// not_null feed,
// Data::MessagePosition position);
+ void applyAffectedMessages(
+ not_null peer,
+ const MTPmessages_AffectedMessages &result);
void sendVoiceMessage(
QByteArray result,
@@ -475,8 +467,8 @@ public:
void createPoll(
const PollData &data,
const SendAction &action,
- FnMut done,
- FnMut fail);
+ Fn done,
+ Fn fail);
void sendPollVotes(
FullMsgId itemId,
const std::vector &options);
@@ -531,7 +523,6 @@ private:
QVector collectMessageIds(const MessageDataRequests &requests);
MessageDataRequests *messageDataRequests(ChannelData *channel, bool onlyExisting = false);
- void applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs);
void gotChatFull(
not_null peer,
@@ -623,14 +614,7 @@ private:
not_null peer,
bool justClear,
bool revoke);
- void sendReadRequest(not_null peer, MsgId upTo);
- int applyAffectedHistory(
- not_null peer,
- const MTPmessages_AffectedHistory &result);
void applyAffectedMessages(const MTPmessages_AffectedMessages &result);
- void applyAffectedMessages(
- not_null peer,
- const MTPmessages_AffectedMessages &result);
void deleteAllFromUserSend(
not_null channel,
@@ -685,8 +669,6 @@ private:
not_null channel);
void migrateFail(not_null peer, const RPCError &error);
- void sendDialogRequests();
-
not_null _session;
base::flat_map _modifyRequests;
@@ -756,22 +738,14 @@ private:
mtpRequestId _contactsRequestId = 0;
mtpRequestId _contactsStatusesRequestId = 0;
- base::flat_set> _dialogFolderRequests;
- base::flat_map<
- not_null,
- std::vector>> _dialogRequests;
- base::flat_map<
- not_null,
- std::vector>> _dialogRequestsPending;
- base::flat_set> _fakeChatListRequests;
base::flat_map, mtpRequestId> _unreadMentionsRequests;
- base::flat_map,
SharedMediaType,
MsgId,
- SliceType>, mtpRequestId> _sharedMediaRequests;
+ SliceType>> _sharedMediaRequests;
base::flat_map, mtpRequestId> _userPhotosRequests;
@@ -800,18 +774,6 @@ private:
rpl::event_stream _sendActions;
- struct ReadRequest {
- ReadRequest(mtpRequestId requestId, MsgId upTo)
- : requestId(requestId)
- , upTo(upTo) {
- }
-
- mtpRequestId requestId = 0;
- MsgId upTo = 0;
- };
- base::flat_map, ReadRequest> _readRequests;
- base::flat_map, MsgId> _readRequestsPending;
-
std::unique_ptr _fileLoader;
base::flat_map> _sendingAlbums;
diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp
index 06e10fc37..f303c4b93 100644
--- a/Telegram/SourceFiles/boxes/add_contact_box.cpp
+++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp
@@ -494,6 +494,7 @@ void GroupInfoBox::prepare() {
_description->setInstantReplaces(Ui::InstantReplaces::Default());
_description->setInstantReplacesEnabled(
_navigation->session().settings().replaceEmojiValue());
+ _description->setSubmitSettings(_navigation->session().settings().sendSubmitWay());
connect(_description, &Ui::InputField::resized, [=] { descriptionResized(); });
connect(_description, &Ui::InputField::submitted, [=] { submit(); });
diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp
index e72908a01..475c04009 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp
@@ -293,8 +293,7 @@ AdminLog::OwnedItem GenerateTextItem(
const auto clientFlags = MTPDmessage_ClientFlag::f_fake_history_item;
const auto replyTo = 0;
const auto viaBotId = 0;
- const auto item = history->owner().makeMessage(
- history,
+ const auto item = history->makeMessage(
++id,
flags,
clientFlags,
diff --git a/Telegram/SourceFiles/boxes/confirm_box.cpp b/Telegram/SourceFiles/boxes/confirm_box.cpp
index c8b6efcd2..c580d3716 100644
--- a/Telegram/SourceFiles/boxes/confirm_box.cpp
+++ b/Telegram/SourceFiles/boxes/confirm_box.cpp
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_file_origin.h"
+#include "data/data_histories.h"
#include "base/unixtime.h"
#include "main/main_session.h"
#include "observer_peer.h"
@@ -785,7 +786,9 @@ void DeleteMessagesBox::deleteAndClear() {
_deleteConfirmedCallback();
}
- base::flat_map, QVector> idsByPeer;
+ auto remove = std::vector>();
+ remove.reserve(_ids.size());
+ base::flat_map, QVector> idsByPeer;
base::flat_map, QVector> scheduledIdsByPeer;
for (const auto itemId : _ids) {
if (const auto item = _session->data().message(itemId)) {
@@ -801,21 +804,15 @@ void DeleteMessagesBox::deleteAndClear() {
}
continue;
}
- const auto wasOnServer = IsServerMsgId(item->id);
- const auto wasLast = (history->lastMessage() == item);
- const auto wasInChats = (history->chatListMessage() == item);
- item->destroy();
-
- if (wasOnServer) {
- idsByPeer[history->peer].push_back(MTP_int(itemId.msg));
- } else if (wasLast || wasInChats) {
- history->requestChatListMessage();
+ remove.push_back(item);
+ if (IsServerMsgId(item->id)) {
+ idsByPeer[history].push_back(MTP_int(itemId.msg));
}
}
}
- for (const auto &[peer, ids] : idsByPeer) {
- peer->session().api().deleteMessages(peer, ids, revoke);
+ for (const auto &[history, ids] : idsByPeer) {
+ history->owner().histories().deleteMessages(history, ids, revoke);
}
for (const auto &[peer, ids] : scheduledIdsByPeer) {
peer->session().api().request(MTPmessages_DeleteScheduledMessages(
@@ -826,6 +823,17 @@ void DeleteMessagesBox::deleteAndClear() {
}).send();
}
+ for (const auto item : remove) {
+ const auto history = item->history();
+ const auto wasLast = (history->lastMessage() == item);
+ const auto wasInChats = (history->chatListMessage() == item);
+ item->destroy();
+
+ if (wasLast || wasInChats) {
+ history->requestChatListMessage();
+ }
+ }
+
const auto session = _session;
Ui::hideLayer();
session->data().sendHistoryChangeNotifications();
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp
index 040f5fd09..510b91bea 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.cpp
+++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp
@@ -767,6 +767,7 @@ not_null CreatePollBox::setupQuestion(
st::createPollFieldPadding);
InitField(getDelegate()->outerContainer(), question, _session);
question->setMaxLength(kQuestionLimit + kErrorLimit);
+ question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
const auto warning = CreateWarningLabel(
container,
diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp
new file mode 100644
index 000000000..1d8940251
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp
@@ -0,0 +1,442 @@
+/*
+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/dictionaries_manager.h"
+
+#ifndef TDESKTOP_DISABLE_SPELLCHECK
+
+#include "base/event_filter.h"
+#include "chat_helpers/spellchecker_common.h"
+#include "core/application.h"
+#include "main/main_account.h"
+#include "main/main_session.h"
+#include "mainwidget.h"
+#include "mtproto/dedicated_file_loader.h"
+#include "spellcheck/spellcheck_utils.h"
+#include "styles/style_layers.h"
+#include "styles/style_settings.h"
+#include "styles/style_boxes.h"
+#include "ui/wrap/vertical_layout.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/labels.h"
+#include "ui/widgets/multi_select.h"
+#include "ui/widgets/popup_menu.h"
+#include "ui/wrap/slide_wrap.h"
+#include "ui/effects/animations.h"
+
+namespace Ui {
+namespace {
+
+using Dictionaries = std::vector;
+using namespace Storage::CloudBlob;
+
+using Loading = MTP::DedicatedLoader::Progress;
+using DictState = BlobState;
+using QueryCallback = Fn;
+constexpr auto kMaxQueryLength = 15;
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
+#define OLD_QT
+using QStringView = QString;
+#endif
+
+class Inner : public Ui::RpWidget {
+public:
+ Inner(QWidget *parent, Dictionaries enabledDictionaries);
+
+ Dictionaries enabledRows() const;
+ QueryCallback queryCallback() const;
+
+private:
+ void setupContent(Dictionaries enabledDictionaries);
+
+ Dictionaries _enabledRows;
+ QueryCallback _queryCallback;
+
+};
+
+inline auto DictExists(int langId) {
+ return Spellchecker::DictionaryExists(langId);
+}
+
+inline auto FilterEnabledDict(Dictionaries dicts) {
+ return dicts | ranges::views::filter(
+ DictExists
+ ) | ranges::to_vector;
+}
+
+DictState ComputeState(int id, bool enabled) {
+ const auto result = enabled ? DictState(Active()) : DictState(Ready());
+ if (DictExists(id)) {
+ return result;
+ }
+ return Available{ Spellchecker::GetDownloadSize(id) };
+}
+
+QString StateDescription(const DictState &state) {
+ return StateDescription(
+ state,
+ tr::lng_settings_manage_enabled_dictionary);
+}
+
+auto CreateMultiSelect(QWidget *parent) {
+ const auto result = Ui::CreateChild(
+ parent,
+ st::contactsMultiSelect,
+ tr::lng_participant_filter());
+
+ result->resizeToWidth(st::boxWidth);
+ result->moveToLeft(0, 0);
+ return result;
+}
+
+Inner::Inner(
+ QWidget *parent,
+ Dictionaries enabledDictionaries) : RpWidget(parent) {
+ setupContent(std::move(enabledDictionaries));
+}
+
+QueryCallback Inner::queryCallback() const {
+ return _queryCallback;
+}
+
+Dictionaries Inner::enabledRows() const {
+ return _enabledRows;
+}
+
+auto AddButtonWithLoader(
+ not_null content,
+ const Spellchecker::Dict &dict,
+ bool buttonEnabled,
+ rpl::producer query) {
+ const auto id = dict.id;
+ buttonEnabled &= DictExists(id);
+
+ const auto locale = Spellchecker::LocaleFromLangId(id);
+ const std::vector indexList = {
+ dict.name,
+ QLocale::languageToString(locale.language()),
+ QLocale::countryToString(locale.country())
+ };
+
+ const auto wrap = content->add(
+ object_ptr>(
+ content,
+ object_ptr(
+ content,
+ rpl::single(dict.name),
+ st::dictionariesSectionButton
+ )
+ )
+ );
+ const auto button = wrap->entity();
+
+ std::move(
+ query
+ ) | rpl::start_with_next([=](auto string) {
+ wrap->toggle(
+ ranges::any_of(indexList, [&](const QString &s) {
+ return s.startsWith(string, Qt::CaseInsensitive);
+ }),
+ anim::type::instant);
+ }, button->lifetime());
+
+ using Loader = Spellchecker::DictLoader;
+ using GlobalLoaderPtr = std::shared_ptr>;
+
+ const auto localLoader = button->lifetime()
+ .make_state>();
+ const auto localLoaderValues = button->lifetime()
+ .make_state>();
+ const auto setLocalLoader = [=](base::unique_qptr loader) {
+ *localLoader = std::move(loader);
+ localLoaderValues->fire(localLoader->get());
+ };
+ const auto destroyLocalLoader = [=] {
+ setLocalLoader(nullptr);
+ };
+
+ const auto buttonState = button->lifetime()
+ .make_state>();
+ const auto dictionaryRemoved = button->lifetime()
+ .make_state>();
+ const auto dictionaryFromGlobalLoader = button->lifetime()
+ .make_state>();
+
+ const auto globalLoader = button->lifetime()
+ .make_state();
+
+ const auto rawGlobalLoaderPtr = [=]() -> Loader* {
+ if (!globalLoader || !*globalLoader || !*globalLoader->get()) {
+ return nullptr;
+ }
+ return globalLoader->get()->get();
+ };
+
+ const auto setGlobalLoaderPtr = [=](GlobalLoaderPtr loader) {
+ if (localLoader->get()) {
+ if (loader && loader->get()) {
+ loader->get()->destroy();
+ }
+ return;
+ }
+ *globalLoader = std::move(loader);
+ localLoaderValues->fire(rawGlobalLoaderPtr());
+ if (rawGlobalLoaderPtr()) {
+ dictionaryFromGlobalLoader->fire({});
+ }
+ };
+
+ Spellchecker::GlobalLoaderChanged(
+ ) | rpl::start_with_next([=](int langId) {
+ if (!langId && rawGlobalLoaderPtr()) {
+ setGlobalLoaderPtr(nullptr);
+ } else if (langId == id) {
+ setGlobalLoaderPtr(Spellchecker::GlobalLoader());
+ }
+ }, button->lifetime());
+
+ const auto label = Ui::CreateChild(
+ button,
+ buttonState->value() | rpl::map(StateDescription),
+ st::settingsUpdateState);
+ label->setAttribute(Qt::WA_TransparentForMouseEvents);
+
+ rpl::combine(
+ button->widthValue(),
+ label->widthValue()
+ ) | rpl::start_with_next([=] {
+ label->moveToLeft(
+ st::settingsUpdateStatePosition.x(),
+ st::settingsUpdateStatePosition.y());
+ }, label->lifetime());
+
+ buttonState->value(
+ ) | rpl::start_with_next([=](const DictState &state) {
+ const auto isToggledSet = state.is();
+ const auto toggled = isToggledSet ? 1. : 0.;
+ const auto over = !button->isDisabled()
+ && (button->isDown() || button->isOver());
+
+ if (toggled == 0. && !over) {
+ label->setTextColorOverride(std::nullopt);
+ } else {
+ label->setTextColorOverride(anim::color(
+ over ? st::contactsStatusFgOver : st::contactsStatusFg,
+ st::contactsStatusFgOnline,
+ toggled));
+ }
+ }, label->lifetime());
+
+ button->toggleOn(
+ rpl::single(
+ buttonEnabled
+ ) | rpl::then(
+ rpl::merge(
+ // Events to toggle on.
+ dictionaryFromGlobalLoader->events(
+ ) | rpl::map([] {
+ return true;
+ }),
+ // Events to toggle off.
+ rpl::merge(
+ dictionaryRemoved->events(),
+ buttonState->value(
+ ) | rpl::filter([](const DictState &state) {
+ return state.is();
+ }) | rpl::map([] {
+ return rpl::empty_value();
+ })
+ ) | rpl::map([] {
+ return false;
+ })
+ )
+ )
+ );
+
+ *buttonState = localLoaderValues->events_starting_with(
+ rawGlobalLoaderPtr() ? rawGlobalLoaderPtr() : localLoader->get()
+ ) | rpl::map([=](Loader *loader) {
+ return (loader && loader->id() == id)
+ ? loader->state()
+ : rpl::single(
+ buttonEnabled
+ ) | rpl::then(
+ rpl::merge(
+ dictionaryRemoved->events(
+ ) | rpl::map([] {
+ return false;
+ }),
+ button->toggledValue()
+ )
+ ) | rpl::map([=](auto enabled) {
+ return ComputeState(id, enabled);
+ });
+ }) | rpl::flatten_latest(
+ ) | rpl::filter([=](const DictState &state) {
+ return !buttonState->current().is() || !state.is();
+ });
+
+ button->toggledValue(
+ ) | rpl::start_with_next([=](bool toggled) {
+ const auto &state = buttonState->current();
+ if (toggled && (state.is() || state.is())) {
+ const auto weak = Ui::MakeWeak(button);
+ setLocalLoader(base::make_unique_q(
+ App::main(),
+ id,
+ Spellchecker::GetDownloadLocation(id),
+ Spellchecker::DictPathByLangId(id),
+ Spellchecker::GetDownloadSize(id),
+ crl::guard(weak, destroyLocalLoader)));
+ } else if (!toggled && state.is()) {
+ if (const auto g = rawGlobalLoaderPtr()) {
+ g->destroy();
+ return;
+ }
+ if (localLoader && localLoader->get()->id() == id) {
+ destroyLocalLoader();
+ }
+ }
+ }, button->lifetime());
+
+ const auto contextMenu = button->lifetime()
+ .make_state>();
+ const auto showMenu = [=] {
+ if (!DictExists(id)) {
+ return false;
+ }
+ *contextMenu = base::make_unique_q(button);
+ contextMenu->get()->addAction(
+ tr::lng_settings_manage_remove_dictionary(tr::now), [=] {
+ Spellchecker::RemoveDictionary(id);
+ dictionaryRemoved->fire({});
+ });
+ contextMenu->get()->popup(QCursor::pos());
+ return true;
+ };
+
+ base::install_event_filter(button, [=](not_null e) {
+ if (e->type() == QEvent::ContextMenu && showMenu()) {
+ return base::EventFilterResult::Cancel;
+ }
+ return base::EventFilterResult::Continue;
+ });
+
+ if (const auto g = Spellchecker::GlobalLoader()) {
+ if (g.get() && g->get()->id() == id) {
+ setGlobalLoaderPtr(g);
+ }
+ }
+
+ return button;
+}
+
+void Inner::setupContent(Dictionaries enabledDictionaries) {
+ const auto content = Ui::CreateChild(this);
+
+ const auto queryStream = content->lifetime()
+ .make_state>();
+
+ for (const auto &dict : Spellchecker::Dictionaries()) {
+ const auto id = dict.id;
+ const auto row = AddButtonWithLoader(
+ content,
+ dict,
+ ranges::contains(enabledDictionaries, id),
+ queryStream->events());
+ row->toggledValue(
+ ) | rpl::start_with_next([=](auto enabled) {
+ if (enabled) {
+ _enabledRows.push_back(id);
+ } else {
+ auto &rows = _enabledRows;
+ rows.erase(ranges::remove(rows, id), end(rows));
+ }
+ }, row->lifetime());
+ }
+
+ _queryCallback = [=](const QString &query) {
+ if (query.size() >= kMaxQueryLength) {
+ return;
+ }
+ queryStream->fire_copy(query);
+ };
+
+ content->resizeToWidth(st::boxWidth);
+ Ui::ResizeFitChild(this, content);
+}
+
+} // namespace
+
+ManageDictionariesBox::ManageDictionariesBox(
+ QWidget*,
+ not_null session)
+: _session(session) {
+}
+
+void ManageDictionariesBox::setInnerFocus() {
+ _setInnerFocus();
+}
+
+void ManageDictionariesBox::prepare() {
+ const auto multiSelect = CreateMultiSelect(this);
+
+ const auto inner = setInnerWidget(
+ object_ptr(
+ this,
+ _session->settings().dictionariesEnabled()),
+ st::boxScroll,
+ multiSelect->height()
+ );
+
+ multiSelect->setQueryChangedCallback(inner->queryCallback());
+ _setInnerFocus = [=] {
+ multiSelect->setInnerFocus();
+ };
+
+ // The initial list of enabled rows may differ from the list of languages
+ // in settings, so we should store it when box opens
+ // and save it when box closes (don't do it when "Save" was pressed).
+ const auto initialEnabledRows = inner->enabledRows();
+
+ setTitle(tr::lng_settings_manage_dictionaries());
+
+ addButton(tr::lng_settings_save(), [=] {
+ _session->settings().setDictionariesEnabled(
+ FilterEnabledDict(inner->enabledRows()));
+ _session->saveSettingsDelayed();
+ // Ignore boxClosing() when the Save button was pressed.
+ lifetime().destroy();
+ closeBox();
+ });
+ addButton(tr::lng_close(), [=] { closeBox(); });
+
+ boxClosing() | rpl::start_with_next([=] {
+ _session->settings().setDictionariesEnabled(
+ FilterEnabledDict(initialEnabledRows));
+ _session->saveSettingsDelayed();
+ }, lifetime());
+
+ setDimensionsToContent(st::boxWidth, inner);
+
+ using namespace rpl::mappers;
+ const auto max = lifetime().make_state(0);
+ rpl::combine(
+ inner->heightValue(),
+ multiSelect->heightValue(),
+ _1 + _2
+ ) | rpl::start_with_next([=](int height) {
+ using std::min;
+ accumulate_max(*max, height);
+ setDimensions(st::boxWidth, min(*max, st::boxMaxListHeight), true);
+ }, inner->lifetime());
+}
+
+} // namespace Ui
+
+#endif // !TDESKTOP_DISABLE_SPELLCHECK
diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.h b/Telegram/SourceFiles/boxes/dictionaries_manager.h
new file mode 100644
index 000000000..f137a76ca
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/dictionaries_manager.h
@@ -0,0 +1,38 @@
+/*
+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
+
+#ifndef TDESKTOP_DISABLE_SPELLCHECK
+
+#include "boxes/abstract_box.h"
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Ui {
+
+class ManageDictionariesBox : public Ui::BoxContent {
+public:
+ ManageDictionariesBox(
+ QWidget*,
+ not_null session);
+
+protected:
+ void prepare() override;
+ void setInnerFocus() override;
+
+private:
+ const not_null _session;
+ Fn _setInnerFocus;
+
+};
+
+} // namespace Ui
+
+#endif // !TDESKTOP_DISABLE_SPELLCHECK
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
index dd20abb01..f00f74c3b 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
@@ -260,7 +260,7 @@ EditCaptionBox::EditCaptionBox(
tr::lng_photo_caption(),
editData);
_field->setMaxLength(Global::CaptionLengthMax());
- _field->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
+ _field->setSubmitSettings(_controller->session().settings().sendSubmitWay());
_field->setInstantReplaces(Ui::InstantReplaces::Default());
_field->setInstantReplacesEnabled(
_controller->session().settings().replaceEmojiValue());
diff --git a/Telegram/SourceFiles/boxes/mute_settings_box.cpp b/Telegram/SourceFiles/boxes/mute_settings_box.cpp
index fb8b67b09..7fd659c9b 100644
--- a/Telegram/SourceFiles/boxes/mute_settings_box.cpp
+++ b/Telegram/SourceFiles/boxes/mute_settings_box.cpp
@@ -76,7 +76,7 @@ void MuteSettingsBox::prepare() {
_save = [=] {
const auto muteForSeconds = group->value() * 3600;
- _peer->session().data().updateNotifySettings(
+ _peer->owner().updateNotifySettings(
_peer,
muteForSeconds);
closeBox();
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 606ecc4f4..5fab61356 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_folder.h"
+#include "data/data_histories.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "lang/lang_keys.h"
@@ -30,33 +31,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
void ShareBotGame(not_null bot, not_null chat) {
- const auto history = chat->owner().historyLoaded(chat);
- const auto randomId = rand_value();
- const auto api = &chat->session().api();
- const auto requestId = api->request(MTPmessages_SendMedia(
- MTP_flags(0),
- chat->input,
- MTP_int(0),
- MTP_inputMediaGame(
- MTP_inputGameShortName(
- bot->inputUser,
- MTP_string(bot->botInfo->shareGameShortName))),
- MTP_string(),
- MTP_long(randomId),
- MTPReplyMarkup(),
- MTPVector(),
- MTP_int(0) // schedule_date
- )).done([=](const MTPUpdates &result) {
- api->applyUpdates(result, randomId);
- }).fail([=](const RPCError &error) {
- api->sendMessageFail(error, chat);
- }).afterRequest(
- history ? history->sendRequestId : 0
- ).send();
-
- if (history) {
- history->sendRequestId = requestId;
- }
+ const auto history = chat->owner().history(chat);
+ auto &histories = history->owner().histories();
+ const auto requestType = Data::Histories::RequestType::Send;
+ histories.sendRequest(history, requestType, [=](Fn finish) {
+ const auto randomId = rand_value();
+ const auto api = &chat->session().api();
+ history->sendRequestId = api->request(MTPmessages_SendMedia(
+ MTP_flags(0),
+ chat->input,
+ MTP_int(0),
+ MTP_inputMediaGame(
+ MTP_inputGameShortName(
+ bot->inputUser,
+ MTP_string(bot->botInfo->shareGameShortName))),
+ MTP_string(),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ MTPVector(),
+ MTP_int(0) // schedule_date
+ )).done([=](const MTPUpdates &result) {
+ api->applyUpdates(result, randomId);
+ finish();
+ }).fail([=](const RPCError &error) {
+ api->sendMessageFail(error, chat);
+ finish();
+ }).afterRequest(
+ history->sendRequestId
+ ).send();
+ return history->sendRequestId;
+ });
Ui::hideLayer();
Ui::showPeerHistory(chat, ShowAtUnreadMsgId);
}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index 5ee2cd89a..3885ec4aa 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -500,6 +500,7 @@ object_ptr Controller::createDescriptionEdit() {
result->entity()->setInstantReplaces(Ui::InstantReplaces::Default());
result->entity()->setInstantReplacesEnabled(
_peer->session().settings().replaceEmojiValue());
+ result->entity()->setSubmitSettings(_peer->session().settings().sendSubmitWay());
Ui::Emoji::SuggestionsController::Init(
_wrap->window(),
result->entity(),
diff --git a/Telegram/SourceFiles/boxes/rate_call_box.cpp b/Telegram/SourceFiles/boxes/rate_call_box.cpp
index 85294a3fe..e1c97e6e8 100644
--- a/Telegram/SourceFiles/boxes/rate_call_box.cpp
+++ b/Telegram/SourceFiles/boxes/rate_call_box.cpp
@@ -85,7 +85,7 @@ void RateCallBox::ratingChanged(int value) {
Ui::InputField::Mode::MultiLine,
tr::lng_call_rate_comment());
_comment->show();
- _comment->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
+ _comment->setSubmitSettings(_session->settings().sendSubmitWay());
_comment->setMaxLength(kRateCallCommentLengthMax);
_comment->resize(width() - (st::callRatingPadding.left() + st::callRatingPadding.right()), _comment->height());
diff --git a/Telegram/SourceFiles/boxes/report_box.cpp b/Telegram/SourceFiles/boxes/report_box.cpp
index 28697df52..ff95cc5aa 100644
--- a/Telegram/SourceFiles/boxes/report_box.cpp
+++ b/Telegram/SourceFiles/boxes/report_box.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "data/data_peer.h"
+#include "main/main_session.h"
#include "boxes/confirm_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/buttons.h"
@@ -106,7 +107,7 @@ void ReportBox::reasonChanged(Reason reason) {
Ui::InputField::Mode::MultiLine,
tr::lng_report_reason_description());
_reasonOtherText->show();
- _reasonOtherText->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
+ _reasonOtherText->setSubmitSettings(_peer->session().settings().sendSubmitWay());
_reasonOtherText->setMaxLength(kReportReasonLengthMax);
_reasonOtherText->resize(width() - (st::boxPadding.left() + st::boxOptionListPadding.left() + st::boxPadding.right()), _reasonOtherText->height());
diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp
index 04376241a..ec573c695 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_files_box.cpp
@@ -2046,7 +2046,7 @@ void SendFilesBox::applyAlbumOrder() {
void SendFilesBox::setupCaption() {
_caption->setMaxLength(Global::CaptionLengthMax());
- _caption->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
+ _caption->setSubmitSettings(_controller->session().settings().sendSubmitWay());
connect(_caption, &Ui::InputField::resized, [=] {
captionResized();
});
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 1608c8557..9719bee0b 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -206,6 +206,7 @@ void ShareBox::prepareCommentField() {
field->setMarkdownReplacesEnabled(rpl::single(true));
field->setEditLinkCallback(
DefaultEditLinkCallback(&_navigation->session(), field));
+ field->setSubmitSettings(_navigation->session().settings().sendSubmitWay());
InitSpellchecker(&_navigation->session(), field);
Ui::SendPendingMoveResizeEvents(_comment);
diff --git a/Telegram/SourceFiles/calls/calls_box_controller.h b/Telegram/SourceFiles/calls/calls_box_controller.h
index d67e22c04..ae5894df4 100644
--- a/Telegram/SourceFiles/calls/calls_box_controller.h
+++ b/Telegram/SourceFiles/calls/calls_box_controller.h
@@ -44,7 +44,7 @@ private:
MTP::Sender _api;
MsgId _offsetId = 0;
- mtpRequestId _loadRequestId = 0;
+ int _loadRequestId = 0; // Not a real mtpRequestId.
bool _allLoaded = false;
};
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
index ad1f896fc..a59a2a2e9 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
@@ -15,13 +15,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/emoji_config.h"
-#include "lang/lang_keys.h"
-#include "base/zlib_help.h"
-#include "layout.h"
#include "core/application.h"
#include "main/main_account.h"
#include "mainwidget.h"
#include "app.h"
+#include "storage/storage_cloud_blob.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
@@ -30,69 +28,37 @@ namespace Ui {
namespace Emoji {
namespace {
-struct Available {
- int size = 0;
+using namespace Storage::CloudBlob;
- inline bool operator<(const Available &other) const {
- return size < other.size;
- }
- inline bool operator==(const Available &other) const {
- return size == other.size;
- }
+struct Set : public Blob {
+ QString previewPath;
};
-struct Ready {
- inline bool operator<(const Ready &other) const {
- return false;
- }
- inline bool operator==(const Ready &other) const {
- return true;
- }
-};
-struct Active {
- inline bool operator<(const Active &other) const {
- return false;
- }
- inline bool operator==(const Active &other) const {
- return true;
- }
+
+inline auto PreviewPath(int i) {
+ return qsl(":/gui/emoji/set%1_preview.webp").arg(i);
+}
+
+const auto kSets = {
+ Set{ {0, 0, 0, "Mac"}, PreviewPath(0) },
+ Set{ {1, 246, 7'336'383, "Android"}, PreviewPath(1) },
+ Set{ {2, 206, 5'038'738, "Twemoji"}, PreviewPath(2) },
+ Set{ {3, 238, 6'992'260, "JoyPixels"}, PreviewPath(3) },
};
+
using Loading = MTP::DedicatedLoader::Progress;
-struct Failed {
- inline bool operator<(const Failed &other) const {
- return false;
- }
- inline bool operator==(const Failed &other) const {
- return true;
- }
-};
-using SetState = base::variant<
- Available,
- Ready,
- Active,
- Loading,
- Failed>;
+using SetState = BlobState;
-class Loader : public QObject {
+class Loader : public BlobLoader {
public:
- Loader(QObject *parent, int id);
+ Loader(
+ QObject *parent,
+ int id,
+ MTP::DedicatedLoader::Location location,
+ const QString &folder,
+ int size);
- int id() const;
-
- rpl::producer state() const;
- void destroy();
-
-private:
- void setImplementation(std::unique_ptr loader);
- void unpack(const QString &path);
- void finalize(const QString &path);
- void fail();
-
- int _id = 0;
- int _size = 0;
- rpl::variable _state;
-
- MTP::WeakInstance _mtproto;
- std::unique_ptr _implementation;
+ void destroy() override;
+ void unpack(const QString &path) override;
};
@@ -127,6 +93,7 @@ private:
void setupHandler();
void load();
void radialAnimationCallback(crl::time now);
+ void updateLoadingToFinished();
int _id = 0;
bool _switching = false;
@@ -148,15 +115,19 @@ void SetGlobalLoader(base::unique_qptr loader) {
}
int GetDownloadSize(int id) {
- const auto sets = Sets();
- return ranges::find(sets, id, &Set::id)->size;
+ return ranges::find(kSets, id, &Set::id)->size;
+}
+
+[[nodiscard]] float64 CountProgress(not_null loading) {
+ return (loading->size > 0)
+ ? (loading->already / float64(loading->size))
+ : 0.;
}
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
- constexpr auto kUsername = "tdhbcfiles";
- const auto sets = Sets();
- const auto i = ranges::find(sets, id, &Set::id);
- return MTP::DedicatedLoader::Location{ kUsername, i->postId };
+ const auto username = kCloudLocationUsername.utf16();
+ const auto i = ranges::find(kSets, id, &Set::id);
+ return MTP::DedicatedLoader::Location{ username, i->postId };
}
SetState ComputeState(int id) {
@@ -169,45 +140,9 @@ SetState ComputeState(int id) {
}
QString StateDescription(const SetState &state) {
- return state.match([](const Available &data) {
- return tr::lng_emoji_set_download(tr::now, lt_size, formatSizeText(data.size));
- }, [](const Ready &data) -> QString {
- return tr::lng_emoji_set_ready(tr::now);
- }, [](const Active &data) -> QString {
- return tr::lng_emoji_set_active(tr::now);
- }, [](const Loading &data) {
- const auto percent = (data.size > 0)
- ? snap((data.already * 100) / float64(data.size), 0., 100.)
- : 0.;
- return tr::lng_emoji_set_loading(
- tr::now,
- lt_percent,
- QString::number(int(std::round(percent))) + '%',
- lt_progress,
- formatDownloadText(data.already, data.size));
- }, [](const Failed &data) {
- return tr::lng_attach_failed(tr::now);
- });
-}
-
-QByteArray ReadFinalFile(const QString &path) {
- constexpr auto kMaxZipSize = 10 * 1024 * 1024;
- auto file = QFile(path);
- if (file.size() > kMaxZipSize || !file.open(QIODevice::ReadOnly)) {
- return QByteArray();
- }
- return file.readAll();
-}
-
-bool ExtractZipFile(zlib::FileToRead &zip, const QString path) {
- constexpr auto kMaxSize = 10 * 1024 * 1024;
- const auto content = zip.readCurrentFileContent(kMaxSize);
- if (content.isEmpty() || zip.error() != UNZ_OK) {
- return false;
- }
- auto file = QFile(path);
- return file.open(QIODevice::WriteOnly)
- && (file.write(content) == content.size());
+ return StateDescription(
+ state,
+ tr::lng_emoji_set_active);
}
bool GoodSetPartName(const QString &name) {
@@ -217,89 +152,24 @@ bool GoodSetPartName(const QString &name) {
}
bool UnpackSet(const QString &path, const QString &folder) {
- const auto bytes = ReadFinalFile(path);
- if (bytes.isEmpty()) {
- return false;
- }
-
- auto zip = zlib::FileToRead(bytes);
- if (zip.goToFirstFile() != UNZ_OK) {
- return false;
- }
- do {
- const auto name = zip.getCurrentFileName();
- const auto path = folder + '/' + name;
- if (GoodSetPartName(name) && !ExtractZipFile(zip, path)) {
- return false;
- }
-
- const auto jump = zip.goToNextFile();
- if (jump == UNZ_END_OF_LIST_OF_FILE) {
- break;
- } else if (jump != UNZ_OK) {
- return false;
- }
- } while (true);
- return true;
+ return UnpackBlob(path, folder, GoodSetPartName);
}
-Loader::Loader(QObject *parent, int id)
-: QObject(parent)
-, _id(id)
-, _size(GetDownloadSize(_id))
-, _state(Loading{ 0, _size })
-, _mtproto(Core::App().activeAccount().mtp()) {
- const auto ready = [=](std::unique_ptr loader) {
- if (loader) {
- setImplementation(std::move(loader));
- } else {
- fail();
- }
- };
- const auto location = GetDownloadLocation(id);
- const auto folder = internal::SetDataPath(id);
- MTP::StartDedicatedLoader(&_mtproto, location, folder, ready);
-}
-
-int Loader::id() const {
- return _id;
-}
-
-rpl::producer Loader::state() const {
- return _state.value();
-}
-
-void Loader::setImplementation(
- std::unique_ptr loader) {
- _implementation = std::move(loader);
- auto convert = [](auto value) {
- return SetState(value);
- };
- _state = _implementation->progress(
- ) | rpl::map([](const Loading &state) {
- return SetState(state);
- });
- _implementation->failed(
- ) | rpl::start_with_next([=] {
- fail();
- }, _implementation->lifetime());
-
- _implementation->ready(
- ) | rpl::start_with_next([=](const QString &filepath) {
- unpack(filepath);
- }, _implementation->lifetime());
-
- QDir(internal::SetDataPath(_id)).removeRecursively();
- _implementation->start();
+Loader::Loader(
+ QObject *parent,
+ int id,
+ MTP::DedicatedLoader::Location location,
+ const QString &folder,
+ int size) : BlobLoader(parent, id, location, folder, size) {
}
void Loader::unpack(const QString &path) {
- const auto folder = internal::SetDataPath(_id);
+ const auto folder = internal::SetDataPath(id());
const auto weak = Ui::MakeWeak(this);
crl::async([=] {
if (UnpackSet(path, folder)) {
QFile(path).remove();
- SwitchToSet(_id, crl::guard(weak, [=](bool success) {
+ SwitchToSet(id(), crl::guard(weak, [=](bool success) {
if (success) {
destroy();
} else {
@@ -314,13 +184,6 @@ void Loader::unpack(const QString &path) {
});
}
-void Loader::finalize(const QString &path) {
-}
-
-void Loader::fail() {
- _state = Failed();
-}
-
void Loader::destroy() {
Expects(GlobalLoader == this);
@@ -334,8 +197,7 @@ Inner::Inner(QWidget *parent) : RpWidget(parent) {
void Inner::setupContent() {
const auto content = Ui::CreateChild(this);
- const auto sets = Sets();
- for (const auto &set : sets) {
+ for (const auto &set : kSets) {
content->add(object_ptr(content, set));
}
@@ -379,6 +241,9 @@ void Row::paintPreview(Painter &p) const {
}
void Row::paintRadio(Painter &p) {
+ if (_loading && !_loading->animating()) {
+ _loading = nullptr;
+ }
const auto loading = _loading
? _loading->computeState()
: Ui::RadialState{ 0., 0, FullArcLength };
@@ -536,7 +401,12 @@ void Row::setupHandler() {
}
void Row::load() {
- SetGlobalLoader(base::make_unique_q(App::main(), _id));
+ SetGlobalLoader(base::make_unique_q(
+ App::main(),
+ _id,
+ GetDownloadLocation(_id),
+ internal::SetDataPath(_id),
+ GetDownloadSize(_id)));
}
void Row::setupLabels(const Set &set) {
@@ -580,14 +450,20 @@ void Row::setupPreview(const Set &set) {
}
}
+void Row::updateLoadingToFinished() {
+ _loading->update(
+ _state.current().is() ? 0. : 1.,
+ true,
+ crl::now());
+}
+
void Row::radialAnimationCallback(crl::time now) {
const auto updated = [&] {
const auto state = _state.current();
if (const auto loading = base::get_if(&state)) {
- const auto progress = (loading->size > 0)
- ? (loading->already / float64(loading->size))
- : 0.;
- return _loading->update(progress, false, now);
+ return _loading->update(CountProgress(loading), false, now);
+ } else {
+ updateLoadingToFinished();
}
return false;
}();
@@ -636,15 +512,9 @@ void Row::setupAnimation() {
if (loading && !_loading) {
_loading = std::make_unique(
[=](crl::time now) { radialAnimationCallback(now); });
- const auto progress = (loading->size > 0)
- ? (loading->already / float64(loading->size))
- : 0.;
- _loading->start(progress);
+ _loading->start(CountProgress(loading));
} else if (!loading && _loading) {
- _loading->update(
- _state.current().is() ? 0. : 1.,
- true,
- crl::now());
+ updateLoadingToFinished();
}
}, lifetime());
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index 1b2d53083..215932831 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -280,17 +280,13 @@ void InitSpellchecker(
not_null session,
not_null field) {
#ifndef TDESKTOP_DISABLE_SPELLCHECK
- if (!Platform::Spellchecker::IsAvailable()) {
- return;
- }
const auto s = Ui::CreateChild(
field.get(),
- session->settings().spellcheckerEnabledValue());
- Spellchecker::SetPhrases({ {
- { &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },
- { &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },
- { &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },
- } });
+ session->settings().spellcheckerEnabledValue(),
+ Spellchecker::SpellingHighlighter::CustomContextMenuItem{
+ tr::lng_settings_manage_dictionaries(tr::now),
+ [=] { Ui::show(Box(session)); }
+ });
field->setExtendedContextMenu(s->contextMenuCreated());
#endif // TDESKTOP_DISABLE_SPELLCHECK
}
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h
index 8f95a9323..812ca859b 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.h
+++ b/Telegram/SourceFiles/chat_helpers/message_field.h
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qt_connection.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
+#include "boxes/dictionaries_manager.h"
#include "spellcheck/spelling_highlighter.h"
#endif // TDESKTOP_DISABLE_SPELLCHECK
diff --git a/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp b/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp
new file mode 100644
index 000000000..ee4b0d7c6
--- /dev/null
+++ b/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp
@@ -0,0 +1,447 @@
+/*
+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 "chat_helpers/spellchecker_common.h"
+
+#ifndef TDESKTOP_DISABLE_SPELLCHECK
+
+#include "base/platform/base_platform_info.h"
+#include "base/zlib_help.h"
+#include "data/data_session.h"
+#include "lang/lang_instance.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "mainwidget.h"
+#include "spellcheck/platform/platform_spellcheck.h"
+#include "spellcheck/spellcheck_utils.h"
+#include "spellcheck/spellcheck_value.h"
+
+#include
+#include
+
+namespace Spellchecker {
+
+namespace {
+
+using namespace Storage::CloudBlob;
+
+constexpr auto kDictExtensions = { "dic", "aff" };
+
+// 31 - QLocale::English, 91 - QLocale::Portuguese.
+constexpr auto kLangsForLWC = { 31, 91 };
+// 225 - QLocale::UnitesStates, 30 - QLocale::Brazil.
+constexpr auto kDefaultCountries = { 225, 30 };
+
+// Language With Country.
+inline auto LWC(QLocale::Country country) {
+ const auto l = QLocale::matchingLocales(
+ QLocale::AnyLanguage,
+ QLocale::AnyScript,
+ country)[0];
+ if (ranges::contains(kDefaultCountries, country)) {
+ return int(l.language());
+ }
+ return (l.language() * 1000) + country;
+}
+
+inline auto LanguageFromLocale(QLocale loc) {
+ const auto locLang = int(loc.language());
+ return (ranges::contains(kLangsForLWC, locLang)
+ && (loc.country() != QLocale::AnyCountry))
+ ? LWC(loc.country())
+ : locLang;
+}
+
+const auto kDictionaries = {
+ Dict{{ QLocale::English, 649, 174'516, "English" }}, // en_US
+ Dict{{ QLocale::Bulgarian, 594, 229'658, "\xd0\x91\xd1\x8a\xd0\xbb\xd0\xb3\xd0\xb0\xd1\x80\xd1\x81\xd0\xba\xd0\xb8" }}, // bg_BG
+ Dict{{ QLocale::Catalan, 595, 417'611, "\x43\x61\x74\x61\x6c\xc3\xa0" }}, // ca_ES
+ Dict{{ QLocale::Czech, 596, 860'286, "\xc4\x8c\x65\xc5\xa1\x74\x69\x6e\x61" }}, // cs_CZ
+ Dict{{ QLocale::Welsh, 597, 177'305, "\x43\x79\x6d\x72\x61\x65\x67" }}, // cy_GB
+ Dict{{ QLocale::Danish, 598, 345'874, "\x44\x61\x6e\x73\x6b" }}, // da_DK
+ Dict{{ QLocale::German, 599, 2'412'780, "\x44\x65\x75\x74\x73\x63\x68" }}, // de_DE
+ Dict{{ QLocale::Greek, 600, 1'389'160, "\xce\x95\xce\xbb\xce\xbb\xce\xb7\xce\xbd\xce\xb9\xce\xba\xce\xac" }}, // el_GR
+ Dict{{ LWC(QLocale::Australia), 601, 175'266, "English (Australia)" }}, // en_AU
+ Dict{{ LWC(QLocale::Canada), 602, 174'295, "English (Canada)" }}, // en_CA
+ Dict{{ LWC(QLocale::UnitedKingdom), 603, 174'433, "English (United Kingdom)" }}, // en_GB
+ Dict{{ QLocale::Spanish, 604, 264'717, "\x45\x73\x70\x61\xc3\xb1\x6f\x6c" }}, // es_ES
+ Dict{{ QLocale::Estonian, 605, 757'394, "\x45\x65\x73\x74\x69" }}, // et_EE
+ Dict{{ QLocale::Persian, 606, 333'911, "\xd9\x81\xd8\xa7\xd8\xb1\xd8\xb3\xdb\x8c" }}, // fa_IR
+ Dict{{ QLocale::French, 607, 321'391, "\x46\x72\x61\x6e\xc3\xa7\x61\x69\x73" }}, // fr_FR
+ Dict{{ QLocale::Hebrew, 608, 622'550, "\xd7\xa2\xd7\x91\xd7\xa8\xd7\x99\xd7\xaa" }}, // he_IL
+ Dict{{ QLocale::Hindi, 609, 56'105, "\xe0\xa4\xb9\xe0\xa4\xbf\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x80" }}, // hi_IN
+ Dict{{ QLocale::Croatian, 610, 668'876, "\x48\x72\x76\x61\x74\x73\x6b\x69" }}, // hr_HR
+ Dict{{ QLocale::Hungarian, 611, 660'402, "\x4d\x61\x67\x79\x61\x72" }}, // hu_HU
+ Dict{{ QLocale::Armenian, 612, 928'746, "\xd5\x80\xd5\xa1\xd5\xb5\xd5\xa5\xd6\x80\xd5\xa5\xd5\xb6" }}, // hy_AM
+ Dict{{ QLocale::Indonesian, 613, 100'134, "\x49\x6e\x64\x6f\x6e\x65\x73\x69\x61" }}, // id_ID
+ Dict{{ QLocale::Italian, 614, 324'613, "\x49\x74\x61\x6c\x69\x61\x6e\x6f" }}, // it_IT
+ Dict{{ QLocale::Korean, 615, 1'256'987, "\xed\x95\x9c\xea\xb5\xad\xec\x96\xb4" }}, // ko_KR
+ Dict{{ QLocale::Lithuanian, 616, 267'427, "\x4c\x69\x65\x74\x75\x76\x69\xc5\xb3" }}, // lt_LT
+ Dict{{ QLocale::Latvian, 617, 641'602, "\x4c\x61\x74\x76\x69\x65\xc5\xa1\x75" }}, // lv_LV
+ Dict{{ QLocale::Norwegian, 618, 588'650, "\x4e\x6f\x72\x73\x6b" }}, // nb_NO
+ Dict{{ QLocale::Dutch, 619, 743'406, "\x4e\x65\x64\x65\x72\x6c\x61\x6e\x64\x73" }}, // nl_NL
+ Dict{{ QLocale::Polish, 620, 1'015'747, "\x50\x6f\x6c\x73\x6b\x69" }}, // pl_PL
+ Dict{{ QLocale::Portuguese, 621, 1'231'999, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73 (Brazil)" }}, // pt_BR
+ Dict{{ LWC(QLocale::Portugal), 622, 138'571, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73" }}, // pt_PT
+ Dict{{ QLocale::Romanian, 623, 455'643, "\x52\x6f\x6d\xc3\xa2\x6e\xc4\x83" }}, // ro_RO
+ Dict{{ QLocale::Russian, 624, 463'194, "\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xb8\xd0\xb9" }}, // ru_RU
+ Dict{{ QLocale::Slovak, 625, 525'328, "\x53\x6c\x6f\x76\x65\x6e\xc4\x8d\x69\x6e\x61" }}, // sk_SK
+ Dict{{ QLocale::Slovenian, 626, 1'143'710, "\x53\x6c\x6f\x76\x65\x6e\xc5\xa1\xc4\x8d\x69\x6e\x61" }}, // sl_SI
+ Dict{{ QLocale::Albanian, 627, 583'412, "\x53\x68\x71\x69\x70" }}, // sq_AL
+ Dict{{ QLocale::Swedish, 628, 593'877, "\x53\x76\x65\x6e\x73\x6b\x61" }}, // sv_SE
+ Dict{{ QLocale::Tamil, 629, 323'193, "\xe0\xae\xa4\xe0\xae\xae\xe0\xae\xbf\xe0\xae\xb4\xe0\xaf\x8d" }}, // ta_IN
+ Dict{{ QLocale::Tajik, 630, 369'931, "\xd0\xa2\xd0\xbe\xd2\xb7\xd0\xb8\xd0\xba\xd3\xa3" }}, // tg_TG
+ Dict{{ QLocale::Turkish, 631, 4'301'099, "\x54\xc3\xbc\x72\x6b\xc3\xa7\x65" }}, // tr_TR
+ Dict{{ QLocale::Ukrainian, 632, 445'711, "\xd0\xa3\xd0\xba\xd1\x80\xd0\xb0\xd1\x97\xd0\xbd\xd1\x81\xd1\x8c\xd0\xba\xd0\xb0" }}, // uk_UA
+ Dict{{ QLocale::Vietnamese, 633, 12'949, "\x54\x69\xe1\xba\xbf\x6e\x67\x20\x56\x69\xe1\xbb\x87\x74" }}, // vi_VN
+ // The Tajik code is 'tg_TG' in Chromium, but QT has only 'tg_TJ'.
+};
+
+inline auto IsSupportedLang(int lang) {
+ return ranges::contains(kDictionaries, lang, &Dict::id);
+}
+
+void EnsurePath() {
+ if (!QDir::current().mkpath(Spellchecker::DictionariesPath())) {
+ LOG(("App Error: Could not create dictionaries path."));
+ }
+}
+
+bool IsGoodPartName(const QString &name) {
+ return ranges::find_if(kDictExtensions, [&](const auto &ext) {
+ return name.endsWith(ext);
+ }) != end(kDictExtensions);
+}
+
+using DictLoaderPtr = std::shared_ptr>;
+
+DictLoaderPtr BackgroundLoader;
+rpl::event_stream BackgroundLoaderChanged;
+
+void SetBackgroundLoader(DictLoaderPtr loader) {
+ BackgroundLoader = std::move(loader);
+}
+
+void DownloadDictionaryInBackground(
+ not_null session,
+ int counter,
+ std::vector langs) {
+ const auto id = langs[counter];
+ counter++;
+ const auto destroyer = [=] {
+ // This is a temporary workaround.
+ const auto copyId = id;
+ const auto copyLangs = langs;
+ const auto copySession = session;
+ const auto copyCounter = counter;
+ BackgroundLoader = nullptr;
+ BackgroundLoaderChanged.fire(0);
+
+ if (DictionaryExists(copyId)) {
+ auto dicts = copySession->settings().dictionariesEnabled();
+ if (!ranges::contains(dicts, copyId)) {
+ dicts.push_back(copyId);
+ copySession->settings().setDictionariesEnabled(std::move(dicts));
+ copySession->saveSettingsDelayed();
+ }
+ }
+
+ if (copyCounter >= copyLangs.size()) {
+ return;
+ }
+ DownloadDictionaryInBackground(copySession, copyCounter, copyLangs);
+ };
+ if (DictionaryExists(id)) {
+ destroyer();
+ return;
+ }
+
+ auto sharedLoader = std::make_shared>();
+ *sharedLoader = base::make_unique_q(
+ App::main(),
+ id,
+ GetDownloadLocation(id),
+ DictPathByLangId(id),
+ GetDownloadSize(id),
+ crl::guard(session, destroyer));
+ SetBackgroundLoader(std::move(sharedLoader));
+ BackgroundLoaderChanged.fire_copy(id);
+}
+
+} // namespace
+
+DictLoaderPtr GlobalLoader() {
+ return BackgroundLoader;
+}
+
+rpl::producer GlobalLoaderChanged() {
+ return BackgroundLoaderChanged.events();
+}
+
+DictLoader::DictLoader(
+ QObject *parent,
+ int id,
+ MTP::DedicatedLoader::Location location,
+ const QString &folder,
+ int size,
+ Fn destroyCallback)
+: BlobLoader(parent, id, location, folder, size)
+, _destroyCallback(std::move(destroyCallback)) {
+}
+
+void DictLoader::unpack(const QString &path) {
+ Expects(_destroyCallback);
+ crl::async([=] {
+ const auto success = Spellchecker::UnpackDictionary(path, id());
+ if (success) {
+ QFile(path).remove();
+ }
+ crl::on_main(success ? _destroyCallback : [=] { fail(); });
+ });
+}
+
+void DictLoader::destroy() {
+ Expects(_destroyCallback);
+
+ _destroyCallback();
+}
+
+void DictLoader::fail() {
+ BlobLoader::fail();
+ destroy();
+}
+
+std::vector Dictionaries() {
+ return kDictionaries | ranges::to_vector;
+}
+
+int GetDownloadSize(int id) {
+ return ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size;
+}
+
+MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
+ const auto username = kCloudLocationUsername.utf16();
+ const auto i = ranges::find(kDictionaries, id, &Spellchecker::Dict::id);
+ return MTP::DedicatedLoader::Location{ username, i->postId };
+}
+
+QString DictPathByLangId(int langId) {
+ EnsurePath();
+ return qsl("%1/%2")
+ .arg(DictionariesPath())
+ .arg(Spellchecker::LocaleFromLangId(langId).name());
+}
+
+QString DictionariesPath() {
+ return cWorkingDir() + qsl("tdata/dictionaries");
+}
+
+bool UnpackDictionary(const QString &path, int langId) {
+ const auto folder = DictPathByLangId(langId);
+ return UnpackBlob(path, folder, IsGoodPartName);
+}
+
+bool DictionaryExists(int langId) {
+ if (!langId) {
+ return true;
+ }
+ const auto folder = DictPathByLangId(langId) + '/';
+ const auto bad = ranges::find_if(kDictExtensions, [&](const auto &ext) {
+ const auto name = Spellchecker::LocaleFromLangId(langId).name();
+ return !QFile(folder + name + '.' + ext).exists();
+ });
+ return (bad == end(kDictExtensions));
+}
+
+bool RemoveDictionary(int langId) {
+ if (!langId) {
+ return true;
+ }
+ const auto fileName = Spellchecker::LocaleFromLangId(langId).name();
+ const auto folder = qsl("%1/%2/")
+ .arg(DictionariesPath())
+ .arg(fileName);
+ return QDir(folder).removeRecursively();
+}
+
+bool WriteDefaultDictionary() {
+ // This is an unused function.
+ const auto en = QLocale::English;
+ if (DictionaryExists(en)) {
+ return false;
+ }
+ const auto fileName = QLocale(en).name();
+ const auto folder = qsl("%1/%2/")
+ .arg(DictionariesPath())
+ .arg(fileName);
+ QDir(folder).removeRecursively();
+
+ const auto path = folder + fileName;
+ QDir().mkpath(folder);
+ auto input = QFile(qsl(":/misc/en_US_dictionary"));
+ auto output = QFile(path);
+ if (input.open(QIODevice::ReadOnly)
+ && output.open(QIODevice::WriteOnly)) {
+ output.write(input.readAll());
+ const auto result = Spellchecker::UnpackDictionary(path, en);
+ output.remove();
+ return result;
+ }
+ return false;
+}
+
+rpl::producer ButtonManageDictsState(
+ not_null session) {
+ if (Platform::Spellchecker::IsSystemSpellchecker()) {
+ return rpl::single(QString());
+ }
+ const auto computeString = [=] {
+ if (!session->settings().spellcheckerEnabled()) {
+ return QString();
+ }
+ if (!session->settings().dictionariesEnabled().size()) {
+ return QString();
+ }
+ const auto dicts = session->settings().dictionariesEnabled();
+ const auto filtered = ranges::view::all(
+ dicts
+ ) | ranges::views::filter(
+ DictionaryExists
+ ) | ranges::to_vector;
+ const auto active = Platform::Spellchecker::ActiveLanguages();
+
+ return (active.size() == filtered.size())
+ ? QString::number(filtered.size())
+ : tr::lng_contacts_loading(tr::now);
+ };
+ const auto emptyValue = [] { return rpl::empty_value(); };
+ return rpl::single(
+ computeString()
+ ) | rpl::then(
+ rpl::merge(
+ Spellchecker::SupportedScriptsChanged(),
+ session->settings().dictionariesEnabledChanges(
+ ) | rpl::map(emptyValue),
+ session->settings().spellcheckerEnabledChanges(
+ ) | rpl::map(emptyValue)
+ ) | rpl::map(computeString)
+ );
+}
+
+std::vector DefaultLanguages() {
+ std::vector langs;
+
+ const auto append = [&](const auto loc) {
+ const auto l = LanguageFromLocale(loc);
+ if (!ranges::contains(langs, l) && IsSupportedLang(l)) {
+ langs.push_back(l);
+ }
+ };
+
+ const auto method = QGuiApplication::inputMethod();
+ langs.reserve(method ? 3 : 2);
+ if (method) {
+ append(method->locale());
+ }
+ append(QLocale(Platform::SystemLanguage()));
+ append(QLocale(Lang::LanguageIdOrDefault(Lang::Current().id())));
+
+ return langs;
+}
+
+void Start(not_null session) {
+ Spellchecker::SetPhrases({ {
+ { &ph::lng_spellchecker_submenu, tr::lng_spellchecker_submenu() },
+ { &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },
+ { &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },
+ { &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },
+ } });
+ const auto settings = &session->settings();
+
+ const auto guard = gsl::finally([=]{
+ if (settings->spellcheckerEnabled()) {
+ Platform::Spellchecker::UpdateLanguages(
+ settings->dictionariesEnabled());
+ }
+ });
+
+ if (Platform::Spellchecker::IsSystemSpellchecker()) {
+ return;
+ }
+
+ Spellchecker::SetWorkingDirPath(DictionariesPath());
+
+ settings->dictionariesEnabledChanges(
+ ) | rpl::start_with_next([](auto dictionaries) {
+ Platform::Spellchecker::UpdateLanguages(dictionaries);
+ }, session->lifetime());
+
+ settings->spellcheckerEnabledChanges(
+ ) | rpl::start_with_next([=](auto enabled) {
+ Platform::Spellchecker::UpdateLanguages(
+ enabled
+ ? settings->dictionariesEnabled()
+ : std::vector());
+ }, session->lifetime());
+
+ const auto method = QGuiApplication::inputMethod();
+
+ const auto connectInput = [=] {
+ if (!method || !settings->spellcheckerEnabled()) {
+ return;
+ }
+ auto callback = [=] {
+ if (BackgroundLoader) {
+ return;
+ }
+ const auto l = LanguageFromLocale(method->locale());
+ if (!IsSupportedLang(l) || DictionaryExists(l)) {
+ return;
+ }
+ crl::on_main(session, [=] {
+ DownloadDictionaryInBackground(session, 0, { l });
+ });
+ };
+ QObject::connect(
+ method,
+ &QInputMethod::localeChanged,
+ std::move(callback));
+ };
+
+ if (settings->autoDownloadDictionaries()) {
+ session->data().contactsLoaded().changes(
+ ) | rpl::start_with_next([=](bool loaded) {
+ if (!loaded) {
+ return;
+ }
+
+ DownloadDictionaryInBackground(session, 0, DefaultLanguages());
+ }, session->lifetime());
+
+ connectInput();
+ }
+
+ rpl::combine(
+ settings->spellcheckerEnabledValue(),
+ settings->autoDownloadDictionariesValue()
+ ) | rpl::start_with_next([=](bool spell, bool download) {
+ if (spell && download) {
+ connectInput();
+ return;
+ }
+ QObject::disconnect(
+ method,
+ &QInputMethod::localeChanged,
+ nullptr,
+ nullptr);
+ }, session->lifetime());
+
+}
+
+} // namespace Spellchecker
+
+#endif // !TDESKTOP_DISABLE_SPELLCHECK
diff --git a/Telegram/SourceFiles/chat_helpers/spellchecker_common.h b/Telegram/SourceFiles/chat_helpers/spellchecker_common.h
new file mode 100644
index 000000000..203c4e047
--- /dev/null
+++ b/Telegram/SourceFiles/chat_helpers/spellchecker_common.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
+
+#ifndef TDESKTOP_DISABLE_SPELLCHECK
+
+#include "storage/storage_cloud_blob.h"
+#include "base/unique_qptr.h"
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Spellchecker {
+
+struct Dict : public Storage::CloudBlob::Blob {
+};
+
+int GetDownloadSize(int id);
+MTP::DedicatedLoader::Location GetDownloadLocation(int id);
+
+[[nodiscard]] QString DictionariesPath();
+[[nodiscard]] QString DictPathByLangId(int langId);
+bool UnpackDictionary(const QString &path, int langId);
+[[nodiscard]] bool DictionaryExists(int langId);
+bool RemoveDictionary(int langId);
+[[nodiscard]] bool IsEn(int langId);
+
+bool WriteDefaultDictionary();
+std::vector Dictionaries();
+
+void Start(not_null session);
+[[nodiscard]] rpl::producer ButtonManageDictsState(
+ not_null session);
+
+std::vector DefaultLanguages();
+
+class DictLoader : public Storage::CloudBlob::BlobLoader {
+public:
+ DictLoader(
+ QObject *parent,
+ int id,
+ MTP::DedicatedLoader::Location location,
+ const QString &folder,
+ int size,
+ Fn destroyCallback);
+
+ void destroy() override;
+
+ rpl::lifetime &lifetime() {
+ return _lifetime;
+ }
+
+private:
+ void unpack(const QString &path) override;
+ void fail() override;
+
+ Fn _destroyCallback;
+
+ rpl::lifetime _lifetime;
+
+};
+
+std::shared_ptr> GlobalLoader();
+rpl::producer GlobalLoaderChanged();
+
+} // namespace Spellchecker
+
+#endif // !TDESKTOP_DISABLE_SPELLCHECK
diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index 09ef3cf96..cb3c40911 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -230,10 +230,6 @@ void Application::run() {
_window = std::make_unique(&activeAccount());
- const auto currentGeometry = _window->widget()->geometry();
- _mediaView = std::make_unique();
- _window->widget()->setGeometry(currentGeometry);
-
QCoreApplication::instance()->installEventFilter(this);
connect(
static_cast(QCoreApplication::instance()),
@@ -262,8 +258,15 @@ void Application::run() {
_window->setupIntro();
}
}
+
+ _window->widget()->show();
+
+ const auto currentGeometry = _window->widget()->geometry();
+ _mediaView = std::make_unique();
+ _window->widget()->setGeometry(currentGeometry);
+
DEBUG_LOG(("Application Info: showing."));
- _window->firstShow();
+ _window->finishFirstShow();
if (!locked() && cStartToSettings()) {
_window->showSettings();
@@ -297,6 +300,8 @@ void Application::showPhoto(not_null link) {
}
void Application::showPhoto(not_null photo, HistoryItem *item) {
+ Expects(_mediaView != nullptr);
+
_mediaView->showPhoto(photo, item);
_mediaView->activateWindow();
_mediaView->setFocus();
@@ -305,12 +310,16 @@ void Application::showPhoto(not_null photo, HistoryItem *item) {
void Application::showPhoto(
not_null photo,
not_null peer) {
+ Expects(_mediaView != nullptr);
+
_mediaView->showPhoto(photo, peer);
_mediaView->activateWindow();
_mediaView->setFocus();
}
void Application::showDocument(not_null document, HistoryItem *item) {
+ Expects(_mediaView != nullptr);
+
if (cUseExternalVideoPlayer()
&& document->isVideoFile()
&& document->loaded()) {
@@ -325,6 +334,8 @@ void Application::showDocument(not_null document, HistoryItem *it
void Application::showTheme(
not_null document,
const Data::CloudTheme &cloud) {
+ Expects(_mediaView != nullptr);
+
_mediaView->showTheme(document, cloud);
_mediaView->activateWindow();
_mediaView->setFocus();
diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp
index c8c711ebb..783dd333f 100644
--- a/Telegram/SourceFiles/core/changelogs.cpp
+++ b/Telegram/SourceFiles/core/changelogs.cpp
@@ -55,6 +55,20 @@ std::map BetaLogs() {
"\xE2\x80\xA2 Rotate photos and videos in the media viewer "
"using the rotate button in the bottom right corner.\n"
+ },
+ {
+ 1009015,
+ "\xE2\x80\xA2 Mark new messages as read "
+ "while scrolling down through them.\n"
+
+ "\xE2\x80\xA2 Bug fixes and other minor improvements."
+ },
+
+ {
+ 1009017,
+ "\xE2\x80\xA2 Spell checker on Windows 7.\n"
+
+ "\xE2\x80\xA2 Bug fixes and other minor improvements."
}
};
};
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 93be97e10..4f9c505ac 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -22,8 +22,8 @@ constexpr auto AppId = "{C4A4AE8F-B9F7-4CC7-8A6C-BF7EEE87ACA5}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Kotatogram Desktop"_cs;
constexpr auto AppFile = "Kotatogram"_cs;
-constexpr auto AppVersion = 1009010;
-constexpr auto AppVersionStr = "1.9.10";
+constexpr auto AppVersion = 1009019;
+constexpr auto AppVersionStr = "1.9.19";
constexpr auto AppBetaVersion = true;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
constexpr auto AppKotatoVersion = 1001007;
diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp
index d002653af..2eae51e78 100644
--- a/Telegram/SourceFiles/data/data_channel.cpp
+++ b/Telegram/SourceFiles/data/data_channel.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_location.h"
+#include "data/data_histories.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "history/history.h"
@@ -709,13 +710,14 @@ void ApplyChannelUpdate(
const auto folder = folderId
? channel->owner().folderLoaded(folderId)
: nullptr;
+ auto &histories = channel->owner().histories();
if (folder && history->folder() != folder) {
// If history folder is unknown or not synced, request both.
- channel->session().api().requestDialogEntry(history);
- channel->session().api().requestDialogEntry(folder);
+ histories.requestDialogEntry(history);
+ histories.requestDialogEntry(folder);
} else if (!history->folderKnown()
|| channel->pts() != update.vpts().v) {
- channel->session().api().requestDialogEntry(history);
+ histories.requestDialogEntry(history);
} else {
history->applyDialogFields(
history->folder(),
@@ -761,6 +763,9 @@ void ApplyChannelUpdate(
channel->session().api().applyNotifySettings(
MTP_inputNotifyPeer(channel->input),
update.vnotify_settings());
+
+ // For clearUpTill() call.
+ channel->owner().sendHistoryChangeNotifications();
}
void ApplyMegagroupAdmins(
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 7aeb0d124..56b6d5ff6 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -569,7 +569,7 @@ void DocumentData::validateLottieSticker() {
void DocumentData::setDataAndCache(const QByteArray &data) {
setData(data);
if (saveToCache() && data.size() <= Storage::kMaxFileInMemory) {
- session().data().cache().put(
+ owner().cache().put(
cacheKey(),
Storage::Cache::Database::TaggedValue(
base::duplicate(data),
diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp
index b42ac116c..5229de94b 100644
--- a/Telegram/SourceFiles/data/data_folder.cpp
+++ b/Telegram/SourceFiles/data/data_folder.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_channel.h"
+#include "data/data_histories.h"
#include "dialogs/dialogs_key.h"
#include "history/history.h"
#include "history/history_item.h"
@@ -108,7 +109,7 @@ void Folder::registerOne(not_null history) {
if (_chatsList.indexed()->size() == 1) {
updateChatListSortPosition();
if (!_cloudUnread.known) {
- session().api().requestDialogEntry(this);
+ owner().histories().requestDialogEntry(this);
}
} else {
updateChatListEntry();
@@ -323,7 +324,7 @@ uint32 Folder::chatListViewVersion() const {
void Folder::requestChatListMessage() {
if (!chatListMessageKnown()) {
- session().api().requestDialogEntry(this);
+ owner().histories().requestDialogEntry(this);
}
}
diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp
new file mode 100644
index 000000000..ee3da9421
--- /dev/null
+++ b/Telegram/SourceFiles/data/data_histories.cpp
@@ -0,0 +1,683 @@
+/*
+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/data_histories.h"
+
+#include "data/data_session.h"
+#include "data/data_channel.h"
+#include "data/data_folder.h"
+#include "main/main_session.h"
+#include "window/notifications_manager.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/view/history_view_element.h"
+#include "core/application.h"
+#include "apiwrap.h"
+
+namespace Data {
+namespace {
+
+constexpr auto kReadRequestTimeout = 3 * crl::time(1000);
+
+} // namespace
+
+Histories::Histories(not_null owner)
+: _owner(owner)
+, _readRequestsTimer([=] { sendReadRequests(); }) {
+}
+
+Session &Histories::owner() const {
+ return *_owner;
+}
+
+Main::Session &Histories::session() const {
+ return _owner->session();
+}
+
+History *Histories::find(PeerId peerId) {
+ const auto i = peerId ? _map.find(peerId) : end(_map);
+ return (i != end(_map)) ? i->second.get() : nullptr;
+}
+
+not_null Histories::findOrCreate(PeerId peerId) {
+ Expects(peerId != 0);
+
+ if (const auto result = find(peerId)) {
+ return result;
+ }
+ const auto [i, ok] = _map.emplace(
+ peerId,
+ std::make_unique(&owner(), peerId));
+ return i->second.get();
+}
+
+void Histories::unloadAll() {
+ for (const auto &[peerId, history] : _map) {
+ history->clear(History::ClearType::Unload);
+ }
+}
+
+void Histories::clearAll() {
+ _map.clear();
+}
+
+void Histories::readInbox(not_null history) {
+ if (history->lastServerMessageKnown()) {
+ const auto last = history->lastServerMessage();
+ readInboxTill(history, last ? last->id : 0);
+ return;
+ } else if (history->loadedAtBottom()) {
+ if (const auto lastId = history->maxMsgId()) {
+ readInboxTill(history, lastId);
+ return;
+ } else if (history->loadedAtTop()) {
+ readInboxTill(history, 0);
+ return;
+ }
+ }
+ requestDialogEntry(history, [=] {
+ Expects(history->lastServerMessageKnown());
+
+ const auto last = history->lastServerMessage();
+ readInboxTill(history, last ? last->id : 0);
+ });
+}
+
+void Histories::readInboxTill(not_null item) {
+ const auto history = item->history();
+ if (!IsServerMsgId(item->id)) {
+ readClientSideMessage(item);
+ auto view = item->mainView();
+ if (!view) {
+ return;
+ }
+ auto block = view->block();
+ auto blockIndex = block->indexInHistory();
+ auto itemIndex = view->indexInBlock();
+ while (blockIndex > 0 || itemIndex > 0) {
+ if (itemIndex > 0) {
+ view = block->messages[--itemIndex].get();
+ } else {
+ while (blockIndex > 0) {
+ block = history->blocks[--blockIndex].get();
+ itemIndex = block->messages.size();
+ if (itemIndex > 0) {
+ view = block->messages[--itemIndex].get();
+ break;
+ }
+ }
+ }
+ item = view->data();
+ if (IsServerMsgId(item->id)) {
+ break;
+ }
+ }
+ if (!IsServerMsgId(item->id)) {
+ LOG(("App Error: "
+ "Can't read history till unknown local message."));
+ return;
+ }
+ }
+ readInboxTill(history, item->id);
+}
+
+void Histories::readInboxTill(not_null history, MsgId tillId) {
+ readInboxTill(history, tillId, false);
+}
+
+void Histories::readInboxTill(
+ not_null history,
+ MsgId tillId,
+ bool force) {
+ Expects(IsServerMsgId(tillId) || (!tillId && !force));
+
+ const auto syncGuard = gsl::finally([&] {
+ if (history->unreadCount() > 0) {
+ if (const auto last = history->lastServerMessage()) {
+ if (last->id == tillId) {
+ history->setUnreadCount(0);
+ history->updateChatListEntry();
+ }
+ }
+ }
+ });
+
+ history->session().notifications().clearIncomingFromHistory(history);
+
+ const auto needsRequest = history->readInboxTillNeedsRequest(tillId);
+ if (!needsRequest && !force) {
+ return;
+ } else if (!history->trackUnreadMessages()) {
+ return;
+ }
+ const auto maybeState = lookup(history);
+ if (maybeState && maybeState->sentReadTill >= tillId) {
+ return;
+ } else if (maybeState && maybeState->willReadTill >= tillId) {
+ if (force) {
+ sendPendingReadInbox(history);
+ }
+ return;
+ } else if (!needsRequest
+ && (!maybeState || !maybeState->willReadTill)) {
+ return;
+ }
+ const auto stillUnread = history->countStillUnreadLocal(tillId);
+ if (!force
+ && stillUnread
+ && history->unreadCountKnown()
+ && *stillUnread == history->unreadCount()) {
+ history->setInboxReadTill(tillId);
+ return;
+ }
+ auto &state = maybeState ? *maybeState : _states[history];
+ state.willReadTill = tillId;
+ if (force || !stillUnread || !*stillUnread) {
+ state.willReadWhen = 0;
+ sendReadRequests();
+ if (!stillUnread) {
+ return;
+ }
+ } else if (!state.willReadWhen) {
+ state.willReadWhen = crl::now() + kReadRequestTimeout;
+ if (!_readRequestsTimer.isActive()) {
+ _readRequestsTimer.callOnce(kReadRequestTimeout);
+ }
+ }
+ history->setInboxReadTill(tillId);
+ history->setUnreadCount(*stillUnread);
+ history->updateChatListEntry();
+}
+
+void Histories::readInboxOnNewMessage(not_null item) {
+ if (!IsServerMsgId(item->id)) {
+ readClientSideMessage(item);
+ } else {
+ readInboxTill(item->history(), item->id, true);
+ }
+}
+
+void Histories::readClientSideMessage(not_null item) {
+ if (item->out() || !item->unread()) {
+ return;
+ }
+ const auto history = item->history();
+ item->markClientSideAsRead();
+ if (const auto unread = history->unreadCount()) {
+ history->setUnreadCount(unread - 1);
+ }
+}
+
+void Histories::requestDialogEntry(not_null folder) {
+ if (_dialogFolderRequests.contains(folder)) {
+ return;
+ }
+ _dialogFolderRequests.emplace(folder);
+
+ auto peers = QVector(
+ 1,
+ MTP_inputDialogPeerFolder(MTP_int(folder->id())));
+ session().api().request(MTPmessages_GetPeerDialogs(
+ MTP_vector(std::move(peers))
+ )).done([=](const MTPmessages_PeerDialogs &result) {
+ applyPeerDialogs(result);
+ _dialogFolderRequests.remove(folder);
+ }).fail([=](const RPCError &error) {
+ _dialogFolderRequests.remove(folder);
+ }).send();
+}
+
+void Histories::requestDialogEntry(
+ not_null history,
+ Fn callback) {
+ const auto i = _dialogRequests.find(history);
+ if (i != end(_dialogRequests)) {
+ if (callback) {
+ i->second.push_back(std::move(callback));
+ }
+ return;
+ }
+
+ const auto [j, ok] = _dialogRequestsPending.try_emplace(history);
+ if (callback) {
+ j->second.push_back(std::move(callback));
+ }
+ if (!ok) {
+ return;
+ }
+ postponeRequestDialogEntries();
+}
+
+void Histories::postponeRequestDialogEntries() {
+ if (_dialogRequestsPending.size() > 1) {
+ return;
+ }
+ Core::App().postponeCall(crl::guard(&session(), [=] {
+ sendDialogRequests();
+ }));
+}
+
+void Histories::sendDialogRequests() {
+ if (_dialogRequestsPending.empty()) {
+ return;
+ }
+ const auto histories = ranges::view::all(
+ _dialogRequestsPending
+ ) | ranges::view::transform([](const auto &pair) {
+ return pair.first;
+ }) | ranges::view::filter([&](not_null history) {
+ const auto state = lookup(history);
+ if (!state) {
+ return true;
+ } else if (!postponeEntryRequest(*state)) {
+ return true;
+ }
+ state->postponedRequestEntry = true;
+ return false;
+ }) | ranges::to_vector;
+
+ auto peers = QVector();
+ const auto dialogPeer = [](not_null history) {
+ return MTP_inputDialogPeer(history->peer->input);
+ };
+ ranges::transform(
+ histories,
+ ranges::back_inserter(peers),
+ dialogPeer);
+ for (auto &[history, callbacks] : base::take(_dialogRequestsPending)) {
+ _dialogRequests.emplace(history, std::move(callbacks));
+ }
+
+ const auto finalize = [=] {
+ for (const auto history : histories) {
+ const auto state = lookup(history);
+ if (!state || !state->postponedRequestEntry) {
+ dialogEntryApplied(history);
+ history->updateChatListExistence();
+ }
+ }
+ };
+ session().api().request(MTPmessages_GetPeerDialogs(
+ MTP_vector(std::move(peers))
+ )).done([=](const MTPmessages_PeerDialogs &result) {
+ applyPeerDialogs(result);
+ finalize();
+ }).fail([=](const RPCError &error) {
+ finalize();
+ }).send();
+}
+
+void Histories::dialogEntryApplied(not_null history) {
+ const auto state = lookup(history);
+ if (state && state->postponedRequestEntry) {
+ return;
+ }
+ history->dialogEntryApplied();
+ if (const auto callbacks = _dialogRequestsPending.take(history)) {
+ for (const auto &callback : *callbacks) {
+ callback();
+ }
+ }
+ if (const auto callbacks = _dialogRequests.take(history)) {
+ for (const auto &callback : *callbacks) {
+ callback();
+ }
+ }
+ if (state && state->sentReadTill && state->sentReadDone) {
+ history->setInboxReadTill(base::take(state->sentReadTill));
+ checkEmptyState(history);
+ }
+}
+
+void Histories::applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs) {
+ Expects(dialogs.type() == mtpc_messages_peerDialogs);
+
+ const auto &data = dialogs.c_messages_peerDialogs();
+ _owner->processUsers(data.vusers());
+ _owner->processChats(data.vchats());
+ _owner->processMessages(data.vmessages(), NewMessageType::Last);
+ for (const auto &dialog : data.vdialogs().v) {
+ dialog.match([&](const MTPDdialog &data) {
+ if (const auto peerId = peerFromMTP(data.vpeer())) {
+ _owner->history(peerId)->applyDialog(nullptr, data);
+ }
+ }, [&](const MTPDdialogFolder &data) {
+ const auto folder = _owner->processFolder(data.vfolder());
+ folder->applyDialog(data);
+ });
+ }
+ _owner->sendHistoryChangeNotifications();
+}
+
+void Histories::changeDialogUnreadMark(
+ not_null history,
+ bool unread) {
+ history->setUnreadMark(unread);
+
+ using Flag = MTPmessages_MarkDialogUnread::Flag;
+ session().api().request(MTPmessages_MarkDialogUnread(
+ MTP_flags(unread ? Flag::f_unread : Flag(0)),
+ MTP_inputDialogPeer(history->peer->input)
+ )).send();
+}
+
+void Histories::requestFakeChatListMessage(
+ not_null history) {
+ if (_fakeChatListRequests.contains(history)) {
+ return;
+ }
+
+ _fakeChatListRequests.emplace(history);
+ sendRequest(history, RequestType::History, [=](Fn finish) {
+ return session().api().request(MTPmessages_GetHistory(
+ history->peer->input,
+ MTP_int(0), // offset_id
+ MTP_int(0), // offset_date
+ MTP_int(0), // add_offset
+ MTP_int(2), // limit
+ MTP_int(0), // max_id
+ MTP_int(0), // min_id
+ MTP_int(0)
+ )).done([=](const MTPmessages_Messages &result) {
+ _fakeChatListRequests.erase(history);
+ history->setFakeChatListMessageFrom(result);
+ finish();
+ }).fail([=](const RPCError &error) {
+ _fakeChatListRequests.erase(history);
+ history->setFakeChatListMessageFrom(MTP_messages_messages(
+ MTP_vector(0),
+ MTP_vector(0),
+ MTP_vector