diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 000000000..30bff840d
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,35 @@
+{
+ "name": "CentOS",
+ "image": "tdesktop:centos_env",
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "C_Cpp.intelliSenseEngine": "disabled",
+ "clangd.arguments": [
+ "--compile-commands-dir=${workspaceFolder}/out"
+ ],
+ "cmake.generator": "Ninja Multi-Config",
+ "cmake.buildDirectory": "${workspaceFolder}/out",
+ "cmake.configureSettings": {
+ "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
+ }
+ },
+ "extensions": [
+ "ms-vscode.cpptools-extension-pack",
+ "llvm-vs-code-extensions.vscode-clangd",
+ "TheQtCompany.qt",
+ "ms-python.python",
+ "ms-azuretools.vscode-docker",
+ "eamodio.gitlens"
+ ]
+ }
+ },
+ "capAdd": [
+ "SYS_PTRACE"
+ ],
+ "securityOpt": [
+ "seccomp=unconfined"
+ ],
+ "workspaceMount": "source=${localWorkspaceFolder},target=/usr/src/tdesktop,type=bind,consistency=cached",
+ "workspaceFolder": "/usr/src/tdesktop"
+}
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 8eb20a2e5..eae84dc67 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -83,6 +83,7 @@ jobs:
fi
docker run --rm \
+ -u $(id -u) \
-v $PWD:/usr/src/tdesktop \
-e CONFIG=Debug \
tdesktop:centos_env \
@@ -114,8 +115,8 @@ jobs:
if: env.UPLOAD_ARTIFACT == 'true'
run: |
cd $REPO_NAME/out/Debug
- sudo mkdir artifact
- sudo mv {Telegram,Updater} artifact/
+ mkdir artifact
+ mv {Telegram,Updater} artifact/
- uses: actions/upload-artifact@v4
if: env.UPLOAD_ARTIFACT == 'true'
name: Upload artifact.
diff --git a/.gitignore b/.gitignore
index 4e6ebb2cf..42b53b2f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ Release/
ipch/
.vs/
.vscode/
+.cache/
/Telegram/log.txt
/Telegram/data
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 92d2d0ff6..f79ceb35b 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1041,6 +1041,7 @@ PRIVATE
info/global_media/info_global_media_inner_widget.h
info/global_media/info_global_media_provider.cpp
info/global_media/info_global_media_provider.h
+ info/media/info_media_buttons.cpp
info/media/info_media_buttons.h
info/media/info_media_common.cpp
info/media/info_media_common.h
@@ -1285,6 +1286,8 @@ PRIVATE
media/streaming/media_streaming_video_track.h
media/view/media_view_group_thumbs.cpp
media/view/media_view_group_thumbs.h
+ media/view/media_view_open_common.cpp
+ media/view/media_view_open_common.h
media/view/media_view_overlay_opengl.cpp
media/view/media_view_overlay_opengl.h
media/view/media_view_overlay_raster.cpp
@@ -1303,7 +1306,6 @@ PRIVATE
media/view/media_view_playback_controls.h
media/view/media_view_playback_progress.cpp
media/view/media_view_playback_progress.h
- media/view/media_view_open_common.h
media/system_media_controls_manager.h
media/system_media_controls_manager.cpp
menu/menu_antispam_validator.cpp
@@ -1553,6 +1555,8 @@ PRIVATE
settings/settings_privacy_security.h
settings/settings_scale_preview.cpp
settings/settings_scale_preview.h
+ settings/settings_shortcuts.cpp
+ settings/settings_shortcuts.h
settings/settings_type.h
settings/settings_websites.cpp
settings/settings_websites.h
diff --git a/Telegram/Resources/default_shortcuts-custom.json b/Telegram/Resources/default_shortcuts-custom.json
index 42d412b15..054ad70d0 100644
--- a/Telegram/Resources/default_shortcuts-custom.json
+++ b/Telegram/Resources/default_shortcuts-custom.json
@@ -1,6 +1,7 @@
// This is a list of your own shortcuts for Telegram Desktop
// You can see full list of commands in the 'shortcuts-default.json' file
// Place a null value instead of a command string to switch the shortcut off
+// You can also edit them in Settings > Chat Settings > Keyboard Shortcuts.
[
// {
diff --git a/Telegram/Resources/icons/menu/shortcut.png b/Telegram/Resources/icons/menu/shortcut.png
new file mode 100644
index 000000000..e5e3389e9
Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut.png differ
diff --git a/Telegram/Resources/icons/menu/shortcut@2x.png b/Telegram/Resources/icons/menu/shortcut@2x.png
new file mode 100644
index 000000000..f50a5dc71
Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut@2x.png differ
diff --git a/Telegram/Resources/icons/menu/shortcut@3x.png b/Telegram/Resources/icons/menu/shortcut@3x.png
new file mode 100644
index 000000000..5eb87725c
Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 87554281d..1cf93bef0 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -642,6 +642,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_chat_quick_action_react" = "Send reaction with double click";
"lng_settings_chat_corner_reaction" = "Reaction button on messages";
+"lng_settings_shortcuts" = "Keyboard shortcuts";
+
+"lng_shortcuts_reset" = "Reset to default";
+"lng_shortcuts_recording" = "Recording...";
+"lng_shortcuts_add_another" = "Add another";
+
+"lng_shortcuts_close" = "Close the window";
+"lng_shortcuts_lock" = "Lock the application";
+"lng_shortcuts_minimize" = "Minimize the window";
+"lng_shortcuts_quit" = "Quit the application";
+"lng_shortcuts_media_play" = "Play the media";
+"lng_shortcuts_media_pause" = "Pause the media";
+"lng_shortcuts_media_play_pause" = "Toggle media playback";
+"lng_shortcuts_media_stop" = "Stop media playback";
+"lng_shortcuts_media_previous" = "Previous track";
+"lng_shortcuts_media_next" = "Next track";
+"lng_shortcuts_search" = "Search messages";
+"lng_shortcuts_chat_previous" = "Previous chat";
+"lng_shortcuts_chat_next" = "Next chat";
+"lng_shortcuts_chat_first" = "First chat";
+"lng_shortcuts_chat_last" = "Last chat";
+"lng_shortcuts_chat_self" = "Saved Messages";
+"lng_shortcuts_chat_pinned_n" = "Pinned chat #{index}";
+"lng_shortcuts_show_account_n" = "Account #{index}";
+"lng_shortcuts_show_all_chats" = "All Chats folder";
+"lng_shortcuts_show_folder_n" = "Folder #{index}";
+"lng_shortcuts_show_folder_last" = "Last folder";
+"lng_shortcuts_folder_next" = "Next folder";
+"lng_shortcuts_folder_previous" = "Previous folder";
+"lng_shortcuts_scheduled" = "Scheduled messages";
+"lng_shortcuts_archive" = "Archived chats";
+"lng_shortcuts_contacts" = "Contacts list";
+"lng_shortcuts_just_send" = "Just send";
+"lng_shortcuts_silent_send" = "Silent send";
+"lng_shortcuts_schedule" = "Schedule";
+"lng_shortcuts_read_chat" = "Mark chat as read";
+"lng_shortcuts_archive_chat" = "Archive chat";
+"lng_shortcuts_media_fullscreen" = "Toggle video fullscreen";
+"lng_shortcuts_show_chat_menu" = "Show chat menu";
+
"lng_settings_chat_reactions_title" = "Quick Reaction";
"lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction";
"lng_settings_chat_message_reply_from" = "Bob Harris";
@@ -2551,6 +2591,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_double_limits_subtitle_accounts" = "Connected Accounts";
"lng_premium_double_limits_about_accounts#one" = "Connect {count} account with different mobile numbers";
"lng_premium_double_limits_about_accounts#other" = "Connect {count} accounts with different mobile numbers";
+
+"lng_premium_double_limits_subtitle_similar_channels" = "Similar Channel";
+"lng_premium_double_limits_about_similar_channels#one" = "View up to {count} similar channel";
+"lng_premium_double_limits_about_similar_channels#other" = "View up to {count} similar channels";
//
"lng_premium_gift_title" = "Gift Telegram Premium";
@@ -3000,9 +3044,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:";
"lng_boost_channel_ask_button" = "Copy Link";
-"lng_boost_channel_or" = "or";
-"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}";
-"lng_boost_channel_gifting_link" = "Get boosts >";
+//"lng_boost_channel_or" = "or";
+//"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}";
+//"lng_boost_channel_gifting_link" = "Get boosts >";
+"lng_boost_group_ask" = "Ask your **Premium** members to boost your group with this link:";
+//"lng_boost_group_gifting" = "Boost your group by gifting your members Telegram Premium. {link}";
"lng_feature_stories#one" = "**{count}** Story Per Day";
"lng_feature_stories#other" = "**{count}** Stories Per Day";
@@ -3326,6 +3372,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_unique_backdrop" = "Backdrop";
"lng_gift_unique_symbol" = "Symbol";
"lng_gift_unique_rarity" = "Only {percent} of such collectibles have this attribute.";
+"lng_gift_unique_availability_label" = "Quantity";
"lng_gift_unique_availability#one" = "{count} of {amount} issued";
"lng_gift_unique_availability#other" = "{count} of {amount} issued";
"lng_gift_unique_info" = "Gifted to {recipient} on {date}.";
@@ -3836,6 +3883,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_choose_image" = "Choose an image";
"lng_choose_file" = "Choose a file";
"lng_choose_files" = "Choose Files";
+"lng_choose_cover" = "Choose video cover";
+"lng_choose_cover_bad" = "Can't use this file as a caption.";
"lng_game_tag" = "Game";
"lng_context_new_window" = "Open in new window";
@@ -3952,6 +4001,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_read_show" = "show when";
"lng_context_edit_shortcut" = "Edit Shortcut";
"lng_context_delete_shortcut" = "Delete Quick Reply";
+"lng_context_gift_send" = "Send Another Gift";
"lng_add_tag_about" = "Tag this message with an emoji for quick search.";
"lng_subscribe_tag_about" = "Organize your Saved Messages with tags. {link}";
@@ -3980,6 +4030,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_disable_spoiler" = "Remove Spoiler";
"lng_context_make_paid" = "Make This Content Paid";
"lng_context_change_price" = "Change Price";
+"lng_context_edit_cover" = "Edit Cover";
+"lng_context_clear_cover" = "Clear Cover";
"lng_context_mention" = "Mention";
"lng_context_search_from" = "Search messages";
@@ -4109,6 +4161,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reply_cant_forward" = "Sorry, you can't reply to a message that was sent before the group was upgraded to a supergroup. Do you wish to forward it and add your comment?";
"lng_share_title" = "Share to";
+"lng_share_at_time_title" = "Share at {time} to";
"lng_share_copy_link" = "Copy share link";
"lng_share_confirm" = "Send";
"lng_share_wrong_user" = "This game was opened from a different user.";
@@ -4232,7 +4285,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_media_save_progress" = "{ready} of {total} {mb}";
"lng_mediaview_save_as" = "Save As...";
"lng_mediaview_copy" = "Copy";
+"lng_mediaview_copy_frame" = "Copy Frame";
"lng_mediaview_forward" = "Forward";
+"lng_mediaview_share_at_time" = "Share at {time}";
"lng_mediaview_delete" = "Delete";
"lng_mediaview_save_to_profile" = "Post to Profile";
"lng_mediaview_pin_story_done" = "Story pinned";
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index a9f716c9e..f6451e78d 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="5.11.1.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 2d5590296..a5a4b7eaa 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,10,7,0
- PRODUCTVERSION 5,10,7,0
+ FILEVERSION 5,11,1,0
+ PRODUCTVERSION 5,11,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
- VALUE "FileVersion", "5.10.7.0"
+ VALUE "FileVersion", "5.11.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.10.7.0"
+ VALUE "ProductVersion", "5.11.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 39318b864..783f6f30f 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,10,7,0
- PRODUCTVERSION 5,10,7,0
+ FILEVERSION 5,11,1,0
+ PRODUCTVERSION 5,11,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
- VALUE "FileVersion", "5.10.7.0"
+ VALUE "FileVersion", "5.11.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.10.7.0"
+ VALUE "ProductVersion", "5.11.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp
index 78e463c11..6cd28824e 100644
--- a/Telegram/SourceFiles/api/api_authorizations.cpp
+++ b/Telegram/SourceFiles/api/api_authorizations.cpp
@@ -217,7 +217,11 @@ void Authorizations::toggleCallsDisabled(uint64 hash, bool disabled) {
MTP_bool(disabled)
)).done([=] {
_toggleCallsDisabledRequests.remove(hash);
- }).fail([=] {
+ }).fail([=](const MTP::Error &error) {
+ LOG(("API Error: toggle calls %1. Hash: %2. %3.")
+ .arg(disabled ? u"disabled"_q : u"enabled"_q)
+ .arg(hash)
+ .arg(error.type()));
_toggleCallsDisabledRequests.remove(hash);
}).send();
_toggleCallsDisabledRequests.emplace(hash, id);
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index 81f098d67..77c30d095 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -74,6 +74,7 @@ struct MessageToSend {
struct RemoteFileInfo {
MTPInputFile file;
std::optional thumb;
+ std::optional videoCover;
std::vector attachedStickers;
};
diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp
index 313d6bfef..4f5073a3b 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -276,16 +276,22 @@ mtpRequestId EditTextMessage(
takeFileReference = [=] { return photo->fileReference(); };
} else if (const auto document = media->document()) {
using Flag = MTPDinputMediaDocument::Flag;
+ const auto videoCover = media->videoCover();
+ const auto videoTimestamp = media->videoTimestamp();
const auto flags = Flag()
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
- | (spoilered ? Flag::f_spoiler : Flag());
+ | (spoilered ? Flag::f_spoiler : Flag())
+ | (videoTimestamp ? Flag::f_video_timestamp : Flag())
+ | (videoCover ? Flag::f_video_cover : Flag());
takeInputMedia = [=] {
return MTP_inputMediaDocument(
MTP_flags(flags),
document->mtpInput(),
- MTPInputPhoto(), // video_cover
+ (videoCover
+ ? videoCover->mtpInput()
+ : MTPInputPhoto()),
MTP_int(media->ttlSeconds()),
- MTPint(), // video_timestamp
+ MTP_int(videoTimestamp),
MTPstring()); // query
};
takeFileReference = [=] { return document->fileReference(); };
diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp
index 83ad8d156..9e2ab1a38 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.cpp
+++ b/Telegram/SourceFiles/api/api_global_privacy.cpp
@@ -13,6 +13,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api {
+PeerId ParsePaidReactionShownPeer(
+ not_null session,
+ const MTPPaidReactionPrivacy &value) {
+ return value.match([&](const MTPDpaidReactionPrivacyDefault &) {
+ return session->userPeerId();
+ }, [](const MTPDpaidReactionPrivacyAnonymous &) {
+ return PeerId();
+ }, [&](const MTPDpaidReactionPrivacyPeer &data) {
+ return data.vpeer().match([&](const MTPDinputPeerSelf &) {
+ return session->userPeerId();
+ }, [](const MTPDinputPeerUser &data) {
+ return peerFromUser(data.vuser_id());
+ }, [](const MTPDinputPeerChat &data) {
+ return peerFromChat(data.vchat_id());
+ }, [](const MTPDinputPeerChannel &data) {
+ return peerFromChannel(data.vchannel_id());
+ }, [](const MTPDinputPeerUserFromMessage &data) -> PeerId {
+ Unexpected("From message peer in ParsePaidReactionShownPeer.");
+ }, [](const MTPDinputPeerChannelFromMessage &data) -> PeerId {
+ Unexpected("From message peer in ParsePaidReactionShownPeer.");
+ }, [](const MTPDinputPeerEmpty &) -> PeerId {
+ Unexpected("Empty peer in ParsePaidReactionShownPeer.");
+ });
+ });
+}
+
GlobalPrivacy::GlobalPrivacy(not_null api)
: _session(&api->session())
, _api(&api->instance()) {
@@ -115,27 +141,27 @@ rpl::producer GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
-void GlobalPrivacy::loadPaidReactionAnonymous() {
- if (_paidReactionAnonymousLoaded) {
+void GlobalPrivacy::loadPaidReactionShownPeer() {
+ if (_paidReactionShownPeerLoaded) {
return;
}
- _paidReactionAnonymousLoaded = true;
+ _paidReactionShownPeerLoaded = true;
_api.request(MTPmessages_GetPaidReactionPrivacy(
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
}).send();
}
-void GlobalPrivacy::updatePaidReactionAnonymous(bool value) {
- _paidReactionAnonymous = value;
+void GlobalPrivacy::updatePaidReactionShownPeer(PeerId shownPeer) {
+ _paidReactionShownPeer = shownPeer;
}
-bool GlobalPrivacy::paidReactionAnonymousCurrent() const {
- return _paidReactionAnonymous.current();
+PeerId GlobalPrivacy::paidReactionShownPeerCurrent() const {
+ return _paidReactionShownPeer.current();
}
-rpl::producer GlobalPrivacy::paidReactionAnonymous() const {
- return _paidReactionAnonymous.value();
+rpl::producer GlobalPrivacy::paidReactionShownPeer() const {
+ return _paidReactionShownPeer.value();
}
void GlobalPrivacy::updateArchiveAndMute(bool value) {
diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h
index aa7bb28fe..6c5848961 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.h
+++ b/Telegram/SourceFiles/api/api_global_privacy.h
@@ -23,6 +23,10 @@ enum class UnarchiveOnNewMessage {
AnyUnmuted,
};
+[[nodiscard]] PeerId ParsePaidReactionShownPeer(
+ not_null session,
+ const MTPPaidReactionPrivacy &value);
+
class GlobalPrivacy final {
public:
explicit GlobalPrivacy(not_null api);
@@ -49,10 +53,10 @@ public:
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer newRequirePremium() const;
- void loadPaidReactionAnonymous();
- void updatePaidReactionAnonymous(bool value);
- [[nodiscard]] bool paidReactionAnonymousCurrent() const;
- [[nodiscard]] rpl::producer paidReactionAnonymous() const;
+ void loadPaidReactionShownPeer();
+ void updatePaidReactionShownPeer(PeerId shownPeer);
+ [[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
+ [[nodiscard]] rpl::producer paidReactionShownPeer() const;
private:
void apply(const MTPGlobalPrivacySettings &data);
@@ -72,9 +76,9 @@ private:
rpl::variable _showArchiveAndMute = false;
rpl::variable _hideReadTime = false;
rpl::variable _newRequirePremium = false;
- rpl::variable _paidReactionAnonymous = false;
+ rpl::variable _paidReactionShownPeer = false;
std::vector> _callbacks;
- bool _paidReactionAnonymousLoaded = false;
+ bool _paidReactionShownPeerLoaded = false;
};
diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp
index 3bbc00f5f..f7331694b 100644
--- a/Telegram/SourceFiles/api/api_media.cpp
+++ b/Telegram/SourceFiles/api/api_media.cpp
@@ -111,7 +111,8 @@ MTPInputMedia PrepareUploadedDocument(
| (info.thumb ? Flag::f_thumb : Flag())
| (item->groupId() ? Flag::f_nosound_video : Flag())
| (info.attachedStickers.empty() ? Flag::f_stickers : Flag())
- | (ttlSeconds ? Flag::f_ttl_seconds : Flag());
+ | (ttlSeconds ? Flag::f_ttl_seconds : Flag())
+ | (info.videoCover ? Flag::f_video_cover : Flag());
const auto document = item->media()->document();
return MTP_inputMediaUploadedDocument(
MTP_flags(flags),
@@ -121,7 +122,7 @@ MTPInputMedia PrepareUploadedDocument(
ComposeSendingDocumentAttributes(document),
MTP_vector(
ranges::to>(info.attachedStickers)),
- MTPInputPhoto(), // video_cover
+ info.videoCover.value_or(MTPInputPhoto()),
MTP_int(0), // video_timestamp
MTP_int(ttlSeconds));
}
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 193e2e71b..80b6f1007 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -549,10 +549,11 @@ void SendConfirmedFile(
using Flag = MTPDmessageMediaDocument::Flag;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
- | (file->spoiler ? Flag::f_spoiler : Flag())),
+ | (file->spoiler ? Flag::f_spoiler : Flag())
+ | (file->videoCover ? Flag::f_video_cover : Flag())),
file->document,
MTPVector(), // alt_documents
- MTPPhoto(), // video_cover
+ file->videoCover ? file->videoCover->photo : MTPPhoto(),
MTPint(), // video_timestamp
MTPint());
} else if (file->type == SendMediaType::Audio) {
@@ -561,10 +562,11 @@ void SendConfirmedFile(
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| Flag::f_voice
- | (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
+ | (ttlSeconds ? Flag::f_ttl_seconds : Flag())
+ | (file->videoCover ? Flag::f_video_cover : Flag())),
file->document,
MTPVector(), // alt_documents
- MTPPhoto(), // video_cover
+ file->videoCover ? file->videoCover->photo : MTPPhoto(),
MTPint(), // video_timestamp
MTP_int(ttlSeconds));
} else if (file->type == SendMediaType::Round) {
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index afcd00979..e34ef373d 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2717,8 +2717,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updatePaidReactionPrivacy: {
const auto &data = update.c_updatePaidReactionPrivacy();
- _session->api().globalPrivacy().updatePaidReactionAnonymous(
- mtpIsTrue(data.vprivate()));
+ _session->api().globalPrivacy().updatePaidReactionShownPeer(
+ Api::ParsePaidReactionShownPeer(_session, data.vprivate()));
} break;
}
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 86cfd12f2..86a10680a 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -148,6 +148,16 @@ void ShowChannelsLimitBox(not_null peer) {
action.replaceMediaOf);
}
+[[nodiscard]] QString FormatVideoTimestamp(TimeId seconds) {
+ const auto minutes = seconds / 60;
+ const auto hours = minutes / 60;
+ return hours
+ ? u"%1h%2m%3s"_q.arg(hours).arg(minutes % 60).arg(seconds % 60)
+ : minutes
+ ? u"%1m%2s"_q.arg(minutes).arg(seconds % 60)
+ : QString::number(seconds);
+}
+
} // namespace
ApiWrap::ApiWrap(not_null session)
@@ -740,7 +750,8 @@ void ApiWrap::finalizeMessageDataRequest(
QString ApiWrap::exportDirectMessageLink(
not_null item,
bool inRepliesContext,
- bool forceNonPublicLink) {
+ bool forceNonPublicLink,
+ std::optional videoTimestamp) {
Expects(item->history()->peer->isChannel());
const auto itemId = item->fullId();
@@ -792,19 +803,6 @@ QString ApiWrap::exportDirectMessageLink(
: linkThreadId
? (QString::number(linkThreadId.bare) + '/' + post)
: post);
- if (linkChannel->hasUsername()
- && !forceNonPublicLink
- && !linkChannel->isMegagroup()
- && !linkCommentId
- && !linkThreadId) {
- if (const auto media = item->media()) {
- if (const auto document = media->document()) {
- if (document->isVideoMessage()) {
- return u"https://telesco.pe/"_q + query;
- }
- }
- }
- }
return session().createInternalLinkFull(query);
};
if (forceNonPublicLink) {
@@ -826,7 +824,14 @@ QString ApiWrap::exportDirectMessageLink(
_unlikelyMessageLinks.emplace_or_assign(itemId, link);
}
}).send();
- return current;
+ const auto addTimestamp = channel->hasUsername()
+ && !inRepliesContext
+ && videoTimestamp.has_value();
+ const auto addedSeparator = (current.indexOf('?') >= 0) ? '&' : '?';
+ const auto addedTimestamp = addTimestamp
+ ? (addedSeparator + u"t="_q + FormatVideoTimestamp(*videoTimestamp))
+ : QString();
+ return current + addedTimestamp;
}
QString ApiWrap::exportDirectStoryLink(not_null story) {
@@ -3624,6 +3629,18 @@ void ApiWrap::editMedia(
file.path,
file.content,
std::move(file.information),
+ (file.videoCover
+ ? std::make_unique(
+ &session(),
+ file.videoCover->path,
+ file.videoCover->content,
+ std::move(file.videoCover->information),
+ nullptr,
+ SendMediaType::Photo,
+ to,
+ TextWithTags(),
+ false)
+ : nullptr),
type,
to,
caption,
@@ -3665,6 +3682,19 @@ void ApiWrap::sendFiles(
file.path,
file.content,
std::move(file.information),
+ (file.videoCover
+ ? std::make_unique(
+ &session(),
+ file.videoCover->path,
+ file.videoCover->content,
+ std::move(file.videoCover->information),
+ nullptr,
+ SendMediaType::Photo,
+ to,
+ TextWithTags(),
+ false,
+ nullptr)
+ : nullptr),
uploadWithType,
to,
caption,
@@ -3690,11 +3720,13 @@ void ApiWrap::sendFile(
auto caption = TextWithTags();
const auto spoiler = false;
const auto information = nullptr;
+ const auto videoCover = nullptr;
_fileLoader->addTask(std::make_unique(
&session(),
QString(),
fileContent,
information,
+ videoCover,
type,
to,
caption,
@@ -4209,19 +4241,30 @@ void ApiWrap::uploadAlbumMedia(
return;
}
const auto &fields = document->c_document();
+ const auto mtpCover = data.vvideo_cover();
+ const auto cover = (mtpCover && mtpCover->type() == mtpc_photo)
+ ? &(mtpCover->c_photo())
+ : (const MTPDphoto*)nullptr;
using Flag = MTPDinputMediaDocument::Flag;
const auto flags = Flag()
| (data.vttl_seconds() ? Flag::f_ttl_seconds : Flag())
- | (spoiler ? Flag::f_spoiler : Flag());
+ | (spoiler ? Flag::f_spoiler : Flag())
+ | (data.vvideo_timestamp() ? Flag::f_video_timestamp : Flag())
+ | (cover ? Flag::f_video_cover : Flag());
const auto media = MTP_inputMediaDocument(
MTP_flags(flags),
MTP_inputDocument(
fields.vid(),
fields.vaccess_hash(),
fields.vfile_reference()),
- MTPInputPhoto(), // video_cover
+ (cover
+ ? MTP_inputPhoto(
+ cover->vid(),
+ cover->vaccess_hash(),
+ cover->vfile_reference())
+ : MTPInputPhoto()),
+ MTP_int(data.vvideo_timestamp().value_or_empty()),
MTP_int(data.vttl_seconds().value_or_empty()),
- MTPint(), // video_timestamp
MTPstring()); // query
sendAlbumWithUploaded(item, groupId, media);
} break;
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index cfbb71256..d6e2bbc67 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -166,7 +166,8 @@ public:
QString exportDirectMessageLink(
not_null item,
bool inRepliesContext,
- bool forceNonPublicLink = false);
+ bool forceNonPublicLink = false,
+ std::optional videoTimestamp = {});
QString exportDirectStoryLink(not_null item);
void requestContacts();
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
index ae2cbc3d7..25501bd29 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
@@ -467,13 +467,16 @@ void EditCaptionBox::rebuildPreview() {
}
} else {
const auto &file = _preparedList.files.front();
-
+ const auto isVideoFile = file.isVideoFile();
const auto media = Ui::SingleMediaPreview::Create(
this,
st::defaultComposeControls,
gifPaused,
file,
- [] { return true; },
+ [=](Ui::AttachActionType type) {
+ return (type != Ui::AttachActionType::EditCover)
+ || isVideoFile;
+ },
Ui::AttachControls::Type::EditOnly);
_isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);
@@ -719,7 +722,7 @@ void EditCaptionBox::setupPhotoEditorEventHandler() {
controller->uiShow(),
&_preparedList.files.front(),
st::sendMediaPreviewSize,
- [=] { rebuildPreview(); });
+ [=](bool ok) { if (ok) rebuildPreview(); });
} else {
EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=](
Ui::PreparedList &&list) {
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 987538be5..db9eaf3a9 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/basic_click_handlers.h" // UrlClickHandler::Open.
#include "ui/boxes/boost_box.h" // StartFireworks.
#include "ui/controls/userpic_button.h"
+#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/premium_top_bar.h"
@@ -405,21 +406,14 @@ void AddTableRow(
auto result = object_ptr(table);
const auto raw = result.data();
- const auto session = &show->session();
- const auto makeContext = [session](Fn update) {
- return Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = std::move(update),
- };
- };
- auto star = session->data().customEmojiManager().creditsEmoji();
+ const auto star = Ui::CreateSingleStarWidget(
+ raw,
+ table->st().defaultValue.style.font->height);
const auto label = Ui::CreateChild(
raw,
- rpl::single(star.append(
- ' ' + Lang::FormatStarsAmountDecimal(entry.credits))),
+ Lang::FormatStarsAmountDecimal(entry.credits),
table->st().defaultValue,
- st::defaultPopupMenu,
- std::move(makeContext));
+ st::defaultPopupMenu);
const auto convert = convertToStars
? Ui::CreateChild(
@@ -430,7 +424,8 @@ void AddTableRow(
table->st().smallButton)
: nullptr;
if (convert) {
- convert->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+ using namespace Ui;
+ convert->setTextTransform(RoundButton::TextTransform::NoTransform);
convert->setClickedCallback(std::move(convertToStars));
}
rpl::combine(
@@ -440,11 +435,13 @@ void AddTableRow(
const auto convertSkip = convertWidth
? (st::normalFont->spacew + convertWidth)
: 0;
- label->resizeToNaturalWidth(width - convertSkip);
- label->moveToLeft(0, 0, width);
+ const auto labelLeft = rect::right(star) + st::normalFont->spacew;
+ label->resizeToNaturalWidth(width - convertSkip - labelLeft);
+ star->moveToLeft(0, 0, width);
+ label->moveToLeft(labelLeft, 0, width);
if (convert) {
convert->moveToLeft(
- label->width() + st::normalFont->spacew,
+ rect::right(label) + st::normalFont->spacew,
(table->st().defaultValue.style.font->ascent
- table->st().smallButton.style.font->ascent),
width);
@@ -1490,10 +1487,15 @@ void AddStarGiftTable(
auto amount = rpl::single(TextWithEntities{
Lang::FormatCountDecimal(entry.limitedCount)
});
+ const auto count = unique
+ ? (entry.limitedCount - entry.limitedLeft)
+ : entry.limitedLeft;
AddTableRow(
table,
- tr::lng_gift_availability(),
- ((!unique && !entry.limitedLeft)
+ (unique
+ ? tr::lng_gift_unique_availability_label()
+ : tr::lng_gift_availability()),
+ ((!unique && !count)
? tr::lng_gift_availability_none(
lt_amount,
std::move(amount),
@@ -1502,12 +1504,12 @@ void AddStarGiftTable(
? tr::lng_gift_unique_availability
: tr::lng_gift_availability_left)(
lt_count_decimal,
- rpl::single(entry.limitedLeft * 1.),
+ rpl::single(count * 1.),
lt_amount,
std::move(amount),
Ui::Text::WithEntities)));
}
- if (!unique && !entry.soldOutInfo) {
+ if (!unique && !entry.soldOutInfo && startUpgrade) {
AddTableRow(
table,
tr::lng_gift_unique_status(),
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 67ef1c81d..a9e33955b 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -922,7 +922,13 @@ void PeerListRow::paintDisabledCheckUserpic(
p.setPen(userpicBorderPen);
p.setBrush(Qt::NoBrush);
- p.drawEllipse(userpicEllipse);
+ if (peer()->forum()) {
+ const auto radius = userpicDiameter
+ * Ui::ForumUserpicRadiusMultiplier();
+ p.drawRoundedRect(userpicEllipse, radius, radius);
+ } else {
+ p.drawEllipse(userpicEllipse);
+ }
p.setPen(iconBorderPen);
p.setBrush(st.disabledCheckFg);
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
index 8b827dc7c..73b3004f0 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
@@ -317,6 +317,7 @@ PreviewWrap::PreviewWrap(
0, // duration
QString(), // author
false, // hasLargeMedia
+ false, // photoIsVideoCover
0)) // pendingTill
, _theme(theme)
, _style(style)
diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
index 0275358f0..12fab358c 100644
--- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
@@ -106,6 +106,8 @@ PeerShortInfoCover::PeerShortInfoCover(
, _statusStyle(std::make_unique(_st.status))
, _status(_widget.get(), std::move(status), _statusStyle->st)
, _roundMask(Images::CornersMask(_st.radius))
+, _roundMaskRetina(
+ Images::CornersMask(_st.radius / style::DevicePixelRatio()))
, _videoPaused(std::move(videoPaused)) {
_widget->setCursor(_cursor);
@@ -190,7 +192,7 @@ void PeerShortInfoCover::paint(QPainter &p) {
if (!frame.isNull()) {
frame = Images::Round(
std::move(frame),
- _roundMask,
+ _roundMaskRetina,
RectPart::TopLeft | RectPart::TopRight);
} else if (_userpicImage.isNull()) {
auto image = QImage(
@@ -226,10 +228,11 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {
const auto top = _widget->height() - fill;
const auto factor = style::DevicePixelRatio();
if (fill > 0) {
+ const auto t = roundedHeight + _scrollTop;
p.drawImage(
- QRect(0, top, roundedWidth, fill),
+ QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor),
image,
- QRect(0, top * factor, roundedWidth * factor, fill * factor));
+ QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor));
}
if (covered <= 0) {
return;
@@ -238,9 +241,9 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {
const auto from = top - rounded;
auto q = QPainter(&_roundedTopImage);
q.drawImage(
- QRect(0, 0, roundedWidth, rounded),
+ QRect(0, 0, roundedWidth * factor, rounded * factor),
image,
- QRect(0, from * factor, roundedWidth * factor, rounded * factor));
+ QRect(0, _scrollTop, roundedWidth * factor, rounded * factor));
q.end();
_roundedTopImage = Images::Round(
std::move(_roundedTopImage),
diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
index 271e1826c..f6baf8c74 100644
--- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
+++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
@@ -123,6 +123,7 @@ private:
object_ptr _additionalStatus = { nullptr };
std::array _roundMask;
+ std::array _roundMaskRetina;
QImage _userpicImage;
QImage _roundedTopImage;
QImage _barSmall;
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
index 570fb75e6..53c6a6744 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
@@ -1526,6 +1526,18 @@ void DoubledLimitsPreviewBox(
Main::Domain::kPremiumMaxAccounts,
till,
});
+ {
+ const auto premium = limits.similarChannelsPremium();
+ entries.push_back({
+ tr::lng_premium_double_limits_subtitle_similar_channels(),
+ tr::lng_premium_double_limits_about_similar_channels(
+ lt_count,
+ rpl::single(float64(premium)),
+ Ui::Text::RichLangValue),
+ limits.similarChannelsDefault(),
+ premium,
+ });
+ }
Ui::Premium::ShowListBox(
box,
st::defaultPremiumLimits,
diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp
index 0031f2d42..ad208b2db 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_files_box.cpp
@@ -250,7 +250,7 @@ SendFilesBox::Block::Block(
int till,
Fn gifPaused,
SendFilesWay way,
- Fn canToggleSpoiler)
+ Fn actionAllowed)
: _items(items)
, _from(from)
, _till(till) {
@@ -268,7 +268,9 @@ SendFilesBox::Block::Block(
st,
my,
way,
- std::move(canToggleSpoiler));
+ [=](int index, Ui::AttachActionType type) {
+ return actionAllowed((*_items)[from + index], type);
+ });
_preview.reset(preview);
} else {
const auto media = Ui::SingleMediaPreview::Create(
@@ -276,7 +278,9 @@ SendFilesBox::Block::Block(
st,
gifPaused,
first,
- std::move(canToggleSpoiler));
+ [=](Ui::AttachActionType type) {
+ return actionAllowed((*_items)[from], type);
+ });
if (media) {
_isSingleMedia = true;
_preview.reset(media);
@@ -352,6 +356,38 @@ rpl::producer SendFilesBox::Block::itemModifyRequest() const {
}
}
+rpl::producer SendFilesBox::Block::itemEditCoverRequest() const {
+ using namespace rpl::mappers;
+
+ const auto preview = _preview.get();
+ const auto from = _from;
+ if (_isAlbum) {
+ const auto album = static_cast(preview);
+ return album->thumbEditCoverRequested() | rpl::map(_1 + from);
+ } else if (_isSingleMedia) {
+ const auto media = static_cast(preview);
+ return media->editCoverRequests() | rpl::map_to(from);
+ } else {
+ return rpl::never();
+ }
+}
+
+rpl::producer SendFilesBox::Block::itemClearCoverRequest() const {
+ using namespace rpl::mappers;
+
+ const auto preview = _preview.get();
+ const auto from = _from;
+ if (_isAlbum) {
+ const auto album = static_cast(preview);
+ return album->thumbClearCoverRequested() | rpl::map(_1 + from);
+ } else if (_isSingleMedia) {
+ const auto media = static_cast(preview);
+ return media->clearCoverRequests() | rpl::map_to(from);
+ } else {
+ return rpl::never();
+ }
+}
+
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
if (_isAlbum) {
const auto album = static_cast(_preview.get());
@@ -1046,7 +1082,16 @@ void SendFilesBox::pushBlock(int from, int till) {
till,
gifPaused,
_sendWay.current(),
- [=] { return !hasPrice(); });
+ [=](const Ui::PreparedFile &file, Ui::AttachActionType type) {
+ return (type == Ui::AttachActionType::ToggleSpoiler)
+ ? !hasPrice()
+ : (type == Ui::AttachActionType::EditCover)
+ ? (file.isVideoFile()
+ && _captionToPeer
+ && (_captionToPeer->isBroadcast()
+ || _captionToPeer->isSelf()))
+ : (file.videoCover != nullptr);
+ });
auto &block = _blocks.back();
const auto widget = _inner->add(
block.takeWidget(),
@@ -1167,7 +1212,79 @@ void SendFilesBox::pushBlock(int from, int till) {
show,
&_list.files[index],
st::sendMediaPreviewSize,
- [=] { refreshAllAfterChanges(from); });
+ [=](bool ok) { if (ok) refreshAllAfterChanges(from); });
+ }, widget->lifetime());
+
+ block.itemEditCoverRequest(
+ ) | rpl::start_with_next([=, show = _show](int index) {
+ applyBlockChanges();
+
+ const auto replace = [=](Ui::PreparedList list) {
+ if (list.files.empty()) {
+ return;
+ }
+ auto &entry = _list.files[index];
+ const auto video = entry.information
+ ? std::get_if(
+ &entry.information->media)
+ : nullptr;
+ if (!video) {
+ return;
+ }
+ auto old = std::shared_ptr(
+ std::move(entry.videoCover));
+ entry.videoCover = std::make_unique(
+ std::move(list.files.front()));
+ Editor::OpenWithPreparedFile(
+ this,
+ show,
+ entry.videoCover.get(),
+ st::sendMediaPreviewSize,
+ crl::guard(this, [=](bool ok) {
+ if (!ok) {
+ _list.files[index].videoCover = old
+ ? std::make_unique(
+ std::move(*old))
+ : nullptr;
+ }
+ refreshAllAfterChanges(from);
+ }),
+ video->thumbnail.size());
+ };
+ const auto checkResult = [=](const Ui::PreparedList &list) {
+ if (list.files.empty()) {
+ return true;
+ }
+ if (list.files.front().type != Ui::PreparedFile::Type::Photo) {
+ show->showToast(tr::lng_choose_cover_bad(tr::now));
+ return false;
+ }
+ return true;
+ };
+ const auto callback = [=](FileDialog::OpenResult &&result) {
+ const auto premium = _show->session().premium();
+ FileDialogCallback(
+ std::move(result),
+ checkResult,
+ replace,
+ premium,
+ show);
+ };
+
+ FileDialog::GetOpenPath(
+ this,
+ tr::lng_choose_cover(tr::now),
+ FileDialog::ImagesFilter(),
+ crl::guard(this, callback));
+ }, widget->lifetime());
+
+ block.itemClearCoverRequest(
+ ) | rpl::start_with_next([=](int index) {
+ applyBlockChanges();
+ refreshAllAfterChanges(from, [&] {
+ auto &entry = _list.files[index];
+ entry.videoCover = nullptr;
+ });
}, widget->lifetime());
block.orderUpdated() | rpl::start_with_next([=]{
diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h
index 9b4123d1c..9a0fea06d 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.h
+++ b/Telegram/SourceFiles/boxes/send_files_box.h
@@ -153,7 +153,9 @@ private:
int till,
Fn gifPaused,
Ui::SendFilesWay way,
- Fn canToggleSpoiler);
+ Fn actionAllowed);
Block(Block &&other) = default;
Block &operator=(Block &&other) = default;
@@ -164,6 +166,8 @@ private:
[[nodiscard]] rpl::producer itemDeleteRequest() const;
[[nodiscard]] rpl::producer itemReplaceRequest() const;
[[nodiscard]] rpl::producer itemModifyRequest() const;
+ [[nodiscard]] rpl::producer itemEditCoverRequest() const;
+ [[nodiscard]] rpl::producer itemClearCoverRequest() const;
[[nodiscard]] rpl::producer<> orderUpdated() const;
void setSendWay(Ui::SendFilesWay way);
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 0c8079585..bfd4dfe9c 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -282,7 +282,9 @@ void ShareBox::prepare() {
_select->resizeToWidth(st::boxWideWidth);
Ui::SendPendingMoveResizeEvents(_select);
- setTitle(tr::lng_share_title());
+ setTitle(_descriptor.titleOverride
+ ? std::move(_descriptor.titleOverride)
+ : tr::lng_share_title());
_inner = setInnerWidget(
object_ptr(this, _descriptor, uiShow()),
@@ -1500,7 +1502,8 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
std::shared_ptr show,
not_null history,
- MessageIdsList msgIds) {
+ MessageIdsList msgIds,
+ std::optional videoTimestamp) {
struct State final {
base::flat_set requests;
};
@@ -1536,6 +1539,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: Flag(0))
| ((forwardOptions == Data::ForwardOptions::NoNamesAndCaptions)
? Flag::f_drop_media_captions
+ : Flag(0))
+ | (videoTimestamp.has_value()
+ ? Flag::f_video_timestamp
: Flag(0));
auto mtpMsgIds = QVector();
mtpMsgIds.reserve(existingIds.size());
@@ -1593,7 +1599,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
MTP_int(options.scheduled),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
- MTPint() // video_timestamp
+ MTP_int(videoTimestamp.value_or(0))
)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
threadHistory->session().api().applyUpdates(updates);
state->requests.remove(reqId);
diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h
index f21b4505b..779a60e0a 100644
--- a/Telegram/SourceFiles/boxes/share_box.h
+++ b/Telegram/SourceFiles/boxes/share_box.h
@@ -104,7 +104,8 @@ public:
[[nodiscard]] static SubmitCallback DefaultForwardCallback(
std::shared_ptr show,
not_null history,
- MessageIdsList msgIds);
+ MessageIdsList msgIds,
+ std::optional videoTimestamp = {});
struct Descriptor {
not_null session;
@@ -113,7 +114,9 @@ public:
FilterCallback filterCallback;
object_ptr bottomWidget = { nullptr };
rpl::producer copyLinkText;
+ rpl::producer titleOverride;
ShareBoxStyleOverrides st;
+ std::optional videoTimestamp;
struct {
int sendersCount = 0;
int captionsCount = 0;
diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp
index 50011546d..c99b64959 100644
--- a/Telegram/SourceFiles/boxes/star_gift_box.cpp
+++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp
@@ -99,8 +99,8 @@ namespace Ui {
namespace {
constexpr auto kPriceTabAll = 0;
-constexpr auto kPriceTabLimited = -1;
-constexpr auto kPriceTabInStock = -2;
+constexpr auto kPriceTabLimited = -2;
+constexpr auto kPriceTabInStock = -1;
constexpr auto kGiftMessageLimit = 255;
constexpr auto kSentToastDuration = 3 * crl::time(1000);
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
@@ -2371,7 +2371,7 @@ void ShowUniqueGiftWearBox(
object_ptr(
raw,
std::move(text),
- st.infoAbout ? *st.infoAbout : st::boxDividerLabel),
+ st.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr(
raw,
@@ -2603,7 +2603,7 @@ void UpgradeBox(
object_ptr(
raw,
std::move(text),
- st::boxDividerLabel),
+ st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr(
raw,
diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp
index 7d5793727..02c5fb144 100644
--- a/Telegram/SourceFiles/calls/calls_instance.cpp
+++ b/Telegram/SourceFiles/calls/calls_instance.cpp
@@ -578,8 +578,7 @@ void Instance::handleCallUpdate(
if (inCall()
&& _currentCall->type() == Call::Type::Outgoing
&& _currentCall->user()->id == session->userPeerId()
- && (peerFromUser(phoneCall.vparticipant_id())
- == _currentCall->user()->session().userPeerId())) {
+ && (user->id == _currentCall->user()->session().userPeerId())) {
// Ignore call from the same running app, other account.
return;
}
diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp
index 74ddcb8f2..673216650 100644
--- a/Telegram/SourceFiles/calls/calls_panel.cpp
+++ b/Telegram/SourceFiles/calls/calls_panel.cpp
@@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/rect.h"
+#include "ui/integration.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
@@ -65,6 +66,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Calls {
namespace {
+constexpr auto kHideControlsTimeout = 5 * crl::time(1000);
+constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000);
+
[[nodiscard]] QByteArray BatterySvg(
const QSize &s,
const QColor &c) {
@@ -118,7 +122,9 @@ Panel::Panel(not_null call)
st::callMicrophoneMute,
&st::callMicrophoneUnmute))
, _name(widget(), st::callName)
-, _status(widget(), st::callStatus) {
+, _status(widget(), st::callStatus)
+, _hideControlsTimer([=] { requestControlsHidden(true); })
+, _controlsShownForceTimer([=] { controlsShownForce(false); }) {
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_layerBg->setHideByBackgroundClick(true);
@@ -188,6 +194,25 @@ void Panel::initWindow() {
&& window()->isFullScreen()) {
window()->showNormal();
}
+ } else if (e->type() == QEvent::WindowStateChange) {
+ const auto state = window()->windowState();
+ _fullScreenOrMaximized = (state & Qt::WindowFullScreen)
+ || (state & Qt::WindowMaximized);
+ } else if (e->type() == QEvent::Enter) {
+ _mouseInside = true;
+ Ui::Integration::Instance().registerLeaveSubscription(
+ window().get());
+ if (!_fullScreenOrMaximized.current()) {
+ requestControlsHidden(false);
+ _hideControlsTimer.cancel();
+ }
+ } else if (e->type() == QEvent::Leave) {
+ _mouseInside = false;
+ Ui::Integration::Instance().unregisterLeaveSubscription(
+ window().get());
+ if (!_fullScreenOrMaximized.current()) {
+ _hideControlsTimer.callOnce(kHideControlsQuickTimeout);
+ }
}
return base::EventFilterResult::Continue;
});
@@ -415,7 +440,11 @@ void Panel::refreshIncomingGeometry() {
void Panel::reinitWithCall(Call *call) {
_callLifetime.destroy();
_call = call;
+ const auto guard = gsl::finally([&] {
+ updateControlsShown();
+ });
if (!_call) {
+ _fingerprint.destroy();
_incoming = nullptr;
_outgoingVideoBubble = nullptr;
_powerSaveBlocker = nullptr;
@@ -457,6 +486,51 @@ void Panel::reinitWithCall(Call *call) {
_window.backend());
_incoming->widget()->hide();
+ _incoming->rp()->shownValue() | rpl::start_with_next([=] {
+ updateControlsShown();
+ }, _incoming->rp()->lifetime());
+
+ _hideControlsFilter = nullptr;
+ _fullScreenOrMaximized.value(
+ ) | rpl::start_with_next([=](bool fullScreenOrMaximized) {
+ if (fullScreenOrMaximized) {
+ class Filter final : public QObject {
+ public:
+ explicit Filter(Fn moved) : _moved(moved) {
+ qApp->installEventFilter(this);
+ }
+
+ bool eventFilter(QObject *watched, QEvent *event) {
+ if (event->type() == QEvent::MouseMove) {
+ _moved(watched);
+ }
+ return false;
+ }
+
+ private:
+ Fn _moved;
+
+ };
+ _hideControlsFilter.reset(new Filter([=](QObject *what) {
+ _mouseInside = true;
+ if (what->isWidgetType()
+ && window()->isAncestorOf(static_cast(what))) {
+ _hideControlsTimer.callOnce(kHideControlsTimeout);
+ requestControlsHidden(false);
+ updateControlsShown();
+ }
+ }));
+ _hideControlsTimer.callOnce(kHideControlsTimeout);
+ } else {
+ _hideControlsFilter = nullptr;
+ _hideControlsTimer.cancel();
+ if (_mouseInside) {
+ requestControlsHidden(false);
+ updateControlsShown();
+ }
+ }
+ }, _incoming->rp()->lifetime());
+
_call->mutedValue(
) | rpl::start_with_next([=](bool mute) {
_mute->entity()->setProgress(mute ? 1. : 0.);
@@ -603,6 +677,8 @@ void Panel::createRemoteAudioMute() {
const auto r = _remoteAudioMute->rect();
auto hq = PainterHighQualityEnabler(p);
+ p.setOpacity(_controlsShownAnimation.value(
+ _controlsShown ? 1. : 0.));
p.setBrush(st::videoPlayIconBg);
p.setPen(Qt::NoPen);
p.drawRoundedRect(r, r.height() / 2, r.height() / 2);
@@ -661,6 +737,8 @@ void Panel::createRemoteLowBattery() {
const auto r = _remoteLowBattery->rect();
auto hq = PainterHighQualityEnabler(p);
+ p.setOpacity(_controlsShownAnimation.value(
+ _controlsShown ? 1. : 0.));
p.setBrush(st::videoPlayIconBg);
p.setPen(Qt::NoPen);
p.drawRoundedRect(r, r.height() / 2, r.height() / 2);
@@ -782,6 +860,9 @@ void Panel::showDevicesMenu(
}
Core::App().saveSettingsDelayed();
};
+ controlsShownForce(true);
+ updateControlsShown();
+
_devicesMenu = MakeDeviceSelectionMenu(
widget(),
&Core::App().mediaDevices(),
@@ -791,6 +872,9 @@ void Panel::showDevicesMenu(
Ui::PopupMenu::VerticalOrigin::Bottom);
_devicesMenu->popup(button->mapToGlobal(QPoint())
- QPoint(st::callDeviceSelectionMenu.menu.widthMin / 2, 0));
+ QObject::connect(_devicesMenu.get(), &QObject::destroyed, window(), [=] {
+ _controlsShownForceTimer.callOnce(kHideControlsQuickTimeout);
+ });
}
void Panel::refreshOutgoingPreviewInBody(State state) {
@@ -823,6 +907,33 @@ QRect Panel::outgoingFrameGeometry() const {
return _outgoingVideoBubble->geometry();
}
+void Panel::requestControlsHidden(bool hidden) {
+ _hideControlsRequested = hidden;
+ updateControlsShown();
+}
+
+void Panel::controlsShownForce(bool shown) {
+ _controlsShownForce = shown;
+ if (shown) {
+ _controlsShownForceTimer.cancel();
+ }
+ updateControlsShown();
+}
+
+void Panel::updateControlsShown() {
+ const auto shown = !_incoming
+ || _incoming->widget()->isHidden()
+ || _controlsShownForce
+ || !_hideControlsRequested;
+ if (_controlsShown != shown) {
+ _controlsShown = shown;
+ _controlsShownAnimation.start([=] {
+ updateControlsGeometry();
+ }, shown ? 0. : 1., shown ? 1. : 0., st::slideDuration);
+ updateControlsGeometry();
+ }
+}
+
void Panel::updateControlsGeometry() {
if (widget()->size().isEmpty()) {
return;
@@ -830,6 +941,8 @@ void Panel::updateControlsGeometry() {
if (_incoming) {
refreshIncomingGeometry();
}
+ const auto shown = _controlsShownAnimation.value(
+ _controlsShown ? 1. : 0.);
if (_fingerprint) {
#ifndef Q_OS_MAC
const auto controlsGeometry = _controls->controls.geometry();
@@ -848,14 +961,14 @@ void Panel::updateControlsGeometry() {
const auto minRight = 0;
#endif // _controls
const auto desired = (widget()->width() - _fingerprint->width()) / 2;
+ const auto top = anim::interpolate(
+ -_fingerprint->height(),
+ st::callFingerprintTop,
+ shown);
if (minLeft) {
- _fingerprint->moveToLeft(
- std::max(desired, minLeft),
- st::callFingerprintTop);
+ _fingerprint->moveToLeft(std::max(desired, minLeft), top);
} else {
- _fingerprint->moveToRight(
- std::max(desired, minRight),
- st::callFingerprintTop);
+ _fingerprint->moveToRight(std::max(desired, minRight), top);
}
}
const auto innerHeight = std::max(widget()->height(), st::callHeightMin);
@@ -885,7 +998,11 @@ void Panel::updateControlsGeometry() {
/ (_outgoingPreviewInBody ? 3 : 2);
_bodyTop = availableTop + skipHeight;
- _buttonsTop = availableTop + available;
+ _buttonsTopShown = availableTop + available;
+ _buttonsTop = anim::interpolate(
+ widget()->height(),
+ _buttonsTopShown,
+ shown);
const auto previewTop = _bodyTop + _bodySt->height + skipHeight;
_userpic->setGeometry(
@@ -908,6 +1025,8 @@ void Panel::updateControlsGeometry() {
(_buttonsTop
- st::callRemoteAudioMuteSkip
- _remoteAudioMute->height()));
+ _remoteAudioMute->update();
+ _remoteAudioMute->entity()->setOpacity(shown);
}
if (_remoteLowBattery) {
_remoteLowBattery->moveToLeft(
@@ -915,6 +1034,8 @@ void Panel::updateControlsGeometry() {
(_buttonsTop
- st::callRemoteAudioMuteSkip
- _remoteLowBattery->height()));
+ _remoteLowBattery->update();
+ _remoteLowBattery->entity()->setOpacity(shown);
}
if (_outgoingPreviewInBody) {
@@ -925,7 +1046,7 @@ void Panel::updateControlsGeometry() {
previewTop,
bodyPreviewSize.width(),
bodyPreviewSize.height()));
- } else {
+ } else if (_outgoingVideoBubble) {
updateOutgoingVideoBubbleGeometry();
}
diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h
index e53045a44..ee24ab2c4 100644
--- a/Telegram/SourceFiles/calls/calls_panel.h
+++ b/Telegram/SourceFiles/calls/calls_panel.h
@@ -111,6 +111,9 @@ private:
[[nodiscard]] bool handleClose() const;
+ void requestControlsHidden(bool hidden);
+ void controlsShownForce(bool shown);
+ void updateControlsShown();
void updateControlsGeometry();
void updateHangupGeometry();
void updateStatusGeometry();
@@ -177,8 +180,19 @@ private:
std::unique_ptr _outgoingVideoBubble;
QPixmap _bottomShadow;
int _bodyTop = 0;
+ int _buttonsTopShown = 0;
int _buttonsTop = 0;
+ base::Timer _hideControlsTimer;
+ base::Timer _controlsShownForceTimer;
+ std::unique_ptr _hideControlsFilter;
+ bool _hideControlsRequested = false;
+ rpl::variable _fullScreenOrMaximized;
+ Ui::Animations::Simple _controlsShownAnimation;
+ bool _controlsShownForce = false;
+ bool _controlsShown = true;
+ bool _mouseInside = false;
+
base::unique_qptr _devicesMenu;
base::Timer _updateDurationTimer;
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 7da5ba6c6..46e4aa04a 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -855,6 +855,17 @@ historyComposeButton: FlatButton {
color: historyComposeButtonBgRipple;
}
}
+historyGiftToChannel: IconButton(defaultIconButton) {
+ width: 46px;
+ height: 46px;
+
+ icon: icon{{ "menu/gift_premium", windowActiveTextFg }};
+ iconOver: icon{{ "menu/gift_premium", windowActiveTextFg }};
+
+ rippleAreaPosition: point(3px, 3px);
+ rippleAreaSize: 40px;
+ ripple: universalRippleAnimation;
+}
historyUnblock: FlatButton(historyComposeButton) {
color: attentionButtonFg;
overColor: attentionButtonFgOver;
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
index 6c6ad61dd..ba80c9604 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
@@ -628,6 +628,13 @@ void EmojiListWidget::applyNextSearchQuery() {
const auto modeChanged = (_searchMode != searching);
clearSelection();
if (modeChanged) {
+ if (_picker) {
+ _picker->hideAnimated();
+ }
+ _colorAllRipple = nullptr;
+ for (auto &set : _custom) {
+ set.ripple = nullptr;
+ }
_searchMode = searching;
}
if (!searching) {
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp
index 1a189fa7f..04ceda97c 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp
@@ -329,6 +329,7 @@ not_null GenerateLocalSticker(
path,
QByteArray(),
nullptr,
+ nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 567caa00a..39a4c9701 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_privacy_security.h"
#include "settings/settings_chat.h"
#include "settings/settings_premium.h"
+#include "storage/storage_account.h"
#include "mainwidget.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
@@ -602,6 +603,8 @@ bool ResolveUsernameOrPhone(
const auto threadParam = params.value(u"thread"_q);
const auto threadId = topicId ? topicId : threadParam.toInt();
const auto gameParam = params.value(u"game"_q);
+ const auto videot = params.value(u"t"_q);
+
if (!gameParam.isEmpty() && validDomain(gameParam)) {
startToken = gameParam;
resolveType = ResolveType::ShareGame;
@@ -618,6 +621,9 @@ bool ResolveUsernameOrPhone(
.phone = phone,
.messageId = post,
.storyId = storyId,
+ .videoTimestamp = (!videot.isEmpty()
+ ? ParseVideoTimestamp(videot)
+ : std::optional()),
.text = params.value(u"text"_q),
.repliesInfo = commentId
? Window::RepliesByLinkInfo{
@@ -781,8 +787,8 @@ bool OpenMediaTimestamp(
if (!controller) {
return false;
}
- const auto time = match->captured(2).toInt();
- if (time < 0) {
+ const auto position = match->captured(2).toInt();
+ if (position < 0) {
return false;
}
const auto base = match->captured(1);
@@ -795,7 +801,7 @@ bool OpenMediaTimestamp(
const auto session = &controller->session();
const auto document = session->data().document(documentId);
const auto context = session->data().message(itemId);
- const auto timeMs = time * crl::time(1000);
+ const auto time = position * crl::time(1000);
if (document->isVideoFile()) {
controller->window().openInMediaView(Media::View::OpenRequest(
controller,
@@ -803,11 +809,9 @@ bool OpenMediaTimestamp(
context,
context ? context->topicRootId() : MsgId(0),
false,
- timeMs));
+ time));
} else if (document->isSong() || document->isVoiceMessage()) {
- session->settings().setMediaLastPlaybackPosition(
- documentId,
- timeMs);
+ session->local().setMediaLastPlaybackPosition(documentId, time);
Media::Player::instance()->play({ document, itemId });
}
return true;
@@ -1759,4 +1763,14 @@ void ResolveAndShowUniqueGift(
ResolveAndShowUniqueGift(std::move(show), slug, {});
}
+TimeId ParseVideoTimestamp(QStringView value) {
+ const auto kExp = u"^(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$"_q;
+ const auto m = QRegularExpression(kExp).match(value);
+ return m.hasMatch()
+ ? (m.capturedView(1).toInt() * 3600
+ + m.capturedView(2).toInt() * 60
+ + m.capturedView(3).toInt())
+ : value.toInt();
+}
+
} // namespace Core
diff --git a/Telegram/SourceFiles/core/local_url_handlers.h b/Telegram/SourceFiles/core/local_url_handlers.h
index 4f9b68003..ce71def0f 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.h
+++ b/Telegram/SourceFiles/core/local_url_handlers.h
@@ -50,4 +50,6 @@ void ResolveAndShowUniqueGift(
std::shared_ptr show,
const QString &slug);
+[[nodiscard]] TimeId ParseVideoTimestamp(QStringView value);
+
} // namespace Core
diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp
index 93c6db96a..bd96398d4 100644
--- a/Telegram/SourceFiles/core/shortcuts.cpp
+++ b/Telegram/SourceFiles/core/shortcuts.cpp
@@ -28,6 +28,7 @@ namespace {
constexpr auto kCountLimit = 2048; // How many shortcuts can be in json file.
rpl::event_stream> RequestsStream;
+bool Paused/* = false*/;
const auto AutoRepeatCommands = base::flat_set{
Command::MediaPrevious,
@@ -112,45 +113,15 @@ const auto CommandByName = base::flat_map{
//
};
-const auto CommandNames = base::flat_map{
- { Command::Close , u"close_telegram"_q },
- { Command::Lock , u"lock_telegram"_q },
- { Command::Minimize , u"minimize_telegram"_q },
- { Command::Quit , u"quit_telegram"_q },
-
- { Command::MediaPlay , u"media_play"_q },
- { Command::MediaPause , u"media_pause"_q },
- { Command::MediaPlayPause , u"media_playpause"_q },
- { Command::MediaStop , u"media_stop"_q },
- { Command::MediaPrevious , u"media_previous"_q },
- { Command::MediaNext , u"media_next"_q },
-
- { Command::Search , u"search"_q },
-
- { Command::ChatPrevious , u"previous_chat"_q },
- { Command::ChatNext , u"next_chat"_q },
- { Command::ChatFirst , u"first_chat"_q },
- { Command::ChatLast , u"last_chat"_q },
- { Command::ChatSelf , u"self_chat"_q },
-
- { Command::FolderPrevious , u"previous_folder"_q },
- { Command::FolderNext , u"next_folder"_q },
- { Command::ShowAllChats , u"all_chats"_q },
-
- { Command::ShowFolder1 , u"folder1"_q },
- { Command::ShowFolder2 , u"folder2"_q },
- { Command::ShowFolder3 , u"folder3"_q },
- { Command::ShowFolder4 , u"folder4"_q },
- { Command::ShowFolder5 , u"folder5"_q },
- { Command::ShowFolder6 , u"folder6"_q },
- { Command::ShowFolderLast , u"last_folder"_q },
-
- { Command::ShowArchive , u"show_archive"_q },
- { Command::ShowContacts , u"show_contacts"_q },
-
- { Command::ReadChat , u"read_chat"_q },
-
- { Command::ShowChatMenu , u"show_chat_menu"_q },
+const base::flat_map &CommandNames() {
+ static const auto result = [&] {
+ auto result = base::flat_map();
+ for (const auto &[name, command] : CommandByName) {
+ result.emplace(command, name);
+ }
+ return result;
+ }();
+ return result;
};
[[maybe_unused]] constexpr auto kNoValue = {
@@ -175,19 +146,39 @@ public:
[[nodiscard]] const QStringList &errors() const;
+ [[nodiscard]] auto keysDefaults() const
+ -> base::flat_map>;
+ [[nodiscard]] auto keysCurrents() const
+ -> base::flat_map>;
+
+ void change(
+ QKeySequence was,
+ QKeySequence now,
+ Command command,
+ std::optional restore);
+ void resetToDefaults();
+
private:
void fillDefaults();
void writeDefaultFile();
+ void writeCustomFile();
bool readCustomFile();
void set(const QString &keys, Command command, bool replace = false);
+ void set(const QKeySequence &result, Command command, bool replace);
void remove(const QString &keys);
+ void remove(const QKeySequence &keys);
void unregister(base::unique_qptr shortcut);
+ void pruneListened();
+
QStringList _errors;
base::flat_map> _shortcuts;
base::flat_multi_map, Command> _commandByObject;
+ std::vector> _listened;
+
+ base::flat_map> _defaults;
base::flat_set _mediaShortcuts;
base::flat_set _supportShortcuts;
@@ -278,6 +269,54 @@ const QStringList &Manager::errors() const {
return _errors;
}
+auto Manager::keysDefaults() const
+-> base::flat_map> {
+ return _defaults;
+}
+
+auto Manager::keysCurrents() const
+-> base::flat_map> {
+ auto result = base::flat_map>();
+ for (const auto &[keys, command] : _shortcuts) {
+ auto i = _commandByObject.findFirst(command);
+ const auto end = _commandByObject.end();
+ for (; i != end && (i->first == command); ++i) {
+ result[keys].emplace(i->second);
+ }
+ }
+ return result;
+}
+
+void Manager::change(
+ QKeySequence was,
+ QKeySequence now,
+ Command command,
+ std::optional restore) {
+ if (!was.isEmpty()) {
+ remove(was);
+ }
+ if (!now.isEmpty()) {
+ set(now, command, true);
+ }
+ if (restore) {
+ Assert(!was.isEmpty());
+ set(was, *restore, true);
+ }
+ writeCustomFile();
+}
+
+void Manager::resetToDefaults() {
+ while (!_shortcuts.empty()) {
+ remove(_shortcuts.begin()->first);
+ }
+ for (const auto &[sequence, commands] : _defaults) {
+ for (const auto command : commands) {
+ set(sequence, command, false);
+ }
+ }
+ writeCustomFile();
+}
+
std::vector Manager::lookup(not_null object) const {
auto result = std::vector();
auto i = _commandByObject.findFirst(object);
@@ -301,11 +340,23 @@ void Manager::toggleSupport(bool toggled) {
}
void Manager::listen(not_null widget) {
+ pruneListened();
+ _listened.push_back(widget.get());
for (const auto &[keys, shortcut] : _shortcuts) {
widget->addAction(shortcut.get());
}
}
+void Manager::pruneListened() {
+ for (auto i = begin(_listened); i != end(_listened);) {
+ if (i->data()) {
+ ++i;
+ } else {
+ i = _listened.erase(i);
+ }
+ }
+}
+
bool Manager::readCustomFile() {
// read custom shortcuts from file if it exists or write an empty custom shortcuts file
QFile file(CustomFilePath());
@@ -440,7 +491,9 @@ void Manager::fillDefaults() {
set(u"ctrl+r"_q, Command::ReadChat);
- set(u"ctrl+="_q, Command::ShowChatMenu);
+ set(u"ctrl+\\"_q, Command::ShowChatMenu);
+
+ _defaults = keysCurrents();
}
void Manager::writeDefaultFile() {
@@ -466,8 +519,8 @@ void Manager::writeDefaultFile() {
auto i = _commandByObject.findFirst(object);
const auto end = _commandByObject.end();
for (; i != end && i->first == object; ++i) {
- const auto j = CommandNames.find(i->second);
- if (j != CommandNames.end()) {
+ const auto j = CommandNames().find(i->second);
+ if (j != CommandNames().end()) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, j->second);
@@ -488,6 +541,55 @@ void Manager::writeDefaultFile() {
}
}
+ auto document = QJsonDocument();
+ document.setArray(shortcuts);
+ file.write(document.toJson(QJsonDocument::Indented));
+}
+
+void Manager::writeCustomFile() {
+ auto shortcuts = QJsonArray();
+ for (const auto &[sequence, shortcut] : _shortcuts) {
+ const auto object = shortcut.get();
+ auto i = _commandByObject.findFirst(object);
+ const auto end = _commandByObject.end();
+ for (; i != end && i->first == object; ++i) {
+ const auto d = _defaults.find(sequence);
+ if (d == _defaults.end() || !d->second.contains(i->second)) {
+ const auto j = CommandNames().find(i->second);
+ if (j != CommandNames().end()) {
+ QJsonObject entry;
+ entry.insert(u"keys"_q, sequence.toString().toLower());
+ entry.insert(u"command"_q, j->second);
+ shortcuts.append(entry);
+ }
+ }
+ }
+ }
+ for (const auto &[sequence, command] : _defaults) {
+ if (!_shortcuts.contains(sequence)) {
+ QJsonObject entry;
+ entry.insert(u"keys"_q, sequence.toString().toLower());
+ entry.insert(u"command"_q, QJsonValue());
+ shortcuts.append(entry);
+ }
+ }
+
+ if (shortcuts.isEmpty()) {
+ WriteDefaultCustomFile();
+ return;
+ }
+
+ auto file = QFile(CustomFilePath());
+ if (!file.open(QIODevice::WriteOnly)) {
+ LOG(("Shortcut Warning: could not write custom shortcuts file."));
+ return;
+ }
+ const char *customHeader = R"HEADER(
+// This is a list of changed shortcuts for Telegram Desktop
+// You can edit them in Settings > Chat Settings > Keyboard Shortcuts.
+
+)HEADER";
+ file.write(customHeader);
auto document = QJsonDocument();
document.setArray(shortcuts);
@@ -504,8 +606,15 @@ void Manager::set(const QString &keys, Command command, bool replace) {
_errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
return;
}
+ set(result, command, replace);
+}
+
+void Manager::set(
+ const QKeySequence &keys,
+ Command command,
+ bool replace) {
auto shortcut = base::make_unique_q();
- shortcut->setShortcut(result);
+ shortcut->setShortcut(keys);
shortcut->setShortcutContext(Qt::ApplicationShortcut);
if (!AutoRepeatCommands.contains(command)) {
shortcut->setAutoRepeat(false);
@@ -516,20 +625,26 @@ void Manager::set(const QString &keys, Command command, bool replace) {
shortcut->setEnabled(false);
}
auto object = shortcut.get();
- auto i = _shortcuts.find(result);
+ auto i = _shortcuts.find(keys);
if (i == end(_shortcuts)) {
- i = _shortcuts.emplace(result, std::move(shortcut)).first;
+ i = _shortcuts.emplace(keys, std::move(shortcut)).first;
} else if (replace) {
unregister(std::exchange(i->second, std::move(shortcut)));
} else {
object = i->second.get();
}
_commandByObject.emplace(object, command);
- if (!shortcut && isMediaShortcut) {
- _mediaShortcuts.emplace(i->second.get());
- }
- if (!shortcut && isSupportShortcut) {
- _supportShortcuts.emplace(i->second.get());
+ if (!shortcut) { // Added the new one.
+ if (isMediaShortcut) {
+ _mediaShortcuts.emplace(i->second.get());
+ }
+ if (isSupportShortcut) {
+ _supportShortcuts.emplace(i->second.get());
+ }
+ pruneListened();
+ for (const auto &widget : _listened) {
+ widget->addAction(i->second.get());
+ }
}
}
@@ -543,7 +658,11 @@ void Manager::remove(const QString &keys) {
_errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
return;
}
- const auto i = _shortcuts.find(result);
+ remove(result);
+}
+
+void Manager::remove(const QKeySequence &keys) {
+ const auto i = _shortcuts.find(keys);
if (i != end(_shortcuts)) {
unregister(std::move(i->second));
_shortcuts.erase(i);
@@ -552,7 +671,7 @@ void Manager::remove(const QString &keys) {
void Manager::unregister(base::unique_qptr shortcut) {
if (shortcut) {
- _commandByObject.erase(shortcut.get());
+ _commandByObject.removeAll(shortcut.get());
_mediaShortcuts.erase(shortcut.get());
_supportShortcuts.erase(shortcut.get());
}
@@ -598,7 +717,9 @@ bool Launch(Command command) {
}
bool Launch(std::vector commands) {
- if (auto handler = RequestHandler(std::move(commands))) {
+ if (Paused) {
+ return false;
+ } else if (auto handler = RequestHandler(std::move(commands))) {
return handler();
}
return false;
@@ -630,6 +751,69 @@ void ToggleSupportShortcuts(bool toggled) {
Data.toggleSupport(toggled);
}
+void Pause() {
+ Paused = true;
+}
+
+void Unpause() {
+ Paused = false;
+}
+
+auto KeysDefaults()
+-> base::flat_map> {
+ return Data.keysDefaults();
+}
+
+auto KeysCurrents()
+-> base::flat_map> {
+ return Data.keysCurrents();
+}
+
+void Change(
+ QKeySequence was,
+ QKeySequence now,
+ Command command,
+ std::optional restore) {
+ Data.change(was, now, command, restore);
+}
+
+void ResetToDefaults() {
+ Data.resetToDefaults();
+}
+
+bool AllowWithoutModifiers(int key) {
+ const auto service = {
+ Qt::Key_Escape,
+ Qt::Key_Tab,
+ Qt::Key_Backtab,
+ Qt::Key_Backspace,
+ Qt::Key_Return,
+ Qt::Key_Enter,
+ Qt::Key_Insert,
+ Qt::Key_Delete,
+ Qt::Key_Pause,
+ Qt::Key_Print,
+ Qt::Key_SysReq,
+ Qt::Key_Clear,
+ Qt::Key_Home,
+ Qt::Key_End,
+ Qt::Key_Left,
+ Qt::Key_Up,
+ Qt::Key_Right,
+ Qt::Key_Down,
+ Qt::Key_PageUp,
+ Qt::Key_PageDown,
+ Qt::Key_Shift,
+ Qt::Key_Control,
+ Qt::Key_Meta,
+ Qt::Key_Alt,
+ Qt::Key_CapsLock,
+ Qt::Key_NumLock,
+ Qt::Key_ScrollLock,
+ };
+ return (key >= 0x80) && !ranges::contains(service, key);
+}
+
void Finish() {
Data.clear();
}
diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h
index 5355d1681..70c8eb63a 100644
--- a/Telegram/SourceFiles/core/shortcuts.h
+++ b/Telegram/SourceFiles/core/shortcuts.h
@@ -139,4 +139,21 @@ void ToggleMediaShortcuts(bool toggled);
// have some conflicts with default input shortcuts, like Ctrl+Delete.
void ToggleSupportShortcuts(bool toggled);
+void Pause();
+void Unpause();
+
+[[nodiscard]] auto KeysDefaults()
+-> base::flat_map>;
+[[nodiscard]] auto KeysCurrents()
+-> base::flat_map>;
+
+void Change(
+ QKeySequence was,
+ QKeySequence now,
+ Command command,
+ std::optional restore = {});
+void ResetToDefaults();
+
+[[nodiscard]] bool AllowWithoutModifiers(int key);
+
} // namespace Shortcuts
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index ba95d96d5..68e59591d 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
-constexpr auto AppVersion = 5010007;
-constexpr auto AppVersionStr = "5.10.7";
+constexpr auto AppVersion = 5011001;
+constexpr auto AppVersionStr = "5.11.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp
index 22041523d..79b9fe4e6 100644
--- a/Telegram/SourceFiles/data/data_channel.cpp
+++ b/Telegram/SourceFiles/data/data_channel.cpp
@@ -1012,7 +1012,7 @@ PeerId ChannelData::groupCallDefaultJoinAs() const {
void ChannelData::setAllowedReactions(Data::AllowedReactions value) {
if (_allowedReactions != value) {
if (value.paidEnabled) {
- session().api().globalPrivacy().loadPaidReactionAnonymous();
+ session().api().globalPrivacy().loadPaidReactionShownPeer();
}
const auto enabled = [](const Data::AllowedReactions &allowed) {
return (allowed.type != Data::AllowedReactionsType::Some)
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index c14d29bfc..c6befff04 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_streaming.h"
#include "data/data_document_media.h"
#include "data/data_reply_preview.h"
+#include "data/data_web_page.h"
#include "lang/lang_keys.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "main/main_session.h"
@@ -1881,3 +1882,18 @@ void DocumentData::collectLocalData(not_null local) {
session().local().writeFileLocation(mediaKey(), _location);
}
}
+
+PhotoData *LookupVideoCover(
+ not_null document,
+ HistoryItem *item) {
+ const auto media = item ? item->media() : nullptr;
+ if (const auto webpage = media ? media->webpage() : nullptr) {
+ if (webpage->document == document && webpage->photoIsVideoCover) {
+ return webpage->photo;
+ }
+ return nullptr;
+ }
+ return (media && media->document() == document)
+ ? media->videoCover()
+ : nullptr;
+}
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 29f4bde56..eb01b6076 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_cloud_file.h"
#include "core/file_location.h"
+class HistoryItem;
+class PhotoData;
enum class ChatRestriction;
class mtpFileLoader;
@@ -402,6 +404,10 @@ private:
};
+[[nodiscard]] PhotoData *LookupVideoCover(
+ not_null document,
+ HistoryItem *item);
+
VoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit);
QByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform);
diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp
index 06c39cb91..e92845840 100644
--- a/Telegram/SourceFiles/data/data_file_origin.cpp
+++ b/Telegram/SourceFiles/data/data_file_origin.cpp
@@ -105,6 +105,7 @@ struct FileReferenceAccumulator {
push(data.vphoto());
}, [&](const MTPDmessageMediaDocument &data) {
push(data.vdocument());
+ push(data.vvideo_cover());
push(data.valt_documents());
}, [&](const MTPDmessageMediaWebPage &data) {
push(data.vwebpage());
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index b3d57000e..02c129987 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -447,16 +447,17 @@ Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
auto result = Call();
result.finishReason = [&] {
if (const auto reason = call.vreason()) {
- switch (reason->type()) {
- case mtpc_phoneCallDiscardReasonBusy:
+ return reason->match([](const MTPDphoneCallDiscardReasonBusy &) {
return CallFinishReason::Busy;
- case mtpc_phoneCallDiscardReasonDisconnect:
+ }, [](const MTPDphoneCallDiscardReasonDisconnect &) {
return CallFinishReason::Disconnected;
- case mtpc_phoneCallDiscardReasonHangup:
+ }, [](const MTPDphoneCallDiscardReasonHangup &) {
return CallFinishReason::Hangup;
- case mtpc_phoneCallDiscardReasonMissed:
+ }, [](const MTPDphoneCallDiscardReasonMissed &) {
return CallFinishReason::Missed;
- }
+ }, [](const MTPDphoneCallDiscardReasonAllowGroupCall &) {
+ return CallFinishReason::AllowGroupCall;
+ });
Unexpected("Call reason type.");
}
return CallFinishReason::Hangup;
@@ -552,6 +553,14 @@ DocumentData *Media::document() const {
return nullptr;
}
+PhotoData *Media::videoCover() const {
+ return nullptr;
+}
+
+TimeId Media::videoTimestamp() const {
+ return 0;
+}
+
bool Media::hasQualitiesList() const {
return false;
}
@@ -968,17 +977,16 @@ std::unique_ptr MediaPhoto::createView(
MediaFile::MediaFile(
not_null parent,
not_null document,
- bool skipPremiumEffect,
- bool hasQualitiesList,
- bool spoiler,
- crl::time ttlSeconds)
+ Args &&args)
: Media(parent)
, _document(document)
+, _videoCover(args.videoCover)
+, _ttlSeconds(args.ttlSeconds)
, _emoji(document->sticker() ? document->sticker()->alt : QString())
-, _skipPremiumEffect(skipPremiumEffect)
-, _hasQualitiesList(hasQualitiesList)
-, _spoiler(spoiler)
-, _ttlSeconds(ttlSeconds) {
+, _videoTimestamp(args.videoTimestamp)
+, _skipPremiumEffect(args.skipPremiumEffect)
+, _hasQualitiesList(args.hasQualitiesList)
+, _spoiler(args.spoiler) {
parent->history()->owner().registerDocumentItem(_document, parent);
if (!_emoji.isEmpty()) {
@@ -1002,19 +1010,28 @@ MediaFile::~MediaFile() {
}
std::unique_ptr MediaFile::clone(not_null parent) {
- return std::make_unique(
- parent,
- _document,
- !_document->session().premium(),
- _hasQualitiesList,
- _spoiler,
- _ttlSeconds);
+ return std::make_unique(parent, _document, MediaFile::Args{
+ .ttlSeconds = _ttlSeconds,
+ .videoCover = _videoCover,
+ .videoTimestamp = _videoTimestamp,
+ .hasQualitiesList = _hasQualitiesList,
+ .skipPremiumEffect = !_document->session().premium(),
+ .spoiler = _spoiler,
+ });
}
DocumentData *MediaFile::document() const {
return _document;
}
+PhotoData *MediaFile::videoCover() const {
+ return _videoCover;
+}
+
+TimeId MediaFile::videoTimestamp() const {
+ return _videoTimestamp;
+}
+
bool MediaFile::hasQualitiesList() const {
return _hasQualitiesList;
}
@@ -1285,7 +1302,11 @@ bool MediaFile::updateSentMedia(const MTPMessageMedia &media) {
"or with ttl_seconds in updateSentMedia()"));
return false;
}
- parent()->history()->owner().documentConvert(_document, *content);
+ const auto owner = &parent()->history()->owner();
+ owner->documentConvert(_document, *content);
+ if (const auto cover = _videoCover ? data.vvideo_cover() : nullptr) {
+ owner->photoConvert(_videoCover, *cover);
+ }
return true;
}
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 739cc7839..b6e2a32bb 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -46,6 +46,7 @@ enum class CallFinishReason : char {
Busy,
Disconnected,
Hangup,
+ AllowGroupCall,
};
struct SharedContact final {
@@ -178,6 +179,8 @@ public:
virtual std::unique_ptr clone(not_null parent) = 0;
virtual DocumentData *document() const;
+ virtual PhotoData *videoCover() const;
+ virtual TimeId videoTimestamp() const;
virtual bool hasQualitiesList() const;
virtual PhotoData *photo() const;
virtual WebPageData *webpage() const;
@@ -297,18 +300,26 @@ private:
class MediaFile final : public Media {
public:
+ struct Args {
+ crl::time ttlSeconds = 0;
+ PhotoData *videoCover = nullptr;
+ TimeId videoTimestamp = 0;
+ bool hasQualitiesList = false;
+ bool skipPremiumEffect = false;
+ bool spoiler = false;
+ };
+
MediaFile(
not_null parent,
not_null document,
- bool skipPremiumEffect,
- bool hasQualitiesList,
- bool spoiler,
- crl::time ttlSeconds);
+ Args &&args);
~MediaFile();
std::unique_ptr clone(not_null parent) override;
DocumentData *document() const override;
+ PhotoData *videoCover() const override;
+ TimeId videoTimestamp() const override;
bool hasQualitiesList() const override;
bool uploading() const override;
@@ -338,14 +349,17 @@ public:
private:
not_null _document;
- QString _emoji;
- bool _skipPremiumEffect = false;
- bool _hasQualitiesList = false;
- bool _spoiler = false;
+ PhotoData *_videoCover = nullptr;
// Video (unsupported) / Voice / Round.
crl::time _ttlSeconds = 0;
+ QString _emoji;
+ TimeId _videoTimestamp = 0;
+ bool _skipPremiumEffect = false;
+ bool _hasQualitiesList = false;
+ bool _spoiler = false;
+
};
class MediaContact final : public Media {
diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp
index f0166b048..6445533e8 100644
--- a/Telegram/SourceFiles/data/data_message_reactions.cpp
+++ b/Telegram/SourceFiles/data/data_message_reactions.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_app_config.h"
#include "main/session/send_as_peers.h"
#include "data/components/credits.h"
+#include "data/data_channel.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_histories.h"
@@ -158,8 +159,23 @@ constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500;
return (i != end(top)) && i->my;
}
-[[nodiscard]] std::optional MaybeAnonymous(uint32 privacySet, uint32 anonymous) {
- return privacySet ? (anonymous == 1) : std::optional();
+[[nodiscard]] std::optional MaybeShownPeer(
+ uint32 privacySet,
+ PeerId shownPeer) {
+ return privacySet ? shownPeer : std::optional();
+}
+
+[[nodiscard]] MTPPaidReactionPrivacy PaidReactionShownPeerToTL(
+ not_null session,
+ std::optional shownPeer) {
+ return !shownPeer
+ ? MTPPaidReactionPrivacy()
+ : !*shownPeer
+ ? MTP_paidReactionPrivacyAnonymous()
+ : (*shownPeer == session->userPeerId())
+ ? MTP_paidReactionPrivacyDefault()
+ : MTP_paidReactionPrivacyPeer(
+ session->data().peer(*shownPeer)->input);
}
} // namespace
@@ -180,6 +196,13 @@ PossibleItemReactionsRef LookupPossibleReactions(
}
}
const auto session = &peer->session();
+ if (const auto channel = peer->asChannel()) {
+ if ((!channel->amCreator())
+ && (channel->adminRights() & ChatAdminRight::Anonymous)
+ && (session->sendAsPeers().resolveChosen(channel) == channel)) {
+ return {};
+ }
+ }
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active);
const auto &top = reactions->list(Reactions::Type::Top);
@@ -1752,7 +1775,7 @@ void Reactions::sendPaidPrivacyRequest(
not_null item,
PaidReactionSend send) {
Expects(!_sendingPaid.contains(item));
- Expects(send.anonymous.has_value());
+ Expects(send.shownPeer.has_value());
Expects(!send.count);
const auto id = item->fullId();
@@ -1761,7 +1784,7 @@ void Reactions::sendPaidPrivacyRequest(
MTPmessages_TogglePaidReactionPrivacy(
item->history()->peer->input,
MTP_int(id.msg),
- MTP_bool(*send.anonymous))
+ PaidReactionShownPeerToTL(&_owner->session(), send.shownPeer))
).done([=] {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
@@ -1795,12 +1818,14 @@ void Reactions::sendPaidRequest(
auto &api = _owner->session().api();
using Flag = MTPmessages_SendPaidReaction::Flag;
const auto requestId = api.request(MTPmessages_SendPaidReaction(
- MTP_flags(send.anonymous ? Flag::f_private : Flag()),
+ MTP_flags(send.shownPeer ? Flag::f_private : Flag()),
item->history()->peer->input,
MTP_int(id.msg),
MTP_int(send.count),
MTP_long(randomId),
- MTP_bool(send.anonymous.value_or(false))
+ (!send.shownPeer
+ ? MTPPaidReactionPrivacy()
+ : PaidReactionShownPeerToTL(&_owner->session(), *send.shownPeer))
)).done([=](const MTPUpdates &result) {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
@@ -1851,9 +1876,9 @@ MessageReactions::~MessageReactions() {
finishPaidSending({
.count = int(paid->sending),
.valid = true,
- .anonymous = MaybeAnonymous(
+ .shownPeer = MaybeShownPeer(
paid->sendingPrivacySet,
- paid->sendingAnonymous),
+ paid->sendingShownPeer),
}, false);
}
}
@@ -2217,7 +2242,7 @@ void MessageReactions::markRead() {
void MessageReactions::scheduleSendPaid(
int count,
- std::optional anonymous) {
+ std::optional shownPeer) {
Expects(count >= 0);
if (!_paid) {
@@ -2225,9 +2250,9 @@ void MessageReactions::scheduleSendPaid(
}
_paid->scheduled += count;
_paid->scheduledFlag = 1;
- if (anonymous.has_value()) {
- _paid->scheduledAnonymous = anonymous.value_or(false) ? 1 : 0;
- _paid->scheduledPrivacySet = anonymous.has_value();
+ if (shownPeer.has_value()) {
+ _paid->scheduledShownPeer = *shownPeer;
+ _paid->scheduledPrivacySet = true;
}
if (count > 0) {
_item->history()->session().credits().lock(StarsAmount(count));
@@ -2248,7 +2273,7 @@ void MessageReactions::cancelScheduledPaid() {
}
_paid->scheduled = 0;
_paid->scheduledFlag = 0;
- _paid->scheduledAnonymous = 0;
+ _paid->scheduledShownPeer = 0;
_paid->scheduledPrivacySet = 0;
}
if (!_paid->sendingFlag && _paid->top.empty()) {
@@ -2263,18 +2288,18 @@ PaidReactionSend MessageReactions::startPaidSending() {
}
_paid->sending = _paid->scheduled;
_paid->sendingFlag = _paid->scheduledFlag;
- _paid->sendingAnonymous = _paid->scheduledAnonymous;
+ _paid->sendingShownPeer = _paid->scheduledShownPeer;
_paid->sendingPrivacySet = _paid->scheduledPrivacySet;
_paid->scheduled = 0;
_paid->scheduledFlag = 0;
- _paid->scheduledAnonymous = 0;
+ _paid->scheduledShownPeer = 0;
_paid->scheduledPrivacySet = 0;
return {
.count = int(_paid->sending),
.valid = true,
- .anonymous = MaybeAnonymous(
+ .shownPeer = MaybeShownPeer(
_paid->sendingPrivacySet,
- _paid->sendingAnonymous),
+ _paid->sendingShownPeer),
};
}
@@ -2284,13 +2309,13 @@ void MessageReactions::finishPaidSending(
Expects(_paid != nullptr);
Expects(send.count == _paid->sending);
Expects(send.valid == (_paid->sendingFlag == 1));
- Expects(send.anonymous == MaybeAnonymous(
+ Expects(send.shownPeer == MaybeShownPeer(
_paid->sendingPrivacySet,
- _paid->sendingAnonymous));
+ _paid->sendingShownPeer));
_paid->sending = 0;
_paid->sendingFlag = 0;
- _paid->sendingAnonymous = 0;
+ _paid->sendingShownPeer = 0;
_paid->sendingPrivacySet = 0;
if (!_paid->scheduledFlag && _paid->top.empty()) {
_paid = nullptr;
@@ -2299,9 +2324,9 @@ void MessageReactions::finishPaidSending(
return top.my;
});
if (i != end(_paid->top)) {
- i->peer = send.anonymous
- ? nullptr
- : _item->history()->session().user().get();
+ i->peer = send.shownPeer
+ ? _item->history()->owner().peer(*send.shownPeer).get()
+ : nullptr;
}
}
if (const auto amount = send.count) {
@@ -2322,22 +2347,23 @@ int MessageReactions::localPaidCount() const {
return _paid ? (_paid->scheduled + _paid->sending) : 0;
}
-bool MessageReactions::localPaidAnonymous() const {
- const auto minePaidAnonymous = [&] {
+PeerId MessageReactions::localPaidShownPeer() const {
+ const auto minePaidShownPeer = [&] {
for (const auto &entry : _paid->top) {
if (entry.my) {
- return !entry.peer;
+ return entry.peer ? entry.peer->id : PeerId();
}
}
const auto api = &_item->history()->session().api();
- return api->globalPrivacy().paidReactionAnonymousCurrent();
+ return api->globalPrivacy().paidReactionShownPeerCurrent();
};
- return _paid
- && ((_paid->scheduledFlag && _paid->scheduledPrivacySet)
- ? (_paid->scheduledAnonymous == 1)
- : (_paid->sendingFlag && _paid->sendingPrivacySet)
- ? (_paid->sendingAnonymous == 1)
- : minePaidAnonymous());
+ return !_paid
+ ? PeerId()
+ : (_paid->scheduledFlag && _paid->scheduledPrivacySet)
+ ? _paid->scheduledShownPeer
+ : (_paid->sendingFlag && _paid->sendingPrivacySet)
+ ? _paid->sendingShownPeer
+ : minePaidShownPeer();
}
bool MessageReactions::clearCloudData() {
diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h
index f1bf36f46..e931ed20e 100644
--- a/Telegram/SourceFiles/data/data_message_reactions.h
+++ b/Telegram/SourceFiles/data/data_message_reactions.h
@@ -71,7 +71,7 @@ struct MyTagInfo {
struct PaidReactionSend {
int count = 0;
bool valid = false;
- std::optional anonymous = false;
+ std::optional shownPeer = PeerId();
};
class Reactions final : private CustomEmojiManager::Listener {
@@ -409,7 +409,7 @@ public:
[[nodiscard]] bool hasUnread() const;
void markRead();
- void scheduleSendPaid(int count, std::optional anonymous);
+ void scheduleSendPaid(int count, std::optional shownPeer);
[[nodiscard]] int scheduledPaid() const;
void cancelScheduledPaid();
@@ -418,19 +418,19 @@ public:
[[nodiscard]] bool localPaidData() const;
[[nodiscard]] int localPaidCount() const;
- [[nodiscard]] bool localPaidAnonymous() const;
+ [[nodiscard]] PeerId localPaidShownPeer() const;
bool clearCloudData();
private:
struct Paid {
std::vector top;
- uint32 scheduled: 29 = 0;
+ PeerId scheduledShownPeer = 0;
+ PeerId sendingShownPeer = 0;
+ uint32 scheduled: 30 = 0;
uint32 scheduledFlag : 1 = 0;
- uint32 scheduledAnonymous : 1 = 0;
uint32 scheduledPrivacySet : 1 = 0;
- uint32 sending : 29 = 0;
+ uint32 sending : 30 = 0;
uint32 sendingFlag : 1 = 0;
- uint32 sendingAnonymous : 1 = 0;
uint32 sendingPrivacySet : 1 = 0;
};
const not_null _item;
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 1d30e1b22..bb4eecaf8 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3524,6 +3524,7 @@ not_null Session::processWebpage(
0,
QString(),
false,
+ false,
data.vdate().v
? data.vdate().v
: (base::unixtime::now() + kDefaultPendingTimeout));
@@ -3551,6 +3552,7 @@ not_null Session::webpage(
0,
QString(),
false,
+ false,
TimeId(0));
}
@@ -3571,6 +3573,7 @@ not_null Session::webpage(
int duration,
const QString &author,
bool hasLargeMedia,
+ bool photoIsVideoCover,
TimeId pendingTill) {
const auto result = webpage(id);
webpageApplyFields(
@@ -3591,6 +3594,7 @@ not_null Session::webpage(
duration,
author,
hasLargeMedia,
+ photoIsVideoCover,
pendingTill);
return result;
}
@@ -3778,6 +3782,7 @@ void Session::webpageApplyFields(
data.vduration().value_or_empty(),
qs(data.vauthor().value_or_empty()),
data.is_has_large_media(),
+ data.is_video_cover_photo(),
pendingTill);
}
@@ -3799,6 +3804,7 @@ void Session::webpageApplyFields(
int duration,
const QString &author,
bool hasLargeMedia,
+ bool photoIsVideoCover,
TimeId pendingTill) {
const auto requestPending = (!page->pendingTill && pendingTill > 0);
const auto changed = page->applyChanges(
@@ -3818,6 +3824,7 @@ void Session::webpageApplyFields(
duration,
author,
hasLargeMedia,
+ photoIsVideoCover,
pendingTill);
if (requestPending) {
_session->api().requestWebPageDelayed(page);
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index bf1520a59..3e5392173 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -631,6 +631,7 @@ public:
int duration,
const QString &author,
bool hasLargeMedia,
+ bool photoIsVideoCover,
TimeId pendingTill);
[[nodiscard]] not_null game(GameId id);
@@ -916,6 +917,7 @@ private:
int duration,
const QString &author,
bool hasLargeMedia,
+ bool photoIsVideoCover,
TimeId pendingTill);
void gameApplyFields(
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 7dff948f5..b3a539eda 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -316,7 +316,7 @@ void Stories::scheduleExpireTimer() {
const auto nearest = _expiring.front().first;
const auto now = base::unixtime::now();
const auto delay = (nearest > now)
- ? (nearest - now)
+ ? std::min(nearest - now, 86'400)
: 0;
_expireTimer.callOnce(delay * crl::time(1000));
}
diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp
index 5ab3f6b2f..bd14735eb 100644
--- a/Telegram/SourceFiles/data/data_web_page.cpp
+++ b/Telegram/SourceFiles/data/data_web_page.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo.h"
#include "data/data_channel.h"
#include "data/data_document.h"
+#include "core/local_url_handlers.h"
#include "lang/lang_keys.h"
#include "iv/iv_data.h"
#include "ui/image/image.h"
@@ -228,6 +229,7 @@ bool WebPageData::applyChanges(
int newDuration,
const QString &newAuthor,
bool newHasLargeMedia,
+ bool newPhotoIsVideoCover,
int newPendingTill) {
if (newPendingTill != 0
&& (!url.isEmpty() || failed)
@@ -265,6 +267,9 @@ bool WebPageData::applyChanges(
|| (hasSiteName + hasTitle + hasDescription < 2)) {
newHasLargeMedia = false;
}
+ if (!newDocument || !newDocument->isVideoFile() || !newPhoto) {
+ newPhotoIsVideoCover = false;
+ }
if (type == newType
&& url == resultUrl
@@ -283,6 +288,7 @@ bool WebPageData::applyChanges(
&& duration == newDuration
&& author == resultAuthor
&& hasLargeMedia == (newHasLargeMedia ? 1 : 0)
+ && photoIsVideoCover == (newPhotoIsVideoCover ? 1 : 0)
&& pendingTill == newPendingTill) {
return false;
}
@@ -291,6 +297,7 @@ bool WebPageData::applyChanges(
}
type = newType;
hasLargeMedia = newHasLargeMedia ? 1 : 0;
+ photoIsVideoCover = newPhotoIsVideoCover ? 1 : 0;
url = resultUrl;
displayUrl = resultDisplayUrl;
siteName = resultSiteName;
@@ -374,6 +381,21 @@ QString WebPageData::displayedSiteName() const {
: siteName;
}
+TimeId WebPageData::extractVideoTimestamp() const {
+ const auto take = [&](const QStringList &list, int index) {
+ return (index >= 0 && index < list.size()) ? list[index] : QString();
+ };
+ const auto hashed = take(url.split('#'), 0);
+ const auto params = take(hashed.split('?'), 1);
+ const auto parts = params.split('&');
+ for (const auto &part : parts) {
+ if (part.startsWith(u"t="_q)) {
+ return Core::ParseVideoTimestamp(part.mid(2));
+ }
+ }
+ return 0;
+}
+
bool WebPageData::computeDefaultSmallMedia() const {
if (!collage.items.empty()) {
return false;
@@ -382,7 +404,8 @@ bool WebPageData::computeDefaultSmallMedia() const {
&& description.empty()
&& author.isEmpty()) {
return false;
- } else if (!document
+ } else if (!uniqueGift
+ && !document
&& photo
&& type != WebPageType::Photo
&& type != WebPageType::Document
diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h
index f97c79474..f8a19f1c1 100644
--- a/Telegram/SourceFiles/data/data_web_page.h
+++ b/Telegram/SourceFiles/data/data_web_page.h
@@ -106,6 +106,7 @@ struct WebPageData {
int newDuration,
const QString &newAuthor,
bool newHasLargeMedia,
+ bool newPhotoIsVideoCover,
int newPendingTill);
static void ApplyChanges(
@@ -114,6 +115,7 @@ struct WebPageData {
const MTPmessages_Messages &result);
[[nodiscard]] QString displayedSiteName() const;
+ [[nodiscard]] TimeId extractVideoTimestamp() const;
[[nodiscard]] bool computeDefaultSmallMedia() const;
[[nodiscard]] bool suggestEnlargePhoto() const;
@@ -134,7 +136,8 @@ struct WebPageData {
std::shared_ptr uniqueGift;
int duration = 0;
TimeId pendingTill = 0;
- uint32 version : 30 = 0;
+ uint32 version : 29 = 0;
+ uint32 photoIsVideoCover : 1 = 0;
uint32 hasLargeMedia : 1 = 0;
uint32 failed : 1 = 0;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 05c774c99..a9871ded2 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -285,6 +285,7 @@ InnerWidget::InnerWidget(
) | rpl::start_with_next([=] {
_topicJumpCache = nullptr;
_chatsFilterTags.clear();
+ _rightButtons.clear();
}, lifetime());
session().downloaderTaskFinished(
@@ -2090,6 +2091,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) {
const auto draggingHeight = _dragging->height();
auto yaddWas = _pinnedRows[_draggingIndex].yadd.current();
auto shift = 0;
+ auto shiftHeight = 0;
auto now = crl::now();
if (_dragStart.y() > localPosition.y() && _draggingIndex > 0) {
shift = -floorclamp(_dragStart.y() - localPosition.y() + (draggingHeight / 2), draggingHeight, 0, _draggingIndex);
@@ -2099,6 +2101,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) {
std::swap(_pinnedRows[from], _pinnedRows[from - 1]);
_pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() - draggingHeight, 0);
_pinnedRows[from].animStartTime = now;
+ shiftHeight -= (*(_shownList->cbegin() + from))->height();
}
} else if (_dragStart.y() < localPosition.y() && _draggingIndex + 1 < pinnedCount) {
shift = floorclamp(localPosition.y() - _dragStart.y() + (draggingHeight / 2), draggingHeight, 0, pinnedCount - _draggingIndex - 1);
@@ -2108,18 +2111,21 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) {
std::swap(_pinnedRows[from], _pinnedRows[from + 1]);
_pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() + draggingHeight, 0);
_pinnedRows[from].animStartTime = now;
+ shiftHeight += (*(_shownList->cbegin() + from))->height();
}
}
if (shift) {
_draggingIndex += shift;
_aboveIndex = _draggingIndex;
- _dragStart.setY(_dragStart.y() + shift * _st->height);
+ _dragStart.setY(_dragStart.y() + shiftHeight);
if (!_pinnedShiftAnimation.animating()) {
_pinnedShiftAnimation.start();
}
}
_aboveTopShift = qCeil(_pinnedRows[_aboveIndex].yadd.current());
- _pinnedRows[_draggingIndex].yadd = anim::value(yaddWas - shift * _st->height, localPosition.y() - _dragStart.y());
+ _pinnedRows[_draggingIndex].yadd = anim::value(
+ yaddWas - shiftHeight,
+ localPosition.y() - _dragStart.y());
if (!_pinnedRows[_draggingIndex].animStartTime) {
_pinnedRows[_draggingIndex].yadd.finish();
}
@@ -2170,7 +2176,7 @@ bool InnerWidget::pinnedShiftAnimationCallback(crl::time now) {
}
if (updateMin >= 0) {
const auto minHeight = _st->height;
- const auto maxHeight = st::forumDialogRow.height;
+ const auto maxHeight = st::taggedForumDialogRow.height;
auto top = pinnedOffset();
auto updateFrom = top + minHeight * (updateMin - 1);
auto updateHeight = maxHeight * (updateMax - updateMin + 3);
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index efd201c28..a2e4d2a3f 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -470,7 +470,13 @@ void Row::PaintCornerBadgeFrame(
for (auto i = 0; i != storiesUnreadCount; ++i) {
segments.push_back({ storiesUnreadBrush, storiesUnread });
}
- Ui::PaintOutlineSegments(q, outline, segments);
+ if (peer && peer->forum()) {
+ const auto radius = context.st->photoSize
+ * Ui::ForumUserpicRadiusMultiplier();
+ Ui::PaintOutlineSegments(q, outline, radius, segments);
+ } else {
+ Ui::PaintOutlineSegments(q, outline, segments);
+ }
}
if (subscribed) {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index cceb49580..6a16476f4 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -1215,6 +1215,7 @@ void Widget::setupShortcuts() {
});
request->check(Command::ShowChatMenu, 1) && request->handle([=] {
if (_inner) {
+ Window::ActivateWindow(controller());
_inner->showPeerMenu();
}
return true;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
index db7da1fe7..c3430f185 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
@@ -442,6 +442,9 @@ void PaintRow(
const auto promoted = (history && history->useTopPromotion())
&& !context.search;
+ const auto verifyInfo = (from && !from->isSelf())
+ ? from->botVerifyDetails()
+ : nullptr;
if (promoted) {
const auto type = history->topPromotionType();
const auto custom = type.isEmpty()
@@ -453,10 +456,10 @@ void PaintRow(
? tr::lng_badge_psa_default(tr::now)
: custom;
PaintRowTopRight(p, text, rectForName, context);
- } else if (const auto info = from ? from->botVerifyDetails() : nullptr) {
- if (!rowBadge.ready(info)) {
+ } else if (verifyInfo) {
+ if (!rowBadge.ready(verifyInfo)) {
rowBadge.set(
- info,
+ verifyInfo,
from->owner().customEmojiManager().factory(),
customEmojiRepaint);
}
diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp
index 54f8369c2..b52888101 100644
--- a/Telegram/SourceFiles/editor/editor_crop.cpp
+++ b/Telegram/SourceFiles/editor/editor_crop.cpp
@@ -42,6 +42,15 @@ QSizeF FlipSizeByRotation(const QSizeF &size, int angle) {
return (((angle / 90) % 2) == 1) ? size.transposed() : size;
}
+[[nodiscard]] QRectF OriginalCrop(QSize outer, QSize inner) {
+ const auto size = inner.scaled(outer, Qt::KeepAspectRatio);
+ return QRectF(
+ (outer.width() - size.width()) / 2,
+ (outer.height() - size.height()) / 2,
+ size.width(),
+ size.height());
+}
+
} // namespace
Crop::Crop(
@@ -60,6 +69,8 @@ Crop::Crop(
, _data(std::move(data))
, _cropOriginal(modifications.crop.isValid()
? modifications.crop
+ : !_data.exactSize.isEmpty()
+ ? OriginalCrop(_imageSize, _data.exactSize)
: QRectF(QPoint(), _imageSize))
, _angle(modifications.angle)
, _flipped(modifications.flipped)
diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h
index 028f55c6f..b52b2110a 100644
--- a/Telegram/SourceFiles/editor/photo_editor_common.h
+++ b/Telegram/SourceFiles/editor/photo_editor_common.h
@@ -32,6 +32,7 @@ struct EditorData {
TextWithEntities about;
QString confirm;
+ QSize exactSize;
CropType cropType = CropType::Rect;
bool keepAspectRatio = false;
};
diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp
index 33ed5e837..257e47ec4 100644
--- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp
+++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp
@@ -26,22 +26,26 @@ void OpenWithPreparedFile(
std::shared_ptr show,
not_null file,
int previewWidth,
- Fn &&doneCallback) {
+ Fn &&doneCallback,
+ QSize exactSize) {
using ImageInfo = Ui::PreparedFileInformation::Image;
const auto image = std::get_if(&file->information->media);
if (!image) {
+ doneCallback(false);
return;
}
const auto photoType = (file->type == Ui::PreparedFile::Type::Photo);
const auto modifiedFileType = (file->type == Ui::PreparedFile::Type::File)
&& !image->modifications.empty();
if (!photoType && !modifiedFileType) {
+ doneCallback(false);
return;
}
const auto sideLimit = PhotoSideLimit();
- auto callback = [=, done = std::move(doneCallback)](
- const PhotoModifications &mods) {
+ const auto accepted = std::make_shared();
+ auto callback = [=](const PhotoModifications &mods) {
+ *accepted = true;
image->modifications = mods;
Storage::UpdateImageDetails(*file, previewWidth, sideLimit);
{
@@ -51,19 +55,26 @@ void OpenWithPreparedFile(
? PreparedFile::Type::Photo
: PreparedFile::Type::File;
}
- done();
+ doneCallback(true);
};
auto copy = image->data;
const auto fileImage = std::make_shared(std::move(copy));
+ const auto keepRatio = !exactSize.isEmpty();
auto editor = base::make_unique_q(
parent,
show,
show,
fileImage,
- image->modifications);
+ image->modifications,
+ EditorData{ .exactSize = exactSize, .keepAspectRatio = keepRatio });
const auto raw = editor.get();
auto layer = std::make_unique(parent, std::move(editor));
InitEditorLayer(layer.get(), raw, std::move(callback));
+ QObject::connect(layer.get(), &QObject::destroyed, [=] {
+ if (!*accepted) {
+ doneCallback(false);
+ }
+ });
show->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h
index f1c2fd445..0259a0fb7 100644
--- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h
+++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h
@@ -34,7 +34,8 @@ void OpenWithPreparedFile(
std::shared_ptr show,
not_null file,
int previewWidth,
- Fn &&doneCallback);
+ Fn &&doneCallback,
+ QSize exactSize = {});
void PrepareProfilePhoto(
not_null parent,
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index 9d40b6009..dee8d1e94 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -385,6 +385,7 @@ QByteArray SerializeMessage(
};
const auto pushPhoto = [&](const Image &image) {
pushPath(image.file, "photo");
+ push("photo_file_size", image.file.size);
if (image.width && image.height) {
push("width", image.width);
push("height", image.height);
@@ -696,8 +697,10 @@ QByteArray SerializeMessage(
}, [&](const Document &data) {
pushPath(data.file, "file");
push("file_name", data.name);
+ push("file_size", data.file.size);
if (data.thumb.width > 0) {
pushPath(data.thumb.file, "thumbnail");
+ push("thumbnail_file_size", data.thumb.file.size);
}
const auto pushType = [&](const QByteArray &value) {
push("media_type", value);
@@ -739,6 +742,7 @@ QByteArray SerializeMessage(
}));
if (!data.vcard.content.isEmpty()) {
pushPath(data.vcard, "contact_vcard");
+ push("contact_vcard_file_size", data.vcard.size);
}
}, [&](const GeoPoint &data) {
pushBare(
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index dd7a46053..4fe6bd08f 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/about_sponsored_box.h"
#include "boxes/delete_messages_box.h"
#include "boxes/report_messages_box.h"
+#include "boxes/star_gift_box.h" // ShowStarGiftBox
#include "boxes/sticker_set_box.h"
#include "boxes/translate_box.h"
#include "chat_helpers/message_field.h"
@@ -1627,6 +1628,7 @@ void HistoryInner::mouseMoveEvent(QMouseEvent *e) {
mouseReleaseEvent(e);
}
if (reallyMoved) {
+ _mouseActive = true;
lastGlobalPosition = e->globalPos();
if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) {
keepScrollDateForNow();
@@ -1673,6 +1675,7 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) {
e->accept();
return; // ignore mouse press, that was hiding context menu
}
+ _mouseActive = true;
mouseActionStart(e->globalPos(), e->button());
}
@@ -2835,6 +2838,29 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
_menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] {
QGuiApplication::clipboard()->setText(phone);
}, &st::menuIconCopy);
+ } else if (const auto gift = media->gift()) {
+ const auto peer = item->history()->peer;
+ const auto user = peer->asUser();
+ if (!user
+ || (!user->isInaccessible()
+ && !user->isNotificationsUser())) {
+ const auto controller = _controller;
+ const auto starGiftUpgrade = gift->upgrade
+ && (gift->type == Data::GiftType::StarGift);
+ const auto isGift = gift->slug.isEmpty()
+ || !gift->channel;
+ const auto out = item->out();
+ const auto outgoingGift = isGift
+ && (starGiftUpgrade ? !out : out);
+ if (outgoingGift) {
+ _menu->addAction(
+ tr::lng_context_gift_send(tr::now),
+ [=] {
+ Ui::ShowStarGiftBox(controller, peer);
+ },
+ &st::menuIconGiftPremium);
+ }
+ }
}
}
if (!item->isService() && view && actionText.isEmpty()) {
@@ -3314,7 +3340,7 @@ void HistoryInner::checkActivation() {
session().data().histories().readInboxTill(view->data());
}
-void HistoryInner::recountHistoryGeometry() {
+void HistoryInner::recountHistoryGeometry(bool initial) {
_contentWidth = _scroll->width();
if (_history->hasPendingResizedItems()
@@ -3372,7 +3398,7 @@ void HistoryInner::recountHistoryGeometry() {
}
auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop);
- if (historyPaddingTopDelta != 0) {
+ if (!initial && historyPaddingTopDelta != 0) {
if (_history->scrollTopItem) {
_history->scrollTopOffset += historyPaddingTopDelta;
} else if (_migrated && _migrated->scrollTopItem) {
@@ -3567,6 +3593,7 @@ void HistoryInner::setShownPinned(HistoryItem *item) {
}
void HistoryInner::enterEventHook(QEnterEvent *e) {
+ _mouseActive = true;
mouseActionUpdate(QCursor::pos());
return TWidget::enterEventHook(e);
}
@@ -3583,6 +3610,7 @@ void HistoryInner::leaveEventHook(QEvent *e) {
_cursor = style::cur_default;
setCursor(_cursor);
}
+ _mouseActive = false;
return TWidget::leaveEventHook(e);
}
@@ -3933,7 +3961,7 @@ auto HistoryInner::reactionButtonParameters(
}
void HistoryInner::mouseActionUpdate() {
- if (hasPendingResizedItems()) {
+ if (hasPendingResizedItems() || !_mouseActive) {
return;
}
diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h
index 4f0ba7bfc..cec98e8b3 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.h
+++ b/Telegram/SourceFiles/history/history_inner_widget.h
@@ -121,7 +121,7 @@ public:
void setItemsRevealHeight(int revealHeight);
void changeItemsRevealHeight(int revealHeight);
void checkActivation();
- void recountHistoryGeometry();
+ void recountHistoryGeometry(bool initial = false);
void updateSize();
void setShownPinned(HistoryItem *item);
@@ -499,6 +499,7 @@ private:
HistoryItem *_dragStateItem = nullptr;
CursorState _mouseCursorState = CursorState();
uint16 _mouseTextSymbol = 0;
+ bool _mouseActive = false;
bool _dragStateUserpic = false;
bool _pressWasInactive = false;
bool _recountedAfterPendingResizedItems = false;
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index f71327eeb..cd253c6a8 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -310,13 +310,19 @@ std::unique_ptr HistoryItem::CreateMedia(
}
return document->match([&](const MTPDdocument &document) -> Result {
const auto list = media.valt_documents();
- return std::make_unique(
- item,
- item->history()->owner().processDocument(document, list),
- media.is_nopremium(),
- list && !list->v.isEmpty(),
- media.is_spoiler(),
- media.vttl_seconds().value_or_empty());
+ const auto owner = &item->history()->owner();
+ const auto data = owner->processDocument(document);
+ using Args = Data::MediaFile::Args;
+ return std::make_unique(item, data, Args{
+ .ttlSeconds = media.vttl_seconds().value_or_empty(),
+ .videoCover = (media.vvideo_cover()
+ ? owner->processPhoto(*media.vvideo_cover()).get()
+ : nullptr),
+ .videoTimestamp = media.vvideo_timestamp().value_or_empty(),
+ .hasQualitiesList = list && !list->v.isEmpty(),
+ .skipPremiumEffect = media.is_nopremium(),
+ .spoiler = media.is_spoiler(),
+ });
}, [](const MTPDdocumentEmpty &) -> Result {
return nullptr;
});
@@ -693,16 +699,12 @@ HistoryItem::HistoryItem(
: HistoryItem(history, fields) {
createComponentsHelper(std::move(fields));
- const auto skipPremiumEffect = !history->session().premium();
const auto video = document->video();
- const auto spoiler = false;
- _media = std::make_unique(
- this,
- document,
- skipPremiumEffect,
- video && !video->qualities.empty(),
- spoiler,
- /*ttlSeconds = */0);
+ using Args = Data::MediaFile::Args;
+ _media = std::make_unique(this, document, Args{
+ .hasQualitiesList = video && !video->qualities.empty(),
+ .skipPremiumEffect = !history->session().premium(),
+ });
setText(caption);
}
@@ -764,6 +766,7 @@ HistoryItem::HistoryItem(
0,
QString(),
false,
+ false,
0);
auto webpageMedia = std::make_unique(
this,
@@ -1903,17 +1906,12 @@ void HistoryItem::applyChanges(not_null story) {
}
void HistoryItem::setStoryFields(not_null story) {
- const auto spoiler = false;
if (const auto photo = story->photo()) {
+ const auto spoiler = false;
_media = std::make_unique(this, photo, spoiler);
} else if (const auto document = story->document()) {
- _media = std::make_unique(
- this,
- document,
- /*skipPremiumEffect=*/false,
- /*hasQualitiesList=*/false,
- spoiler,
- /*ttlSeconds = */0);
+ using Args = Data::MediaFile::Args;
+ _media = std::make_unique(this, document, Args{});
}
setText(story->caption());
if (story->pinnedToTop()) {
@@ -2693,14 +2691,16 @@ bool HistoryItem::canReact() const {
return true;
}
-void HistoryItem::addPaidReaction(int count, std::optional anonymous) {
+void HistoryItem::addPaidReaction(
+ int count,
+ std::optional shownPeer) {
Expects(count >= 0);
Expects(_history->peer->isBroadcast() || isDiscussionPost());
if (!_reactions) {
_reactions = std::make_unique(this);
}
- _reactions->scheduleSendPaid(count, anonymous);
+ _reactions->scheduleSendPaid(count, shownPeer);
if (count > 0) {
_history->owner().notifyItemDataChange(this);
}
@@ -2796,8 +2796,10 @@ int HistoryItem::reactionsPaidScheduled() const {
return _reactions ? _reactions->scheduledPaid() : 0;
}
-bool HistoryItem::reactionsLocalAnonymous() const {
- return _reactions ? _reactions->localPaidAnonymous() : false;
+PeerId HistoryItem::reactionsLocalShownPeer() const {
+ return _reactions
+ ? _reactions->localPaidShownPeer()
+ : _history->session().userPeerId();
}
bool HistoryItem::reactionsAreTags() const {
@@ -2825,9 +2827,8 @@ auto HistoryItem::topPaidReactionsWithLocal() const
result,
[](const TopPaid &entry) { return entry.my != 0; });
const auto peerForMine = [&] {
- return _reactions->localPaidAnonymous()
- ? nullptr
- : history()->session().user().get();
+ const auto peerId = _reactions->localPaidShownPeer();
+ return peerId ? history()->owner().peer(peerId).get() : nullptr;
};
if (const auto local = _reactions->localPaidCount()) {
const auto top = [&](int mine) {
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index dd35aa65c..4c4624d6a 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -458,7 +458,7 @@ public:
void toggleReaction(
const Data::ReactionId &reaction,
HistoryReactionSource source);
- void addPaidReaction(int count, std::optional anonymous = {});
+ void addPaidReaction(int count, std::optional shownPeer = {});
void cancelScheduledPaidReaction();
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
void finishPaidReactionSending(
@@ -476,7 +476,7 @@ public:
[[nodiscard]] auto topPaidReactionsWithLocal() const
-> std::vector;
[[nodiscard]] int reactionsPaidScheduled() const;
- [[nodiscard]] bool reactionsLocalAnonymous() const;
+ [[nodiscard]] PeerId reactionsLocalShownPeer() const;
[[nodiscard]] bool canViewReactions() const;
[[nodiscard]] std::vector chosenReactions() const;
[[nodiscard]] Data::ReactionId lookupUnreadReaction(
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index c2f7a48a4..002e35e56 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h"
#include "base/qt/qt_key_modifiers.h"
+#include "base/options.h"
#include "lang/lang_keys.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/spoiler_mess.h"
@@ -67,8 +68,20 @@ namespace {
const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_";
+base::options::toggle FastButtonsModeOption({
+ .id = kOptionFastButtonsMode,
+ .name = "Fast buttons mode",
+ .description = "Trigger inline keyboard buttons by 1-9 keyboard keys.",
+});
+
} // namespace
+const char kOptionFastButtonsMode[] = "fast-buttons-mode";
+
+bool FastButtonsMode() {
+ return FastButtonsModeOption.value();
+}
+
void HistoryMessageVia::create(
not_null owner,
UserId userId) {
@@ -946,10 +959,10 @@ void ReplyKeyboard::paint(
}
bool ReplyKeyboard::hasFastButtonMode() const {
- return _item->inlineReplyKeyboard()
+ return FastButtonsMode()
+ && _item->inlineReplyKeyboard()
&& (_item == _item->history()->lastMessage())
- && _item->history()->session().supportMode()
- && _item->history()->session().supportHelper().fastButtonMode(
+ && _item->history()->session().fastButtonsBots().enabled(
_item->history()->peer);
}
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index c2281496b..331ee24df 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -54,6 +54,9 @@ namespace style {
struct BotKeyboardButton;
} // namespace style
+extern const char kOptionFastButtonsMode[];
+[[nodiscard]] bool FastButtonsMode();
+
struct HistoryMessageVia : public RuntimeComponent {
void create(not_null owner, UserId userId);
void resize(int32 availw) const;
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 0c0fed493..f61c61bde 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/moderate_messages_box.h"
#include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h"
+#include "boxes/star_gift_box.h"
#include "boxes/peers/edit_peer_permissions_box.h" // ShowAboutGigagroup.
#include "boxes/peers/edit_peer_requests_box.h"
#include "core/file_utilities.h"
@@ -409,6 +410,7 @@ HistoryWidget::HistoryWidget(
_joinChannel->addClickHandler([=] { joinChannel(); });
_muteUnmute->addClickHandler([=] { toggleMuteUnmute(); });
_discuss->addClickHandler([=] { goToDiscussionGroup(); });
+ setupGiftToChannelButton();
_reportMessages->addClickHandler([=] { reportSelectedMessages(); });
_field->submits(
) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) {
@@ -1087,6 +1089,18 @@ void HistoryWidget::refreshJoinChannelText() {
}
}
+void HistoryWidget::refreshGiftToChannelShown() {
+ if (!_giftToChannelIn || !_giftToChannelOut) {
+ return;
+ }
+ const auto channel = _peer->asChannel();
+ const auto shown = channel
+ && channel->isBroadcast()
+ && channel->stargiftsAvailable();
+ _giftToChannelIn->setVisible(shown);
+ _giftToChannelOut->setVisible(shown);
+}
+
void HistoryWidget::refreshTopBarActiveChat() {
const auto state = computeDialogsEntryState();
_topBar->setActiveChat(state, _history->sendActionPainter());
@@ -2090,6 +2104,7 @@ void HistoryWidget::setupShortcuts() {
return true;
});
request->check(Command::ShowChatMenu, 1) && request->handle([=] {
+ Window::ActivateWindow(controller());
_topBar->showPeerMenu();
return true;
});
@@ -2112,6 +2127,25 @@ void HistoryWidget::setupShortcuts() {
}, lifetime());
}
+void HistoryWidget::setupGiftToChannelButton() {
+ const auto setupButton = [=](not_null parent) {
+ auto *button = Ui::CreateChild(
+ parent.get(),
+ st::historyGiftToChannel);
+ parent->widthValue() | rpl::start_with_next([=](int width) {
+ button->moveToRight(0, 0);
+ }, button->lifetime());
+ button->setClickedCallback([=] {
+ if (_peer) {
+ Ui::ShowStarGiftBox(controller(), _peer);
+ }
+ });
+ return button;
+ };
+ _giftToChannelIn = setupButton(_muteUnmute);
+ _giftToChannelOut = setupButton(_joinChannel);
+}
+
void HistoryWidget::pushReplyReturn(not_null item) {
if (item->history() != _history && item->history() != _migrated) {
return;
@@ -2270,15 +2304,12 @@ bool HistoryWidget::insideJumpToEndInsteadOfToUnread() const {
}
void HistoryWidget::showHistory(
- const PeerId &peerId,
+ PeerId peerId,
MsgId showAtMsgId,
- const TextWithEntities &highlightPart,
- int highlightPartOffsetHint) {
-
+ const Window::SectionShow ¶ms) {
_pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt;
- _showAtMsgHighlightPart = {};
- _showAtMsgHighlightPartOffsetHint = 0;
+ _showAtMsgParams = {};
const auto wasState = controller()->dialogsEntryStateCurrent();
const auto startBot = (showAtMsgId == ShowAndStartBotMsgId);
@@ -2328,16 +2359,10 @@ void HistoryWidget::showHistory(
).arg(_history->inboxReadTillId().bare
).arg(Logs::b(_history->loadedAtBottom())
).arg(showAtMsgId.bare));
- delayedShowAt(
- showAtMsgId,
- highlightPart,
- highlightPartOffsetHint);
+ delayedShowAt(showAtMsgId, params);
} else if (_showAtMsgId != showAtMsgId) {
clearAllLoadRequests();
- setMsgId(
- showAtMsgId,
- highlightPart,
- highlightPartOffsetHint);
+ setMsgId(showAtMsgId, params);
firstLoadMessages();
doneShow();
}
@@ -2357,10 +2382,7 @@ void HistoryWidget::showHistory(
_cornerButtons.skipReplyReturn(skipId);
}
- setMsgId(
- showAtMsgId,
- highlightPart,
- highlightPartOffsetHint);
+ setMsgId(showAtMsgId, params);
if (_historyInited) {
DEBUG_LOG(("JumpToEnd(%1, %2, %3): "
"Showing instant at %4."
@@ -2465,8 +2487,7 @@ void HistoryWidget::showHistory(
clearInlineBot();
_showAtMsgId = showAtMsgId;
- _showAtMsgHighlightPart = highlightPart;
- _showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint;
+ _showAtMsgParams = params;
_historyInited = false;
_contactStatus = nullptr;
_businessBotStatus = nullptr;
@@ -2485,6 +2506,8 @@ void HistoryWidget::showHistory(
) | rpl::start_with_next([=] {
updateControlsGeometry();
}, _contactStatus->bar().lifetime());
+
+ refreshGiftToChannelShown();
if (const auto user = _peer->asUser()) {
_businessBotStatus = std::make_unique(
controller(),
@@ -2977,14 +3000,12 @@ void HistoryWidget::refreshSilentToggle() {
}
void HistoryWidget::setupFastButtonMode() {
- if (!session().supportMode()) {
- return;
- }
const auto field = _field->rawTextEdit();
base::install_event_filter(field, [=](not_null e) {
if (e->type() != QEvent::KeyPress
|| !_history
- || !session().supportHelper().fastButtonMode(_history->peer)
+ || !FastButtonsMode()
+ || !session().fastButtonsBots().enabled(_history->peer)
|| !_field->getLastText().isEmpty()) {
return base::EventFilterResult::Continue;
}
@@ -3737,10 +3758,7 @@ void HistoryWidget::messagesReceived(
}
_delayedShowAtRequest = 0;
- setMsgId(
- _delayedShowAtMsgId,
- _delayedShowAtMsgHighlightPart,
- _delayedShowAtMsgHighlightPartOffsetHint);
+ setMsgId(_delayedShowAtMsgId, _delayedShowAtMsgParams);
historyLoaded();
}
if (session().supportMode()) {
@@ -3992,15 +4010,11 @@ void HistoryWidget::loadMessagesDown() {
void HistoryWidget::delayedShowAt(
MsgId showAtMsgId,
- const TextWithEntities &highlightPart,
- int highlightPartOffsetHint) {
+ const Window::SectionShow ¶ms) {
if (!_history) {
return;
}
- if (_delayedShowAtMsgHighlightPart != highlightPart) {
- _delayedShowAtMsgHighlightPart = highlightPart;
- }
- _delayedShowAtMsgHighlightPartOffsetHint = highlightPartOffsetHint;
+ _delayedShowAtMsgParams = params;
if (_delayedShowAtRequest && _delayedShowAtMsgId == showAtMsgId) {
return;
}
@@ -4135,7 +4149,10 @@ void HistoryWidget::preloadHistoryIfNeeded() {
preloadHistoryByScroll();
checkReplyReturns();
}
- if (clearMaybeSendStart() && !_history->isDisplayedEmpty()) {
+ const auto hasNonEmpty = _history->findFirstNonEmpty();
+ const auto readyForBotStart = hasNonEmpty
+ || (_history->loadedAtTop() && _history->loadedAtBottom());
+ if (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) {
sendBotStartCommand();
}
}
@@ -4663,12 +4680,8 @@ PeerData *HistoryWidget::peer() const {
// Sometimes _showAtMsgId is set directly.
void HistoryWidget::setMsgId(
MsgId showAtMsgId,
- const TextWithEntities &highlightPart,
- int highlightPartOffsetHint) {
- if (_showAtMsgHighlightPart != highlightPart) {
- _showAtMsgHighlightPart = highlightPart;
- }
- _showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint;
+ const Window::SectionShow ¶ms) {
+ _showAtMsgParams = params;
if (_showAtMsgId != showAtMsgId) {
_showAtMsgId = showAtMsgId;
if (_history) {
@@ -6429,8 +6442,8 @@ int HistoryWidget::countInitialScrollTop() {
enqueueMessageHighlight({
item,
- base::take(_showAtMsgHighlightPart),
- base::take(_showAtMsgHighlightPartOffsetHint),
+ base::take(_showAtMsgParams.highlightPart),
+ base::take(_showAtMsgParams.highlightPartOffsetHint),
});
const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result);
@@ -6647,6 +6660,22 @@ void HistoryWidget::updateHistoryGeometry(
}
const auto toY = std::clamp(newScrollTop, 0, _scroll->scrollTopMax());
synteticScrollToY(toY);
+ if (initial && _showAtMsgId) {
+ const auto timestamp = base::take(_showAtMsgParams.videoTimestamp);
+ if (timestamp.has_value()) {
+ const auto item = session().data().message(_peer, _showAtMsgId);
+ const auto media = item ? item->media() : nullptr;
+ const auto document = media ? media->document() : nullptr;
+ if (document && document->isVideoFile()) {
+ controller()->openDocument(
+ document,
+ true,
+ { item->fullId() },
+ nullptr,
+ timestamp);
+ }
+ }
+ }
}
void HistoryWidget::revealItemsCallback() {
@@ -6748,7 +6777,7 @@ void HistoryWidget::startMessageSendingAnimation(
void HistoryWidget::updateListSize() {
Expects(_list != nullptr);
- _list->recountHistoryGeometry();
+ _list->recountHistoryGeometry(!_historyInited);
auto washidden = _scroll->isHidden();
if (washidden) {
_scroll->show();
@@ -8517,9 +8546,13 @@ void HistoryWidget::fullInfoUpdated() {
handlePeerUpdate();
checkSuggestToGigagroup();
- if (clearMaybeSendStart() && !_history->isDisplayedEmpty()) {
+ const auto hasNonEmpty = _history->findFirstNonEmpty();
+ const auto readyForBotStart = hasNonEmpty
+ || (_history->loadedAtTop() && _history->loadedAtBottom());
+ if (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) {
sendBotStartCommand();
}
+ refreshGiftToChannelShown();
}
if (updateCmdStartShown()) {
refresh = true;
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index 478d35853..7c062f7da 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/field_characters_count_manager.h"
#include "data/data_report.h"
#include "window/section_widget.h"
+#include "window/window_session_controller.h"
#include "ui/widgets/fields/input_field.h"
#include "mtproto/sender.h"
@@ -86,10 +87,6 @@ namespace Webrtc {
enum class RecordAvailability : uchar;
} // namespace Webrtc
-namespace Window {
-class SessionController;
-} // namespace Window
-
namespace ChatHelpers {
class TabbedPanel;
class TabbedSelector;
@@ -160,10 +157,7 @@ public:
void loadMessages();
void loadMessagesDown();
void firstLoadMessages();
- void delayedShowAt(
- MsgId showAtMsgId,
- const TextWithEntities &highlightPart,
- int highlightPartOffsetHint);
+ void delayedShowAt(MsgId showAtMsgId, const Window::SectionShow ¶ms);
bool updateReplaceMediaButton();
void updateFieldPlaceholder();
@@ -176,10 +170,7 @@ public:
History *history() const;
PeerData *peer() const;
- void setMsgId(
- MsgId showAtMsgId,
- const TextWithEntities &highlightPart = {},
- int highlightPartOffsetHint = 0);
+ void setMsgId(MsgId showAtMsgId, const Window::SectionShow ¶ms = {});
MsgId msgId() const;
bool hasTopBarShadow() const {
@@ -244,10 +235,9 @@ public:
bool applyDraft(
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void showHistory(
- const PeerId &peer,
+ PeerId peerId,
MsgId showAtMsgId,
- const TextWithEntities &highlightPart = {},
- int highlightPartOffsetHint = 0);
+ const Window::SectionShow ¶ms = {});
void setChooseReportMessagesDetails(
Data::ReportInput reportInput,
Fn)> callback);
@@ -406,6 +396,7 @@ private:
void refreshTopBarActiveChat();
void refreshJoinChannelText();
+ void refreshGiftToChannelShown();
void requestMessageData(MsgId msgId);
void messageDataReceived(not_null peer, MsgId msgId);
@@ -527,6 +518,7 @@ private:
}
void setupShortcuts();
+ void setupGiftToChannelButton();
void handlePeerMigration();
@@ -731,8 +723,7 @@ private:
bool _canSendTexts = false;
MsgId _showAtMsgId = ShowAtUnreadMsgId;
base::flat_set _topicsRequested;
- TextWithEntities _showAtMsgHighlightPart;
- int _showAtMsgHighlightPartOffsetHint = 0;
+ Window::SectionShow _showAtMsgParams;
bool _showAndMaybeSendStart = false;
int _firstLoadRequest = 0; // Not real mtpRequestId.
@@ -740,8 +731,7 @@ private:
int _preloadDownRequest = 0; // Not real mtpRequestId.
MsgId _delayedShowAtMsgId = -1;
- TextWithEntities _delayedShowAtMsgHighlightPart;
- int _delayedShowAtMsgHighlightPartOffsetHint = 0;
+ Window::SectionShow _delayedShowAtMsgParams;
int _delayedShowAtRequest = 0; // Not real mtpRequestId.
History *_supportPreloadHistory = nullptr;
@@ -789,6 +779,8 @@ private:
object_ptr _botStart;
object_ptr _joinChannel;
object_ptr _muteUnmute;
+ QPointer _giftToChannelIn;
+ QPointer _giftToChannelOut;
object_ptr _discuss;
object_ptr _reportMessages;
struct {
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
index 212840323..2d03f6dfa 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
@@ -947,7 +947,8 @@ void DraftOptionsBox(
AddFilledSkip(bottom);
- if (!hasOnlyForcedForwardedInfo) {
+ if (!hasOnlyForcedForwardedInfo
+ && !HasOnlyDroppedForwardedInfo(items)) {
Settings::AddButtonWithIcon(
bottom,
(dropNames
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp
index 239108e13..022b124ab 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp
@@ -153,7 +153,7 @@ void ForwardPanel::updateTexts() {
Unexpected("Corrupt forwarded information in message.");
}
}
- if (!keepNames) {
+ if (!keepNames || HasOnlyDroppedForwardedInfo(_data.items)) {
from = tr::lng_forward_sender_names_removed(tr::now);
} else if (names.size() > 2) {
from = tr::lng_forwarding_from(
@@ -445,4 +445,13 @@ bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
return true;
}
+bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list) {
+ for (const auto &item : list) {
+ if (!item->computeDropForwardedInfo()) {
+ return false;
+ }
+ }
+ return true;
+}
+
} // namespace HistoryView::Controls
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h
index 133292ceb..54624d47d 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h
@@ -85,5 +85,6 @@ void EditWebPageOptions(
Fn done);
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list);
+[[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list);
} // namespace HistoryView::Controls
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index bbce1f07d..bd446022a 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -1315,25 +1315,28 @@ base::unique_qptr FillContextMenu(
void CopyPostLink(
not_null controller,
FullMsgId itemId,
- Context context) {
- CopyPostLink(controller->uiShow(), itemId, context);
+ Context context,
+ std::optional videoTimestamp) {
+ CopyPostLink(controller->uiShow(), itemId, context, videoTimestamp);
}
void CopyPostLink(
std::shared_ptr show,
FullMsgId itemId,
- Context context) {
+ Context context,
+ std::optional videoTimestamp) {
const auto item = show->session().data().message(itemId);
if (!item || !item->hasDirectLink()) {
return;
}
const auto inRepliesContext = (context == Context::Replies);
- const auto forceNonPublicLink = base::IsCtrlPressed();
+ const auto forceNonPublicLink = !videoTimestamp && base::IsCtrlPressed();
QGuiApplication::clipboard()->setText(
item->history()->session().api().exportDirectMessageLink(
item,
inRepliesContext,
- forceNonPublicLink));
+ forceNonPublicLink,
+ videoTimestamp));
const auto isPublicLink = [&] {
if (forceNonPublicLink) {
@@ -1354,7 +1357,7 @@ void CopyPostLink(
}
return channel->hasUsername();
}();
- if (isPublicLink) {
+ if (isPublicLink && !videoTimestamp) {
show->showToast({
.text = tr::lng_channel_public_link_copied(
tr::now, Ui::Text::Bold
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h
index 1dac246bb..0e14fa803 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.h
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h
@@ -60,11 +60,13 @@ base::unique_qptr FillContextMenu(
void CopyPostLink(
not_null controller,
FullMsgId itemId,
- Context context);
+ Context context,
+ std::optional videoTimestamp = {});
void CopyPostLink(
std::shared_ptr show,
FullMsgId itemId,
- Context context);
+ Context context,
+ std::optional videoTimestamp = {});
void CopyStoryLink(
std::shared_ptr show,
FullStoryId storyId);
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 91d085ce2..ebd36aef6 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "main/main_session.h"
+#include "settings/settings_premium.h"
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "window/themes/window_theme.h" // IsNightMode.
@@ -378,6 +379,7 @@ struct Message::CommentsButton {
struct Message::FromNameStatus {
EmojiStatusId id;
std::unique_ptr custom;
+ ClickHandlerPtr link;
int skip = 0;
};
@@ -2815,6 +2817,25 @@ bool Message::getStateFromName(
Unexpected("Corrupt forwarded information in message.");
}
}();
+
+ const auto statusWidth = (from && _fromNameStatus)
+ ? st::dialogsPremiumIcon.icon.width()
+ : 0;
+ if (statusWidth && availableWidth > statusWidth) {
+ const auto x = availableLeft + std::min(
+ availableWidth - statusWidth,
+ nameText->maxWidth()
+ ) - (_fromNameStatus->custom ? (2 * _fromNameStatus->skip) : 0);
+ const auto checkWidth = _fromNameStatus->custom
+ ? (st::emojiSize - 2 * _fromNameStatus->skip)
+ : statusWidth;
+ if (point.x() >= x && point.x() < x + checkWidth) {
+ ensureFromNameStatusLink(from);
+ outResult->link = _fromNameStatus->link;
+ return true;
+ }
+ availableWidth -= statusWidth;
+ }
if (point.x() >= availableLeft
&& point.x() < availableLeft + availableWidth
&& point.x() < availableLeft + nameText->maxWidth()) {
@@ -2835,6 +2856,21 @@ bool Message::getStateFromName(
return false;
}
+void Message::ensureFromNameStatusLink(not_null peer) const {
+ Expects(_fromNameStatus != nullptr);
+
+ if (_fromNameStatus->link) {
+ return;
+ }
+ _fromNameStatus->link = std::make_shared([=](
+ ClickContext context) {
+ const auto controller = ExtractController(context);
+ if (controller) {
+ Settings::ShowEmojiStatusPremium(controller, peer);
+ }
+ });
+}
+
bool Message::getStateTopicButton(
QPoint point,
QRect &trect,
diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h
index 0b5341795..e3541e10f 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.h
+++ b/Telegram/SourceFiles/history/view/history_view_message.h
@@ -300,6 +300,7 @@ private:
void refreshRightBadge();
void validateFromNameText(PeerData *from) const;
+ void ensureFromNameStatusLink(not_null peer) const;
mutable std::unique_ptr _rightAction;
mutable ClickHandlerPtr _fastReplyLink;
diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp
index d79ec1005..2fd79450b 100644
--- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp
@@ -163,7 +163,7 @@ PaidReactionToast::~PaidReactionToast() {
bool PaidReactionToast::maybeShowFor(not_null item) {
const auto count = item->reactionsPaidScheduled();
- const auto anonymous = item->reactionsLocalAnonymous();
+ const auto shownPeer = item->reactionsLocalShownPeer();
const auto at = _owner->reactions().sendingScheduledPaidAt(item);
if (!count || !at) {
return false;
@@ -173,14 +173,14 @@ bool PaidReactionToast::maybeShowFor(not_null item) {
if (at <= crl::now() + ignore) {
return false;
}
- showFor(item->fullId(), count, anonymous, at - ignore, total);
+ showFor(item->fullId(), count, shownPeer, at - ignore, total);
return true;
}
void PaidReactionToast::showFor(
FullMsgId itemId,
int count,
- bool anonymous,
+ PeerId shownPeer,
crl::time finish,
crl::time total) {
const auto old = _weak.get();
@@ -188,7 +188,7 @@ void PaidReactionToast::showFor(
if (i != end(_stack)) {
if (old && i + 1 == end(_stack)) {
_count = count;
- _anonymous = anonymous;
+ _shownPeer = shownPeer;
_timeFinish = finish;
return;
}
@@ -202,14 +202,14 @@ void PaidReactionToast::showFor(
_hiding.push_back(base::take(_weak));
}
_count.reset();
- _anonymous.reset();
+ _shownPeer.reset();
_timeFinish.reset();
_count = count;
- _anonymous = anonymous;
+ _shownPeer = shownPeer;
_timeFinish = finish;
auto text = rpl::combine(
rpl::conditional(
- _anonymous.value(),
+ _shownPeer.value() | rpl::map(rpl::mappers::_1 == PeerId()),
tr::lng_paid_react_toast_anonymous(
lt_count,
_count.value() | tr::to_count(),
diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h
index 080a62c32..485778e20 100644
--- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h
+++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h
@@ -40,7 +40,7 @@ private:
void showFor(
FullMsgId itemId,
int count,
- bool anonymous,
+ PeerId shownPeer,
crl::time left,
crl::time total);
@@ -54,7 +54,7 @@ private:
base::weak_ptr _weak;
std::vector> _hiding;
rpl::variable _count;
- rpl::variable _anonymous;
+ rpl::variable _shownPeer;
rpl::variable _timeFinish;
std::vector _stack;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
index e9899a55d..f537d78ca 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
@@ -510,9 +510,19 @@ QSize Document::countOptimalSize() {
accumulate_max(maxWidth, tleft + named->name.maxWidth() + tright);
accumulate_min(maxWidth, st::msgMaxWidth);
}
- if (voice && voice->transcribe) {
- maxWidth += st::historyTranscribeSkip
- + voice->transcribe->size().width();
+ if (voice) {
+ const auto maxWaveformWidth = ::Media::Player::kWaveformSamplesCount *
+ (st::msgWaveformBar + st::msgWaveformSkip);
+ const auto transcribeWidth = voice->transcribe
+ ? (voice->transcribe->size().width() + st::historyTranscribeSkip)
+ : 0;
+ accumulate_max(
+ maxWidth,
+ maxWaveformWidth
+ + rect::m::sum::h(st.padding)
+ + st.thumbSize
+ + st.thumbSkip
+ + transcribeWidth);
}
auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom();
diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp
index 71d3a75c2..a2dd5d2e9 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp
@@ -69,7 +69,10 @@ QSize Game::countOptimalSize() {
// init attach
if (!_attach) {
- _attach = CreateAttach(_parent, _data->document, _data->photo);
+ _attach = CreateAttach(
+ _parent,
+ _data->document,
+ _data->document ? nullptr : _data->photo);
}
// init strings
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
index 7b449b1e3..4dc43f99b 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_utility.h"
+#include "media/view/media_view_open_common.h"
#include "media/view/media_view_playback_progress.h"
#include "ui/boxes/confirm_box.h"
#include "ui/painter.h"
@@ -47,6 +48,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/spoiler_mess.h"
+#include "data/data_photo.h"
+#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_streaming.h"
@@ -54,6 +57,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_document_media.h"
+#include "data/data_web_page.h"
+#include "storage/storage_account.h"
#include "styles/style_chat.h"
#include
@@ -145,6 +150,7 @@ Gif::Gif(
bool spoiler)
: File(parent, realParent)
, _data(document)
+, _videoCover(LookupVideoCover(document, realParent))
, _storyId(realParent->media()
? realParent->media()->storyId()
: FullStoryId())
@@ -154,7 +160,9 @@ Gif::Gif(
? std::make_unique()
: nullptr)
, _downloadSize(Ui::FormatSizeText(_data->size))
-, _sensitiveSpoiler(realParent->isMediaSensitive()) {
+, _videoTimestamp(::Media::View::ExtractVideoTimestamp(realParent))
+, _sensitiveSpoiler(realParent->isMediaSensitive())
+, _hasVideoCover(realParent->media() && realParent->media()->videoCover()) {
if (_data->isVideoMessage() && _parent->data()->media()->ttlSeconds()) {
if (_spoiler) {
_drawTtl = CreateTtlPaintCallback([=] { repaint(); });
@@ -200,6 +208,12 @@ Gif::Gif(
if ((_dataMedia = _data->activeMediaView())) {
dataMediaCreated();
+ } else if (_videoCover) {
+ if (_videoCover->inlineThumbnailBytes().isEmpty()
+ && (_videoCover->hasExact(Data::PhotoSize::Small)
+ || _videoCover->hasExact(Data::PhotoSize::Thumbnail))) {
+ _videoCover->load(Data::PhotoSize::Small, realParent->fullId());
+ }
} else {
_data->loadThumbnail(realParent->fullId());
if (!autoplayEnabled()) {
@@ -396,6 +410,14 @@ bool Gif::downloadInCorner() const {
&& !_data->inappPlaybackFailed();
}
+bool Gif::autoplayUnderCursor() const {
+ return (_videoTimestamp || _hasVideoCover);
+}
+
+bool Gif::underCursor() const {
+ return ClickHandler::getActive() == currentVideoLink();
+}
+
bool Gif::autoplayEnabled() const {
if (_realParent->isSponsored()) {
return true;
@@ -413,6 +435,8 @@ bool Gif::hideMessageText() const {
void Gif::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
+ _smallGroupPart = false;
+
ensureDataMediaCreated();
const auto item = _parent->data();
const auto loaded = dataLoaded();
@@ -469,11 +493,14 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
validateSpoilerImageCache(rthumb.size(), rounding);
}
- const auto startPlay = autoplay
+ const auto canStartPlay = autoplay
&& !_streamed
&& !activeRoundPlaying
&& !fullHiddenBySpoiler;
- if (startPlay) {
+ const auto shouldBePlaying = !autoplayUnderCursor() || underCursor();
+ if (!shouldBePlaying && _videoTimestamp != 0) {
+ const_cast(this)->stopAnimation();
+ } else if (canStartPlay) {
const_cast(this)->playAnimation(true);
} else {
checkStreamedIsStarted();
@@ -514,8 +541,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
const auto skipDrawingContent = context.skipDrawingParts
== PaintContext::SkipDrawingParts::Content;
- if (streamed && !skipDrawingContent && !fullHiddenBySpoiler) {
- auto paused = context.paused;
+ const auto drawStreamed = streamed && (shouldBePlaying || !_videoCover);
+ if (drawStreamed && !skipDrawingContent && !fullHiddenBySpoiler) {
+ auto paused = context.paused || !shouldBePlaying;
auto request = ::Media::Streaming::FrameRequest{
.outer = QSize(usew, painth) * style::DevicePixelRatio(),
.blurredBackground = true,
@@ -583,6 +611,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
validateThumbCache({ usew, painth }, isRound, rounding);
p.drawImage(rthumb, _thumbCache);
}
+ if (!isRound) {
+ paintTimestampMark(p, rthumb, rounding);
+ }
if (revealed < 1.) {
p.setOpacity(1. - revealed);
@@ -868,6 +899,70 @@ void Gif::paintTranscribe(
context);
}
+void Gif::paintTimestampMark(
+ Painter &p,
+ QRect rthumb,
+ std::optional rounding) const {
+ if (_videoTimestamp <= 0 && _videoPosition < crl::time(200)) {
+ return;
+ }
+ const auto convert = [](Ui::BubbleCornerRounding rounding) {
+ return (rounding == Ui::BubbleCornerRounding::Small)
+ ? Ui::BubbleRadiusSmall()
+ : (rounding == Ui::BubbleCornerRounding::Large)
+ ? Ui::BubbleRadiusLarge()
+ : 0;
+ };
+ const auto radiusl = rounding
+ ? convert(rounding->bottomLeft)
+ : st::roundRadiusSmall;
+ const auto radiusr = rounding
+ ? convert(rounding->bottomRight)
+ : st::roundRadiusSmall;
+ const auto line = st::historyVideoTimestampProgressLine;
+ const auto duration = _data->duration();
+ const auto position = (_videoPosition > 0)
+ ? _videoPosition
+ : (_videoTimestamp * crl::time(1000));
+ if (rthumb.height() <= line
+ || rthumb.width() <= radiusl + radiusr
+ || position > duration) {
+ return;
+ }
+ auto hq = PainterHighQualityEnabler(p);
+ const auto used = rthumb.width() - radiusl - radiusr;
+ const auto progress = position / float64(duration);
+ const auto edge = radiusl + int(base::SafeRound(used * progress));
+ const auto top = rthumb.y() + rthumb.height() - line;
+ p.save();
+ p.setPen(Qt::NoPen);
+ if (edge > 0) {
+ p.setBrush(st::windowBgActive);
+
+ p.setClipRect(rthumb.x(), top, edge, line);
+ p.drawRoundedRect(
+ rthumb.x(),
+ top - 2 * radiusl,
+ edge + radiusl,
+ line + 2 * radiusl,
+ radiusl,
+ radiusl);
+ }
+ if (const auto width = rthumb.width() - edge; width > 0) {
+ const auto left = rthumb.x() + edge;
+ p.setBrush(st::mediaviewPlaybackProgressFg);
+ p.setClipRect(left, top, width, line);
+ p.drawRoundedRect(
+ left - radiusr,
+ top - 2 * radiusr,
+ width + radiusr,
+ line + 2 * radiusr,
+ radiusr,
+ radiusr);
+ }
+ p.restore();
+}
+
void Gif::drawSpoilerTag(
Painter &p,
QRect rthumb,
@@ -891,6 +986,8 @@ QImage Gif::spoilerTagBackground() const {
}
void Gif::validateVideoThumbnail() const {
+ Expects(!_videoCover);
+
const auto content = _dataMedia->videoThumbnailContent();
if (_videoThumbnailFrame || content.isEmpty()) {
return;
@@ -906,13 +1003,25 @@ void Gif::validateThumbCache(
QSize outer,
bool isEllipse,
std::optional rounding) const {
- const auto good = _dataMedia->goodThumbnail();
- const auto normal = good ? good : _dataMedia->thumbnail();
+ const auto good = _videoCoverMedia
+ ? _videoCoverMedia->image(Data::PhotoSize::Large)
+ : _dataMedia->goodThumbnail();
+ const auto normal = good
+ ? good
+ : _videoCoverMedia
+ ? nullptr
+ : _dataMedia->thumbnail();
if (!normal) {
- _data->loadThumbnail(_realParent->fullId());
- validateVideoThumbnail();
+ if (_videoCoverMedia) {
+ _videoCover->load(Data::PhotoSize::Small, _realParent->fullId());
+ } else {
+ _data->loadThumbnail(_realParent->fullId());
+ validateVideoThumbnail();
+ }
}
- const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get();
+ const auto videothumb = (normal || _videoCoverMedia)
+ ? nullptr
+ : _videoThumbnailFrame.get();
const auto blurred = normal
? (!good
&& (normal->width() < kUseNonBlurredThreshold)
@@ -934,9 +1043,17 @@ void Gif::validateThumbCache(
}
QImage Gif::prepareThumbCache(QSize outer) const {
- const auto good = _dataMedia->goodThumbnail();
- const auto normal = good ? good : _dataMedia->thumbnail();
- const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get();
+ const auto good = _videoCoverMedia
+ ? _videoCoverMedia->image(Data::PhotoSize::Large)
+ : _dataMedia->goodThumbnail();
+ const auto normal = good
+ ? good
+ : _videoCoverMedia
+ ? nullptr
+ : _dataMedia->thumbnail();
+ const auto videothumb = (normal || _videoCoverMedia)
+ ? nullptr
+ : _videoThumbnailFrame.get();
auto blurred = (!good
&& normal
&& (normal->width() < kUseNonBlurredThreshold)
@@ -946,6 +1063,10 @@ QImage Gif::prepareThumbCache(QSize outer) const {
const auto blurFromLarge = good || (normal && !blurred);
const auto large = blurFromLarge ? normal : videothumb;
if (videothumb) {
+ } else if (_videoCoverMedia) {
+ if (const auto embedded = _videoCoverMedia->thumbnailInline()) {
+ blurred = embedded;
+ }
} else if (const auto embedded = _dataMedia->thumbnailInline()) {
blurred = embedded;
}
@@ -971,7 +1092,9 @@ void Gif::validateSpoilerImageCache(
&& _spoiler->backgroundRounding == rounding) {
return;
}
- const auto normal = _dataMedia->thumbnail();
+ const auto normal = _videoCoverMedia
+ ? _videoCoverMedia->image(Data::PhotoSize::Small)
+ : _dataMedia->thumbnail();
auto container = std::optional();
const auto downscale = [&](Image *image) {
if (!image || (image->width() <= 40 && image->height() <= 40)) {
@@ -983,7 +1106,9 @@ void Gif::validateSpoilerImageCache(
Qt::SmoothTransformation));
return &*container;
};
- const auto embedded = _dataMedia->thumbnailInline();
+ const auto embedded = _videoCoverMedia
+ ? _videoCoverMedia->thumbnailInline()
+ : _dataMedia->thumbnailInline();
const auto blurred = embedded ? embedded : downscale(normal);
_spoiler->background = Images::Round(
PrepareWithBlurredBackground(
@@ -1174,15 +1299,7 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
: (isRound && _parent->data()->media()->ttlSeconds())
? _openl // Overriden.
: _spoiler->link)
- : _data->uploading()
- ? _cancell
- : _realParent->isSending()
- ? nullptr
- : (dataLoaded() || _dataMedia->canBePlayed(_realParent))
- ? _openl
- : _data->loading()
- ? _cancell
- : _savel;
+ : currentVideoLink();
}
const auto checkBottomInfo = !inWebPage
&& (unwrapped || !bubble || isBubbleBottom());
@@ -1290,8 +1407,8 @@ void Gif::drawGrouped(
|| _data->displayLoading();
const auto st = context.st;
const auto sti = context.imageStyle();
- const auto fullFeatured = fullFeaturedGrouped(sides);
- const auto cornerDownload = fullFeatured && downloadInCorner();
+ _smallGroupPart = !fullFeaturedGrouped(sides);
+ const auto cornerDownload = !_smallGroupPart && downloadInCorner();
const auto canBePlayed = _dataMedia->canBePlayed(_realParent);
const auto revealed = _spoiler
@@ -1302,16 +1419,22 @@ void Gif::drawGrouped(
validateSpoilerImageCache(geometry.size(), rounding);
}
- const auto autoplay = fullFeatured
+ const auto autoplay = !_smallGroupPart
&& autoplayEnabled()
&& canBePlayed
&& CanPlayInline(_data);
- const auto startPlay = autoplay && !_streamed;
- if (startPlay) {
+ const auto canStartPlay = autoplay
+ && !_streamed
+ && !fullHiddenBySpoiler;
+ const auto shouldBePlaying = !autoplayUnderCursor() || underCursor();
+ if (!shouldBePlaying && _videoTimestamp != 0) {
+ const_cast(this)->stopAnimation();
+ } else if (canStartPlay) {
const_cast(this)->playAnimation(true);
} else {
checkStreamedIsStarted();
}
+
const auto streamingMode = _streamed || autoplay;
const auto activeOwnPlaying = activeOwnStreamed();
@@ -1364,7 +1487,9 @@ void Gif::drawGrouped(
activeOwnPlaying->frozenStatusText = QString();
}
p.drawImage(geometry, streamed->frame(request));
- if (!context.paused) {
+ const auto paused = context.paused
+ || (autoplayUnderCursor() && !underCursor());
+ if (!paused) {
streamed->markFrameShown();
}
}
@@ -1479,7 +1604,7 @@ void Gif::drawGrouped(
}
p.setOpacity(1.);
}
- if (fullFeatured) {
+ if (!_smallGroupPart) {
drawCornerStatus(p, context, geometry.topLeft());
}
}
@@ -1492,8 +1617,7 @@ TextState Gif::getStateGrouped(
if (!geometry.contains(point)) {
return {};
}
- const auto isFullFeaturedGrouped = fullFeaturedGrouped(sides);
- if (isFullFeaturedGrouped) {
+ if (!_smallGroupPart) {
const auto state = cornerStatusTextState(
point,
request,
@@ -1503,37 +1627,53 @@ TextState Gif::getStateGrouped(
}
}
ensureDataMediaCreated();
+
auto link = (_spoiler && !_spoiler->revealed)
? (_sensitiveSpoiler ? spoilerTagLink() : _spoiler->link)
- : _data->uploading()
+ : currentVideoLink();
+ return TextState(_parent, std::move(link));
+}
+
+ClickHandlerPtr Gif::currentVideoLink() const {
+ return _data->uploading()
? _cancell
: _realParent->isSending()
? nullptr
: dataLoaded()
? _openl
- : (_data->loading() && !isFullFeaturedGrouped)
+ : (_data->loading() && _smallGroupPart)
? _cancell
: _dataMedia->canBePlayed(_realParent)
? _openl
+ : _data->loading()
+ ? _cancell
: _savel;
- return TextState(_parent, std::move(link));
}
void Gif::ensureDataMediaCreated() const {
- if (_dataMedia) {
+ if (_dataMedia && (!_videoCover || _videoCoverMedia)) {
return;
}
_dataMedia = _data->createMediaView();
+ _videoCoverMedia = _videoCover
+ ? _videoCover->createMediaView()
+ : nullptr;
dataMediaCreated();
}
void Gif::dataMediaCreated() const {
Expects(_dataMedia != nullptr);
- _dataMedia->goodThumbnailWanted();
- _dataMedia->thumbnailWanted(_realParent->fullId());
- if (!autoplayEnabled()) {
- _dataMedia->videoThumbnailWanted(_realParent->fullId());
+ if (_videoCoverMedia) {
+ _videoCoverMedia->wanted(
+ Data::PhotoSize::Large,
+ _realParent->fullId());
+ } else {
+ _dataMedia->goodThumbnailWanted();
+ _dataMedia->thumbnailWanted(_realParent->fullId());
+ if (!autoplayEnabled()) {
+ _dataMedia->videoThumbnailWanted(_realParent->fullId());
+ }
}
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
@@ -1670,12 +1810,18 @@ void Gif::validateGroupedCache(
ensureDataMediaCreated();
- const auto good = _dataMedia->goodThumbnail();
- const auto thumb = _dataMedia->thumbnail();
+ const auto good = _videoCoverMedia
+ ? _videoCoverMedia->image(Data::PhotoSize::Large)
+ : _dataMedia->goodThumbnail();
+ const auto thumb = _videoCoverMedia
+ ? nullptr
+ : _dataMedia->thumbnail();
const auto image = good
? good
: thumb
? thumb
+ : _videoCoverMedia
+ ? _videoCoverMedia->thumbnailInline()
: _dataMedia->thumbnailInline();
const auto blur = !good
&& (!thumb
@@ -1746,7 +1892,8 @@ void Gif::updateStatusText() const {
}
const auto round = activeRoundStreamed();
const auto own = activeOwnStreamed();
- if (round || (own && own->frozenFrame.isNull() && _data->isVideoFile())) {
+ if (round || (own && _data->isVideoFile())) {
+ const auto frozen = own && !own->frozenFrame.isNull();
const auto streamed = round ? round : &own->instance;
const auto state = streamed->player().prepareLegacyState();
if (state.length) {
@@ -1756,9 +1903,17 @@ void Gif::updateStatusText() const {
} else if (!::Media::Player::IsStoppedOrStopping(state.state)) {
position = state.position;
}
- statusSize = -1 - int((state.length - position) / state.frequency + 1);
+ if (!frozen) {
+ statusSize = -1 - int((state.length - position) / state.frequency + 1);
+ }
+ _videoPosition = std::max(
+ crl::time(position * crl::time(1000) / state.frequency),
+ crl::time(1));
} else {
- statusSize = -1 - (_data->duration() / 1000);
+ if (!frozen) {
+ statusSize = -1 - (_data->duration() / 1000);
+ }
+ _videoPosition = 0;
}
}
if (statusSize != _statusSize) {
@@ -1918,6 +2073,10 @@ void Gif::startStreamedPlayer() const {
//if (!_streamed->withSound) {
options.mode = ::Media::Streaming::Mode::Video;
options.loop = true;
+ options.position = _videoTimestamp
+ ? (_videoTimestamp * crl::time(1000))
+ : _parent->history()->session().local().mediaLastPlaybackPosition(
+ _data->id);
//}
_streamed->instance.play(options);
}
@@ -1925,10 +2084,12 @@ void Gif::startStreamedPlayer() const {
void Gif::checkStreamedIsStarted() const {
if (!_streamed || _streamed->instance.playerLocked()) {
return;
- } else if (_streamed->instance.paused()) {
- _streamed->instance.resume();
}
- if (!_streamed->instance.active() && !_streamed->instance.failed()) {
+ if (_streamed->instance.active()) {
+ if (_streamed->instance.paused()) {
+ _streamed->instance.resume();
+ }
+ } else if (!_streamed->instance.failed()) {
startStreamedPlayer();
}
}
@@ -1941,6 +2102,7 @@ void Gif::setStreamed(std::unique_ptr value) {
history()->owner().registerHeavyViewPart(_parent);
togglePollingStory(true);
} else if (removed) {
+ _videoPosition = 0;
_parent->checkHeavyPart();
}
}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h
index 45edc9ce3..f663d00d2 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h
@@ -15,9 +15,11 @@ struct HistoryMessageVia;
struct HistoryMessageReply;
struct HistoryMessageForwarded;
class Painter;
+class PhotoData;
namespace Data {
class DocumentMedia;
+class PhotoMedia;
} // namespace Data
namespace Media {
@@ -37,6 +39,7 @@ enum class Error;
namespace HistoryView {
+class Photo;
class Reply;
class TranscribeButton;
@@ -137,6 +140,8 @@ private:
void dataMediaCreated() const;
[[nodiscard]] bool autoplayEnabled() const;
+ [[nodiscard]] bool autoplayUnderCursor() const;
+ [[nodiscard]] bool underCursor() const;
void playAnimation(bool autoplay) override;
QSize countOptimalSize() override;
@@ -163,6 +168,10 @@ private:
int y,
bool right,
const PaintContext &context) const;
+ void paintTimestampMark(
+ Painter &p,
+ QRect rthumb,
+ std::optional