Merge tag 'v5.0.1' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/info/profile/info_profile_actions.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
AlexeyZavar 2024-05-04 21:07:52 +03:00
commit eeb5e5a206
265 changed files with 10241 additions and 2033 deletions

View file

@ -1,32 +1,11 @@
name: No Response name: Can't reproduce.
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on: on:
issue_comment:
types: [created]
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 3 * * *'
jobs: jobs:
waiting-for-answer:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
responseRequiredLabel: waiting for answer
needs-user-action:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
responseRequiredLabel: needs user action
cant-reproduce: cant-reproduce:
if: github.event_name != 'issue_comment'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: lee-dohm/no-response@v0.5.0 - uses: lee-dohm/no-response@v0.5.0

View file

@ -49,7 +49,6 @@ jobs:
defines: defines:
- "" - ""
- "DESKTOP_APP_DISABLE_X11_INTEGRATION" - "DESKTOP_APP_DISABLE_X11_INTEGRATION"
- "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION"
env: env:
UPLOAD_ARTIFACT: "true" UPLOAD_ARTIFACT: "true"

View file

@ -49,7 +49,7 @@ jobs:
env: env:
GIT: "https://github.com" GIT: "https://github.com"
OPENALDIR: "/usr/local/opt/openal-soft" CMAKE_PREFIX_PATH: "/usr/local/opt/ffmpeg@6:/usr/local/opt/openal-soft"
UPLOAD_ARTIFACT: "true" UPLOAD_ARTIFACT: "true"
ONLY_CACHE: "false" ONLY_CACHE: "false"
MANUAL_CACHING: "1" MANUAL_CACHING: "1"
@ -69,7 +69,7 @@ jobs:
run: | run: |
brew update brew update
brew upgrade || true brew upgrade || true
brew install autoconf automake boost cmake ffmpeg openal-soft openssl opus ninja pkg-config python qt yasm xz brew install autoconf automake boost cmake ffmpeg@6 openal-soft openssl opus ninja pkg-config python qt yasm xz
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version > CACHE_KEY.txt xcodebuild -version > CACHE_KEY.txt

16
.github/workflows/needs-user-action.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Needs user action.
on:
issue_comment:
types: [created]
schedule:
- cron: '0 2 * * *'
jobs:
needs-user-action:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
responseRequiredLabel: needs user action

View file

@ -64,7 +64,7 @@ jobs:
uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8
- name: Telegram Desktop snap build. - name: Telegram Desktop snap build.
run: sg lxd -c 'snap run snapcraft -v' run: sg lxd -c 'snap run snapcraft --verbosity=debug'
- name: Move artifact. - name: Move artifact.
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'

View file

@ -0,0 +1,16 @@
name: Waiting for answer.
on:
issue_comment:
types: [created]
schedule:
- cron: '30 0 * * *'
jobs:
waiting-for-answer:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
responseRequiredLabel: waiting for answer

9
.gitmodules vendored
View file

@ -82,12 +82,6 @@
[submodule "Telegram/ThirdParty/dispatch"] [submodule "Telegram/ThirdParty/dispatch"]
path = Telegram/ThirdParty/dispatch path = Telegram/ThirdParty/dispatch
url = https://github.com/apple/swift-corelibs-libdispatch url = https://github.com/apple/swift-corelibs-libdispatch
[submodule "Telegram/ThirdParty/plasma-wayland-protocols"]
path = Telegram/ThirdParty/plasma-wayland-protocols
url = https://github.com/KDE/plasma-wayland-protocols.git
[submodule "Telegram/ThirdParty/wayland-protocols"]
path = Telegram/ThirdParty/wayland-protocols
url = https://github.com/gitlab-freedesktop-mirrors/wayland-protocols.git
[submodule "Telegram/ThirdParty/kimageformats"] [submodule "Telegram/ThirdParty/kimageformats"]
path = Telegram/ThirdParty/kimageformats path = Telegram/ThirdParty/kimageformats
url = https://github.com/KDE/kimageformats.git url = https://github.com/KDE/kimageformats.git
@ -97,9 +91,6 @@
[submodule "Telegram/ThirdParty/cld3"] [submodule "Telegram/ThirdParty/cld3"]
path = Telegram/ThirdParty/cld3 path = Telegram/ThirdParty/cld3
url = https://github.com/google/cld3.git url = https://github.com/google/cld3.git
[submodule "Telegram/ThirdParty/wayland"]
path = Telegram/ThirdParty/wayland
url = https://github.com/gitlab-freedesktop-mirrors/wayland.git
[submodule "Telegram/ThirdParty/libprisma"] [submodule "Telegram/ThirdParty/libprisma"]
path = Telegram/ThirdParty/libprisma path = Telegram/ThirdParty/libprisma
url = https://github.com/desktop-app/libprisma.git url = https://github.com/desktop-app/libprisma.git

View file

@ -62,7 +62,7 @@ if (NOT DESKTOP_APP_USE_PACKAGED)
if (WIN32) if (WIN32)
set(qt_version 5.15.13) set(qt_version 5.15.13)
elseif (APPLE) elseif (APPLE)
set(qt_version 6.2.7) set(qt_version 6.2.8)
endif() endif()
endif() endif()
include(cmake/external/qt/package.cmake) include(cmake/external/qt/package.cmake)

View file

@ -342,6 +342,8 @@ PRIVATE
boxes/local_storage_box.h boxes/local_storage_box.h
boxes/max_invite_box.cpp boxes/max_invite_box.cpp
boxes/max_invite_box.h boxes/max_invite_box.h
boxes/moderate_messages_box.cpp
boxes/moderate_messages_box.h
boxes/peer_list_box.cpp boxes/peer_list_box.cpp
boxes/peer_list_box.h boxes/peer_list_box.h
boxes/peer_list_controllers.cpp boxes/peer_list_controllers.cpp
@ -529,10 +531,14 @@ PRIVATE
data/business/data_business_info.h data/business/data_business_info.h
data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.cpp
data/business/data_shortcut_messages.h data/business/data_shortcut_messages.h
data/components/recent_peers.cpp
data/components/recent_peers.h
data/components/scheduled_messages.cpp data/components/scheduled_messages.cpp
data/components/scheduled_messages.h data/components/scheduled_messages.h
data/components/sponsored_messages.cpp data/components/sponsored_messages.cpp
data/components/sponsored_messages.h data/components/sponsored_messages.h
data/components/top_peers.cpp
data/components/top_peers.h
data/notify/data_notify_settings.cpp data/notify/data_notify_settings.cpp
data/notify/data_notify_settings.h data/notify/data_notify_settings.h
data/notify/data_peer_notify_settings.cpp data/notify/data_peer_notify_settings.cpp
@ -677,6 +683,18 @@ PRIVATE
data/data_wall_paper.h data/data_wall_paper.h
data/data_web_page.cpp data/data_web_page.cpp
data/data_web_page.h data/data_web_page.h
dialogs/ui/dialogs_layout.cpp
dialogs/ui/dialogs_layout.h
dialogs/ui/dialogs_message_view.cpp
dialogs/ui/dialogs_message_view.h
dialogs/ui/dialogs_stories_content.cpp
dialogs/ui/dialogs_stories_content.h
dialogs/ui/dialogs_suggestions.cpp
dialogs/ui/dialogs_suggestions.h
dialogs/ui/dialogs_topics_view.cpp
dialogs/ui/dialogs_topics_view.h
dialogs/ui/dialogs_video_userpic.cpp
dialogs/ui/dialogs_video_userpic.h
dialogs/dialogs_entry.cpp dialogs/dialogs_entry.cpp
dialogs/dialogs_entry.h dialogs/dialogs_entry.h
dialogs/dialogs_indexed_list.cpp dialogs/dialogs_indexed_list.cpp
@ -699,16 +717,6 @@ PRIVATE
dialogs/dialogs_search_tags.h dialogs/dialogs_search_tags.h
dialogs/dialogs_widget.cpp dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h dialogs/dialogs_widget.h
dialogs/ui/dialogs_layout.cpp
dialogs/ui/dialogs_layout.h
dialogs/ui/dialogs_message_view.cpp
dialogs/ui/dialogs_message_view.h
dialogs/ui/dialogs_stories_content.cpp
dialogs/ui/dialogs_stories_content.h
dialogs/ui/dialogs_topics_view.cpp
dialogs/ui/dialogs_topics_view.h
dialogs/ui/dialogs_video_userpic.cpp
dialogs/ui/dialogs_video_userpic.h
editor/color_picker.cpp editor/color_picker.cpp
editor/color_picker.h editor/color_picker.h
editor/controllers/controllers.h editor/controllers/controllers.h
@ -1268,11 +1276,6 @@ PRIVATE
payments/payments_checkout_process.h payments/payments_checkout_process.h
payments/payments_form.cpp payments/payments_form.cpp
payments/payments_form.h payments/payments_form.h
platform/linux/linux_wayland_integration_dummy.cpp
platform/linux/linux_wayland_integration.cpp
platform/linux/linux_wayland_integration.h
platform/linux/linux_xdp_open_with_dialog.cpp
platform/linux/linux_xdp_open_with_dialog.h
platform/linux/file_utilities_linux.cpp platform/linux/file_utilities_linux.cpp
platform/linux/file_utilities_linux.h platform/linux/file_utilities_linux.h
platform/linux/launcher_linux.cpp platform/linux/launcher_linux.cpp
@ -1625,16 +1628,6 @@ if (NOT build_winstore)
) )
endif() endif()
if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
remove_target_sources(Telegram ${src_loc}
platform/linux/linux_wayland_integration.cpp
)
else()
remove_target_sources(Telegram ${src_loc}
platform/linux/linux_wayland_integration_dummy.cpp
)
endif()
if (DESKTOP_APP_USE_PACKAGED) if (DESKTOP_APP_USE_PACKAGED)
remove_target_sources(Telegram ${src_loc} remove_target_sources(Telegram ${src_loc}
platform/mac/mac_iconv_helper.c platform/mac/mac_iconv_helper.c
@ -1774,19 +1767,6 @@ else()
desktop-app::external_xcb desktop-app::external_xcb
) )
endif() endif()
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
qt_generate_wayland_protocol_client_sources(Telegram
FILES
${third_party_loc}/wayland/protocol/wayland.xml
${third_party_loc}/plasma-wayland-protocols/src/protocols/plasma-shell.xml
)
target_link_libraries(Telegram
PRIVATE
desktop-app::external_wayland_client
)
endif()
endif() endif()
if (build_macstore) if (build_macstore)

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -134,9 +134,32 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
.page-slide { .page-slide {
position: relative; position: relative;
width: 100%; width: 100%;
min-height: 100%;
margin-left: 0%; margin-left: 0%;
transition: margin 240ms ease-in-out; transition: margin 240ms ease-in-out;
} }
.page-footer {
height: 32px;
margin-top: -32px;
background: var(--td-window-bg-over);
}
.page-footer .content {
padding: 3px 18px;
font-size: 15px;
color: var(--td-window-sub-text-fg);
text-align: center;
}
.page-footer .wrong {
position: relative;
padding: 5px;
margin: -5px;
color: var(--td-window-sub-text-fg);
text-decoration: none;
cursor: pointer;
}
.page-footer .wrong:hover {
text-decoration: underline;
}
.hidden-left, .hidden-left,
.hidden-right { .hidden-right {
pointer-events: none; pointer-events: none;
@ -148,7 +171,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
margin-left: 100%; margin-left: 100%;
} }
article { article {
padding-bottom: 12px; padding-bottom: 40px;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
white-space: pre-wrap; white-space: pre-wrap;
@ -893,6 +916,9 @@ section.related a.related-link:after {
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
section.related a.related-link:last-child:after {
border-bottom: 0px;
}
section.related .related-link-url { section.related .related-link-url {
display: block; display: block;
font-size: 15px; font-size: 15px;
@ -1027,6 +1053,9 @@ section.channel > a > h4 {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
} }
.media-outer {
margin-bottom: 16px;
}
.photo-wrap, .photo-wrap,
.video-wrap { .video-wrap {
width: 100%; width: 100%;

View file

@ -26,7 +26,7 @@ var IV = {
} }
target = target.parentNode; target = target.parentNode;
} }
if (!target || !target.hasAttribute('href')) { if (!target || (context === '' && !target.hasAttribute('href'))) {
return; return;
} }
var base = document.createElement('A'); var base = document.createElement('A');
@ -413,9 +413,12 @@ var IV = {
var article = function (el) { var article = function (el) {
return el.getElementsByTagName('article')[0]; return el.getElementsByTagName('article')[0];
}; };
var from = article(IV.findPageScroll()); var footer = function (el) {
var to = article(IV.makeScrolledContent(data.html)); return el.getElementsByClassName('page-footer')[0];
morphdom(from, to, { };
var from = IV.findPageScroll();
var to = IV.makeScrolledContent(data.html);
morphdom(article(from), article(to), {
onBeforeElUpdated: function (fromEl, toEl) { onBeforeElUpdated: function (fromEl, toEl) {
if (fromEl.classList.contains('video') if (fromEl.classList.contains('video')
&& toEl.classList.contains('video') && toEl.classList.contains('video')
@ -439,6 +442,7 @@ var IV = {
return !fromEl.isEqualNode(toEl); return !fromEl.isEqualNode(toEl);
} }
}); });
morphdom(footer(from), footer(to));
IV.initMedia(); IV.initMedia();
eval(data.js); eval(data.js);
}, },
@ -477,9 +481,7 @@ var IV = {
var result = document.createElement('div'); var result = document.createElement('div');
result.className = 'page-scroll'; result.className = 'page-scroll';
result.tabIndex = '-1'; result.tabIndex = '-1';
result.innerHTML = '<div class="page-slide"><article>' result.innerHTML = html.trim();
+ html
+ '</article></div>';
result.onscroll = IV.frameScrolled; result.onscroll = IV.frameScrolled;
return result; return result;
}, },

View file

@ -730,6 +730,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_angle_backend_d3d11on12" = "D3D11on12"; "lng_settings_angle_backend_d3d11on12" = "D3D11on12";
"lng_settings_angle_backend_opengl" = "OpenGL"; "lng_settings_angle_backend_opengl" = "OpenGL";
"lng_settings_angle_backend_disabled" = "Disabled"; "lng_settings_angle_backend_disabled" = "Disabled";
"lng_settings_top_peers_title" = "Frequent contacts";
"lng_settings_top_peers_suggest" = "Suggest frequent contacts";
"lng_settings_top_peers_about" = "Display people you message frequently at the top of the search section for quick access.";
"lng_settings_sensitive_title" = "Sensitive content"; "lng_settings_sensitive_title" = "Sensitive content";
"lng_settings_sensitive_disable_filtering" = "Disable filtering"; "lng_settings_sensitive_disable_filtering" = "Disable filtering";
"lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices."; "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices.";
@ -840,6 +843,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_auto_night_mode_on" = "System"; "lng_settings_auto_night_mode_on" = "System";
"lng_settings_auto_night_warning" = "You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first."; "lng_settings_auto_night_warning" = "You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first.";
"lng_settings_auto_night_disable" = "Disable"; "lng_settings_auto_night_disable" = "Disable";
"lng_settings_font_family" = "Font family";
"lng_settings_color_title" = "Color preview"; "lng_settings_color_title" = "Color preview";
"lng_settings_color_reply" = "Reply to your message"; "lng_settings_color_reply" = "Reply to your message";
@ -1492,6 +1496,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages."; "lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages.";
"lng_manage_peer_reactions_some_title" = "Only allow these reactions"; "lng_manage_peer_reactions_some_title" = "Only allow these reactions";
"lng_manage_peer_reactions_available" = "Available reactions"; "lng_manage_peer_reactions_available" = "Available reactions";
"lng_manage_peer_reactions_available_ph" = "Add reactions...";
"lng_manage_peer_reactions_own" = "You can also {link} emoji packs and use them as reactions."; "lng_manage_peer_reactions_own" = "You can also {link} emoji packs and use them as reactions.";
"lng_manage_peer_reactions_own_link" = "create your own"; "lng_manage_peer_reactions_own_link" = "create your own";
"lng_manage_peer_reactions_level#one" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reaction."; "lng_manage_peer_reactions_level#one" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reaction.";
@ -1500,6 +1505,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_reactions_boost_link" = "here"; "lng_manage_peer_reactions_boost_link" = "here";
"lng_manage_peer_reactions_limit" = "Channels can't have more custom reactions."; "lng_manage_peer_reactions_limit" = "Channels can't have more custom reactions.";
"lng_manage_peer_reactions_max_title" = "Maximum number of reactions";
"lng_manage_peer_reactions_max_slider#one" = "{count} reaction per post";
"lng_manage_peer_reactions_max_slider#other" = "{count} reactions per post";
"lng_manage_peer_reactions_max_about" = "Limit the number of different reactions that can be added to a post, including already published ones.";
"lng_manage_peer_antispam" = "Aggressive Anti-Spam"; "lng_manage_peer_antispam" = "Aggressive Anti-Spam";
"lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions."; "lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.";
"lng_manage_peer_antispam_not_enough#one" = "Aggressive filtering can be enabled only in groups with more than **{count} member**."; "lng_manage_peer_antispam_not_enough#one" = "Aggressive filtering can be enabled only in groups with more than **{count} member**.";
@ -2269,6 +2279,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_business_about_chat_intro" = "Customize the message people see before they start a chat with you."; "lng_business_about_chat_intro" = "Customize the message people see before they start a chat with you.";
"lng_business_subtitle_chat_links" = "Links to Chat"; "lng_business_subtitle_chat_links" = "Links to Chat";
"lng_business_about_chat_links" = "Create links that start a chat with you, suggesting the first message."; "lng_business_about_chat_links" = "Create links that start a chat with you, suggesting the first message.";
"lng_business_subtitle_sponsored" = "Ads in Channels";
"lng_business_button_sponsored" = "Do Not Hide Ads";
"lng_business_about_sponsored" = "As a Premium subscriber, you dont see any ads on Telegram, but you can turn them on, for example, to view your own ads that you launched on the {link}";
"lng_business_about_sponsored_link" = "Telegram Ad Platform {emoji}";
"lng_business_about_sponsored_url" = "https://ads.telegram.org";
"lng_location_title" = "Location"; "lng_location_title" = "Location";
"lng_location_about" = "Display the location of your business on your account."; "lng_location_about" = "Display the location of your business on your account.";
@ -2839,7 +2854,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_in_dlg_audio_count#other" = "{count} audio"; "lng_in_dlg_audio_count#other" = "{count} audio";
"lng_ban_user" = "Ban User"; "lng_ban_user" = "Ban User";
"lng_ban_users" = "Ban users";
"lng_restrict_users" = "Restrict users";
"lng_delete_all_from_user" = "Delete all from {user}"; "lng_delete_all_from_user" = "Delete all from {user}";
"lng_delete_all_from_users" = "Delete all from users";
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
"lng_restrict_users_part" = "Partially restrict users {emoji}";
"lng_restrict_user_full" = "Fully ban this user {emoji}";
"lng_restrict_users_full" = "Fully ban users {emoji}";
"lng_restrict_users_part_single_header" = "What can this user do?";
"lng_restrict_users_part_header#one" = "What can {count} selected user do?";
"lng_restrict_users_part_header#other" = "What can {count} selected users do?";
"lng_report_spam" = "Report Spam"; "lng_report_spam" = "Report Spam";
"lng_report_spam_and_leave" = "Report spam and leave"; "lng_report_spam_and_leave" = "Report spam and leave";
"lng_report_spam_done" = "Thank you for your report."; "lng_report_spam_done" = "Thank you for your report.";
@ -3067,6 +3092,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_unread_bar_some" = "Unread messages"; "lng_unread_bar_some" = "Unread messages";
"lng_maps_point" = "Location"; "lng_maps_point" = "Location";
"lng_live_location" = "Live Location";
"lng_live_location_now" = "updated just now";
"lng_live_location_minutes#one" = "updated {count} minute ago";
"lng_live_location_minutes#other" = "updated {count} minutes ago";
"lng_live_location_hours#one" = "updated {count} hour ago";
"lng_live_location_hours#other" = "updated {count} hours ago";
"lng_live_location_today" = "updated today at {time}";
"lng_live_location_yesterday" = "updated yesterday at {time}";
"lng_live_location_date_time" = "updated {date} at {time}";
"lng_save_photo" = "Save image"; "lng_save_photo" = "Save image";
"lng_save_video" = "Save video"; "lng_save_video" = "Save video";
"lng_save_audio_file" = "Save audio file"; "lng_save_audio_file" = "Save audio file";
@ -3410,6 +3444,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_forward" = "Forward"; "lng_mediaview_forward" = "Forward";
"lng_mediaview_delete" = "Delete"; "lng_mediaview_delete" = "Delete";
"lng_mediaview_save_to_profile" = "Save to Profile"; "lng_mediaview_save_to_profile" = "Save to Profile";
"lng_mediaview_pin_story_done" = "Story pinned";
"lng_mediaview_pin_story_about" = "Now it will be always shown on the top.";
"lng_mediaview_pin_stories_done#one" = "{count} story pinned";
"lng_mediaview_pin_stories_done#other" = "{count} stories pinned";
"lng_mediaview_pin_stories_about#one" = "Now it will be always shown on the top.";
"lng_mediaview_pin_stories_about#other" = "Now they will be always shown on the top.";
"lng_mediaview_unpin_story_done" = "Story unpinned.";
"lng_mediaview_unpin_stories_done#one" = "{count} story unpinned";
"lng_mediaview_unpin_stories_done#other" = "{count} stories unpinned";
"lng_mediaview_pin_limit#one" = "You can't pin more than {count} story.";
"lng_mediaview_pin_limit#other" = "You can't pin more than {count} stories.";
"lng_mediaview_archive_story" = "Archive Story"; "lng_mediaview_archive_story" = "Archive Story";
"lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_photos_all" = "View all photos";
"lng_mediaview_files_all" = "View all files"; "lng_mediaview_files_all" = "View all files";
@ -4704,6 +4749,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_view_button_boost" = "Boost"; "lng_view_button_boost" = "Boost";
"lng_view_button_giftcode" = "Open"; "lng_view_button_giftcode" = "Open";
"lng_view_button_iv" = "Instant View"; "lng_view_button_iv" = "Instant View";
"lng_view_button_stickerset" = "View stickers";
"lng_view_button_emojipack" = "View emoji";
"lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_hide_ads" = "Hide";
"lng_sponsored_title" = "What are sponsored messages?"; "lng_sponsored_title" = "What are sponsored messages?";
@ -5073,6 +5120,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_iv_share" = "Share"; "lng_iv_share" = "Share";
"lng_iv_join_channel" = "Join"; "lng_iv_join_channel" = "Join";
"lng_iv_window_title" = "Instant View"; "lng_iv_window_title" = "Instant View";
"lng_iv_wrong_layout" = "Wrong layout?";
"lng_limit_download_title" = "Download speed limited"; "lng_limit_download_title" = "Download speed limited";
"lng_limit_download_subscribe" = "Subscribe to {link} and increase download speed {increase}."; "lng_limit_download_subscribe" = "Subscribe to {link} and increase download speed {increase}.";
@ -5087,6 +5135,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_limit_upload_increase_speed" = "by **{percent}**"; "lng_limit_upload_increase_speed" = "by **{percent}**";
"lng_limit_upload_subscribe_link" = "Telegram Premium"; "lng_limit_upload_subscribe_link" = "Telegram Premium";
"lng_recent_frequent" = "Frequent contacts";
"lng_recent_frequent_all" = "Show all";
"lng_recent_frequent_collapse" = "Collapse";
"lng_recent_title" = "Recent";
"lng_recent_clear" = "Clear";
"lng_recent_clear_sure" = "Do you want to clear your search history?";
"lng_recent_remove" = "Remove from Recent";
"lng_recent_clear_all" = "Clear all";
"lng_recent_hide_top" = "Remove all & Disable";
"lng_recent_hide_sure" = "Are you sure you want to clear and disable frequent contacts list?\n\nYou can always turn this feature back on in Settings > Privacy > Suggest Frequent Contacts.";
"lng_recent_hide_button" = "Hide";
"lng_recent_none" = "Recent search results\nwill appear here.";
"lng_recent_chats" = "Chats";
"lng_recent_channels" = "Channels";
"lng_channels_none_title" = "No channels yet...";
"lng_channels_none_about" = "You are not currently subscribed to any channels.";
"lng_channels_your_title" = "Channels you joined";
"lng_channels_your_more" = "Show more";
"lng_channels_your_less" = "Show less";
"lng_channels_recommended" = "Recommended channels";
"lng_font_box_title" = "Choose font family";
"lng_font_default" = "Default";
"lng_font_system" = "System font";
"lng_font_not_found" = "Font not found.";
// Wnd specific // Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program..."; "lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

@ -24,5 +24,7 @@
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file> <file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
<file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file> <file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file>
<file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file> <file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file>
<file alias="search.tgs">../../animations/search.tgs</file>
<file alias="noresults.tgs">../../animations/noresults.tgs</file>
</qresource> </qresource>
</RCC> </RCC>

View file

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

View file

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

View file

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

View file

@ -212,7 +212,7 @@ void ApplyBotsList(
} }
[[nodiscard]] ChatParticipants::Channels ParseSimilar( [[nodiscard]] ChatParticipants::Channels ParseSimilar(
not_null<ChannelData*> channel, not_null<Main::Session*> session,
const MTPmessages_Chats &chats) { const MTPmessages_Chats &chats) {
auto result = ChatParticipants::Channels(); auto result = ChatParticipants::Channels();
std::vector<not_null<ChannelData*>>(); std::vector<not_null<ChannelData*>>();
@ -220,13 +220,13 @@ void ApplyBotsList(
const auto &list = data.vchats().v; const auto &list = data.vchats().v;
result.list.reserve(list.size()); result.list.reserve(list.size());
for (const auto &chat : list) { for (const auto &chat : list) {
const auto peer = channel->owner().processChat(chat); const auto peer = session->data().processChat(chat);
if (const auto channel = peer->asChannel()) { if (const auto channel = peer->asChannel()) {
result.list.push_back(channel); result.list.push_back(channel);
} }
} }
if constexpr (MTPDmessages_chatsSlice::Is<decltype(data)>()) { if constexpr (MTPDmessages_chatsSlice::Is<decltype(data)>()) {
if (channel->session().premiumPossible()) { if (session->premiumPossible()) {
result.more = data.vcount().v - data.vchats().v.size(); result.more = data.vcount().v - data.vchats().v.size();
} }
} }
@ -234,6 +234,12 @@ void ApplyBotsList(
return result; return result;
} }
[[nodiscard]] ChatParticipants::Channels ParseSimilar(
not_null<ChannelData*> channel,
const MTPmessages_Chats &chats) {
return ParseSimilar(&channel->session(), chats);
}
} // namespace } // namespace
ChatParticipant::ChatParticipant( ChatParticipant::ChatParticipant(
@ -351,7 +357,8 @@ QString ChatParticipant::rank() const {
} }
ChatParticipants::ChatParticipants(not_null<ApiWrap*> api) ChatParticipants::ChatParticipants(not_null<ApiWrap*> api)
: _api(&api->instance()) { : _session(&api->session())
, _api(&api->instance()) {
} }
void ChatParticipants::requestForAdd( void ChatParticipants::requestForAdd(
@ -585,6 +592,33 @@ ChatParticipants::Parsed ChatParticipants::ParseRecent(
return result; return result;
} }
void ChatParticipants::Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
MTP_chatBannedRights(
MTP_flags(MTPDchatBannedRights::Flags::from_raw(
uint32(newRights.flags))),
MTP_int(newRights.until))
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
void ChatParticipants::requestSelf(not_null<ChannelData*> channel) { void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
if (_selfParticipantRequests.contains(channel)) { if (_selfParticipantRequests.contains(channel)) {
return; return;
@ -730,8 +764,11 @@ void ChatParticipants::loadSimilarChannels(not_null<ChannelData*> channel) {
return; return;
} }
} }
using Flag = MTPchannels_GetChannelRecommendations::Flag;
_similar[channel].requestId = _api.request( _similar[channel].requestId = _api.request(
MTPchannels_GetChannelRecommendations(channel->inputChannel) MTPchannels_GetChannelRecommendations(
MTP_flags(Flag::f_channel),
channel->inputChannel)
).done([=](const MTPmessages_Chats &result) { ).done([=](const MTPmessages_Chats &result) {
auto &similar = _similar[channel]; auto &similar = _similar[channel];
similar.requestId = 0; similar.requestId = 0;
@ -766,4 +803,29 @@ auto ChatParticipants::similarLoaded() const
return _similarLoaded.events(); return _similarLoaded.events();
} }
void ChatParticipants::loadRecommendations() {
if (_recommendationsLoaded.current() || _recommendations.requestId) {
return;
}
_recommendations.requestId = _api.request(
MTPchannels_GetChannelRecommendations(
MTP_flags(0),
MTP_inputChannelEmpty())
).done([=](const MTPmessages_Chats &result) {
_recommendations.requestId = 0;
auto parsed = ParseSimilar(_session, result);
_recommendations.channels = std::move(parsed);
_recommendations.channels.more = 0;
_recommendationsLoaded = true;
}).send();
}
const ChatParticipants::Channels &ChatParticipants::recommendations() const {
return _recommendations.channels;
}
rpl::producer<> ChatParticipants::recommendationsLoaded() const {
return _recommendationsLoaded.changes() | rpl::to_empty;
}
} // namespace Api } // namespace Api

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class ApiWrap; class ApiWrap;
class ChannelData; class ChannelData;
namespace Main {
class Session;
} // namespace Main
namespace Ui { namespace Ui {
class Show; class Show;
} // namespace Ui } // namespace Ui
@ -96,6 +100,13 @@ public:
static Parsed ParseRecent( static Parsed ParseRecent(
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
const TLMembers &data); const TLMembers &data);
static void Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail);
void add( void add(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer, not_null<PeerData*> peer,
@ -134,12 +145,18 @@ public:
[[nodiscard]] auto similarLoaded() const [[nodiscard]] auto similarLoaded() const
-> rpl::producer<not_null<ChannelData*>>; -> rpl::producer<not_null<ChannelData*>>;
void loadRecommendations();
[[nodiscard]] const Channels &recommendations() const;
[[nodiscard]] rpl::producer<> recommendationsLoaded() const;
private: private:
struct SimilarChannels { struct SimilarChannels {
Channels channels; Channels channels;
mtpRequestId requestId = 0; mtpRequestId requestId = 0;
}; };
const not_null<Main::Session*> _session;
MTP::Sender _api; MTP::Sender _api;
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>; using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
@ -165,6 +182,9 @@ private:
base::flat_map<not_null<ChannelData*>, SimilarChannels> _similar; base::flat_map<not_null<ChannelData*>, SimilarChannels> _similar;
rpl::event_stream<not_null<ChannelData*>> _similarLoaded; rpl::event_stream<not_null<ChannelData*>> _similarLoaded;
SimilarChannels _recommendations;
rpl::variable<bool> _recommendationsLoaded = false;
}; };
} // namespace Api } // namespace Api

View file

@ -62,6 +62,10 @@ void ConfirmPhone::resolve(
return bad("FirebaseSms"); return bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) { }, [&](const MTPDauth_sentCodeTypeEmailCode &) {
return bad("EmailCode"); return bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
return bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
return bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) { }, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
return bad("SetUpEmailRequired"); return bad("SetUpEmailRequired");
}); });

View file

@ -602,6 +602,41 @@ bool PremiumGiftCodeOptions::giveawayGiftsPurchaseAvailable() const {
false); false);
} }
SponsoredToggle::SponsoredToggle(not_null<Main::Session*> session)
: _api(&session->api().instance()) {
}
rpl::producer<bool> SponsoredToggle::toggled() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
_api.request(MTPusers_GetFullUser(
MTP_inputUserSelf()
)).done([=](const MTPusers_UserFull &result) {
consumer.put_next_copy(
result.data().vfull_user().data().is_sponsored_enabled());
}).fail([=] { consumer.put_next(false); }).send();
return lifetime;
};
}
rpl::producer<rpl::no_value, QString> SponsoredToggle::setToggled(bool v) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
_api.request(MTPaccount_ToggleSponsoredMessages(
MTP_bool(v)
)).done([=] {
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
RequirePremiumState ResolveRequiresPremiumToWrite( RequirePremiumState ResolveRequiresPremiumToWrite(
not_null<PeerData*> peer, not_null<PeerData*> peer,
History *maybeHistory) { History *maybeHistory) {

View file

@ -216,6 +216,18 @@ private:
}; };
class SponsoredToggle final {
public:
explicit SponsoredToggle(not_null<Main::Session*> session);
[[nodiscard]] rpl::producer<bool> toggled();
[[nodiscard]] rpl::producer<rpl::no_value, QString> setToggled(bool);
private:
MTP::Sender _api;
};
enum class RequirePremiumState { enum class RequirePremiumState {
Unknown, Unknown,
Yes, Yes,

View file

@ -760,14 +760,14 @@ rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
channel()->inputChannel channel()->inputChannel
)).done([=](const MTPstats_BroadcastRevenueStats &result) { )).done([=](const MTPstats_BroadcastRevenueStats &result) {
const auto &data = result.data(); const auto &data = result.data();
const auto &balances = data.vbalances().data();
_data = Data::EarnStatistics{ _data = Data::EarnStatistics{
.topHoursGraph = StatisticalGraphFromTL( .topHoursGraph = StatisticalGraphFromTL(
data.vtop_hours_graph()), data.vtop_hours_graph()),
.revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()), .revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()),
.currentBalance = data.vcurrent_balance().v, .currentBalance = balances.vcurrent_balance().v,
.availableBalance = data.vavailable_balance().v, .availableBalance = balances.vavailable_balance().v,
.overallRevenue = data.voverall_revenue().v, .overallRevenue = balances.voverall_revenue().v,
.usdRate = data.vusd_rate().v, .usdRate = data.vusd_rate().v,
}; };

View file

@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_dc_options.h" #include "mtproto/mtproto_dc_options.h"
#include "data/business/data_shortcut_messages.h" #include "data/business/data_shortcut_messages.h"
#include "data/components/scheduled_messages.h" #include "data/components/scheduled_messages.h"
#include "data/components/top_peers.h"
#include "data/notify/data_notify_settings.h" #include "data/notify/data_notify_settings.h"
#include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers.h"
#include "data/data_saved_messages.h" #include "data/data_saved_messages.h"
@ -1585,6 +1586,11 @@ void Updates::feedUpdate(const MTPUpdate &update) {
} else { } else {
if (existing) { if (existing) {
existing->destroy(); existing->destroy();
} else {
// Not the server-side date, but close enough.
session().topPeers().increment(
local->history()->peer,
local->date());
} }
local->setRealId(d.vid().v); local->setRealId(d.vid().v);
} }

View file

@ -940,7 +940,7 @@ int BackgroundPreviewBox::textsTop() const {
- st::historyPaddingBottom - st::historyPaddingBottom
- (_service ? _service->height() : 0) - (_service ? _service->height() : 0)
- _text1->height() - _text1->height()
- (forChannel() ? _text2->height() : 0); - (forChannel() ? 0 : _text2->height());
} }
QRect BackgroundPreviewBox::radialRect() const { QRect BackgroundPreviewBox::radialRect() const {

View file

@ -546,7 +546,7 @@ rightsToggle: Toggle(defaultToggle) {
vsize: 0px; vsize: 0px;
vshift: 1px; vshift: 1px;
stroke: 2px; stroke: 2px;
duration: 120; duration: universalDuration;
} }
rightsButton: SettingsButton(defaultSettingsButton) { rightsButton: SettingsButton(defaultSettingsButton) {
@ -691,6 +691,10 @@ createPollOptionField: InputField(createPollField) {
placeholderMargins: margins(2px, 0px, 2px, 0px); placeholderMargins: margins(2px, 0px, 2px, 0px);
heightMax: 68px; heightMax: 68px;
} }
createPollOptionFieldPremium: InputField(createPollOptionField) {
textMargins: margins(22px, 11px, 68px, 11px);
}
createPollOptionFieldPremiumEmojiPosition: point(15px, -1px);
createPollSolutionField: InputField(createPollField) { createPollSolutionField: InputField(createPollField) {
textMargins: margins(0px, 4px, 0px, 4px); textMargins: margins(0px, 4px, 0px, 4px);
border: 1px; border: 1px;
@ -704,7 +708,7 @@ createPollOptionRemove: CrossButton {
cross: CrossAnimation { cross: CrossAnimation {
size: 22px; size: 22px;
skip: 6px; skip: 6px;
stroke: 1.; stroke: 1.5;
minScale: 0.3; minScale: 0.3;
} }
crossFg: boxTitleCloseFg; crossFg: boxTitleCloseFg;
@ -718,6 +722,7 @@ createPollOptionRemove: CrossButton {
} }
} }
createPollOptionRemovePosition: point(11px, 9px); createPollOptionRemovePosition: point(11px, 9px);
createPollOptionEmojiPositionSkip: 4px;
createPollWarning: FlatLabel(defaultFlatLabel) { createPollWarning: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg; textFg: windowSubTextFg;
palette: TextPalette(defaultTextPalette) { palette: TextPalette(defaultTextPalette) {
@ -1074,3 +1079,23 @@ collectibleBox: Box(defaultBox) {
buttonHeight: 36px; buttonHeight: 36px;
button: collectibleCopy; button: collectibleCopy;
} }
moderateBoxUserpic: UserpicButton(defaultUserpicButton) {
size: size(34px, 42px);
photoSize: 34px;
photoPosition: point(0px, 4px);
}
moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }};
moderateBoxExpandHeight: 20px;
moderateBoxExpandRight: 10px;
moderateBoxExpandInnerSkip: 2px;
moderateBoxExpandFont: font(11px);
moderateBoxExpandToggleSize: 4px;
moderateBoxExpandToggleFourStrokes: 3px;
moderateBoxExpandIcon: icon{{ "info/edit/expand_arrow_small-flip_vertical", windowActiveTextFg }};
moderateBoxExpandIconDown: icon{{ "info/edit/expand_arrow_small", windowActiveTextFg }};
moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
palette: TextPalette(defaultTextPalette) {
selectLinkFg: windowActiveTextFg;
}
}

View file

@ -7,33 +7,41 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/create_poll_box.h" #include "boxes/create_poll_box.h"
#include "lang/lang_keys.h" #include "base/call_delayed.h"
#include "data/data_poll.h" #include "base/event_filter.h"
#include "ui/toast/toast.h" #include "base/random.h"
#include "ui/wrap/vertical_layout.h" #include "base/unique_qptr.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "main/main_session.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "menu/menu_send.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/view/history_view_schedule_box.h" #include "history/view/history_view_schedule_box.h"
#include "base/unique_qptr.h" #include "lang/lang_keys.h"
#include "base/event_filter.h" #include "main/main_session.h"
#include "base/call_delayed.h" #include "menu/menu_send.h"
#include "base/random.h" #include "ui/controls/emoji_button.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h" // defaultComposeFiles.
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace { namespace {
@ -46,12 +54,107 @@ constexpr auto kSolutionLimit = 200;
constexpr auto kWarnSolutionLimit = 60; constexpr auto kWarnSolutionLimit = 60;
constexpr auto kErrorLimit = 99; constexpr auto kErrorLimit = 99;
[[nodiscard]] not_null<Ui::EmojiButton*> AddEmojiToggleToField(
not_null<Ui::InputField*> field,
not_null<Ui::BoxContent*> box,
not_null<Window::SessionController*> controller,
not_null<ChatHelpers::TabbedPanel*> emojiPanel,
QPoint shift) {
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
field->parentWidget(),
st::defaultComposeFiles.emoji);
const auto fade = Ui::CreateChild<Ui::FadeAnimation>(
emojiToggle,
emojiToggle,
0.5);
{
const auto fadeTarget = Ui::CreateChild<Ui::RpWidget>(emojiToggle);
fadeTarget->resize(emojiToggle->size());
fadeTarget->paintRequest(
) | rpl::start_with_next([=](const QRect &rect) {
auto p = QPainter(fadeTarget);
if (fade->animating()) {
p.fillRect(fadeTarget->rect(), st::boxBg);
}
fade->paint(p);
}, fadeTarget->lifetime());
rpl::single(false) | rpl::then(
field->focusedChanges()
) | rpl::start_with_next([=](bool shown) {
if (shown) {
fade->fadeIn(st::universalDuration);
} else {
fade->fadeOut(st::universalDuration);
}
}, emojiToggle->lifetime());
fade->fadeOut(1);
fade->finish();
}
const auto outer = box->getDelegate()->outerContainer();
const auto allow = [](not_null<DocumentData*>) { return true; };
InitMessageFieldHandlers(
controller,
field,
Window::GifPauseReason::Layer,
allow);
Ui::Emoji::SuggestionsController::Init(
outer,
field,
&controller->session(),
Ui::Emoji::SuggestionsController::Options{
.suggestCustomEmoji = true,
.allowCustomWithoutPremium = allow,
});
const auto updateEmojiPanelGeometry = [=] {
const auto parent = emojiPanel->parentWidget();
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
const auto right = local.x() + emojiToggle->width() * 3;
const auto isDropDown = local.y() < parent->height() / 2;
emojiPanel->setDropDown(isDropDown);
if (isDropDown) {
emojiPanel->moveTopRight(
local.y() + emojiToggle->height(),
right);
} else {
emojiPanel->moveBottomRight(local.y(), right);
}
};
rpl::combine(
box->sizeValue(),
field->geometryValue()
) | rpl::start_with_next([=](QSize outer, QRect inner) {
emojiToggle->moveToLeft(
rect::right(inner) + shift.x(),
inner.y() + shift.y());
emojiToggle->update();
}, emojiToggle->lifetime());
emojiToggle->installEventFilter(emojiPanel);
emojiToggle->addClickHandler([=] {
updateEmojiPanelGeometry();
emojiPanel->toggleAnimated();
});
const auto filterCallback = [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter) {
updateEmojiPanelGeometry();
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(emojiToggle, filterCallback);
return emojiToggle;
}
class Options { class Options {
public: public:
Options( Options(
not_null<QWidget*> outer, not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel,
bool chooseCorrectEnabled); bool chooseCorrectEnabled);
[[nodiscard]] bool hasOptions() const; [[nodiscard]] bool hasOptions() const;
@ -140,9 +243,10 @@ private:
[[nodiscard]] auto createChooseCorrectGroup() [[nodiscard]] auto createChooseCorrectGroup()
-> std::shared_ptr<Ui::RadiobuttonGroup>; -> std::shared_ptr<Ui::RadiobuttonGroup>;
not_null<QWidget*> _outer; not_null<Ui::BoxContent*> _box;
not_null<Ui::VerticalLayout*> _container; not_null<Ui::VerticalLayout*> _container;
const not_null<Main::Session*> _session; const not_null<Window::SessionController*> _controller;
ChatHelpers::TabbedPanel * const _emojiPanel;
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup; std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
int _position = 0; int _position = 0;
std::vector<std::unique_ptr<Option>> _list; std::vector<std::unique_ptr<Option>> _list;
@ -154,6 +258,7 @@ private:
rpl::event_stream<not_null<QWidget*>> _scrollToWidget; rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
rpl::event_stream<> _backspaceInFront; rpl::event_stream<> _backspaceInFront;
rpl::event_stream<> _tabbed; rpl::event_stream<> _tabbed;
rpl::lifetime _emojiPanelLifetime;
}; };
@ -193,8 +298,9 @@ not_null<Ui::FlatLabel*> CreateWarningLabel(
if (value >= 0) { if (value >= 0) {
result->setText(QString::number(value)); result->setText(QString::number(value));
} else { } else {
constexpr auto kMinus = QChar(0x2212);
result->setMarkedText(Ui::Text::Colorized( result->setMarkedText(Ui::Text::Colorized(
QString::number(value))); kMinus + QString::number(std::abs(value))));
} }
result->setVisible(shown); result->setVisible(shown);
})); }));
@ -223,7 +329,9 @@ Options::Option::Option(
, _field( , _field(
Ui::CreateChild<Ui::InputField>( Ui::CreateChild<Ui::InputField>(
_content.get(), _content.get(),
st::createPollOptionField, session->user()->isPremium()
? st::createPollOptionFieldPremium
: st::createPollOptionField,
Ui::InputField::Mode::NoNewlines, Ui::InputField::Mode::NoNewlines,
tr::lng_polls_create_option_add())) { tr::lng_polls_create_option_add())) {
InitField(outer, _field, session); InitField(outer, _field, session);
@ -299,7 +407,7 @@ void Options::Option::createRemove() {
const auto remove = Ui::CreateChild<Ui::CrossButton>( const auto remove = Ui::CreateChild<Ui::CrossButton>(
field.get(), field.get(),
st::createPollOptionRemove); st::createPollOptionRemove);
remove->hide(anim::type::instant); remove->show(anim::type::instant);
const auto toggle = lifetime.make_state<rpl::variable<bool>>(false); const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
_removeAlways = lifetime.make_state<rpl::variable<bool>>(false); _removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
@ -309,6 +417,7 @@ void Options::Option::createRemove() {
// Don't capture 'this'! Because Option is a value type. // Don't capture 'this'! Because Option is a value type.
*toggle = !field->getLastText().isEmpty(); *toggle = !field->getLastText().isEmpty();
}, field->lifetime()); }, field->lifetime());
#if 0
rpl::combine( rpl::combine(
toggle->value(), toggle->value(),
_removeAlways->value(), _removeAlways->value(),
@ -316,6 +425,7 @@ void Options::Option::createRemove() {
) | rpl::start_with_next([=](bool shown) { ) | rpl::start_with_next([=](bool shown) {
remove->toggle(shown, anim::type::normal); remove->toggle(shown, anim::type::normal);
}, remove->lifetime()); }, remove->lifetime());
#endif
field->widthValue( field->widthValue(
) | rpl::start_with_next([=](int width) { ) | rpl::start_with_next([=](int width) {
@ -456,10 +566,16 @@ void Options::Option::removePlaceholder() const {
PollAnswer Options::Option::toPollAnswer(int index) const { PollAnswer Options::Option::toPollAnswer(int index) const {
Expects(index >= 0 && index < kMaxOptionsCount); Expects(index >= 0 && index < kMaxOptionsCount);
const auto text = field()->getTextWithTags();
auto result = PollAnswer{ auto result = PollAnswer{
field()->getLastText().trimmed(), TextWithEntities{
QByteArray(1, ('0' + index)) .text = text.text,
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
},
QByteArray(1, ('0' + index)),
}; };
TextUtilities::Trim(result.text);
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false; result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
return result; return result;
} }
@ -469,13 +585,15 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
} }
Options::Options( Options::Options(
not_null<QWidget*> outer, not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel,
bool chooseCorrectEnabled) bool chooseCorrectEnabled)
: _outer(outer) : _box(box)
, _container(container) , _container(container)
, _session(session) , _controller(controller)
, _emojiPanel(emojiPanel)
, _chooseCorrectGroup(chooseCorrectEnabled , _chooseCorrectGroup(chooseCorrectEnabled
? createChooseCorrectGroup() ? createChooseCorrectGroup()
: nullptr) : nullptr)
@ -645,12 +763,40 @@ void Options::addEmptyOption() {
(*(_list.end() - 2))->toggleRemoveAlways(true); (*(_list.end() - 2))->toggleRemoveAlways(true);
} }
_list.push_back(std::make_unique<Option>( _list.push_back(std::make_unique<Option>(
_outer, _box,
_container, _container,
_session, &_controller->session(),
_position + _list.size() + _destroyed.size(), _position + _list.size() + _destroyed.size(),
_chooseCorrectGroup)); _chooseCorrectGroup));
const auto field = _list.back()->field(); const auto field = _list.back()->field();
if (const auto emojiPanel = _emojiPanel) {
const auto emojiToggle = AddEmojiToggleToField(
field,
_box,
_controller,
emojiPanel,
QPoint(
-st::createPollOptionFieldPremium.textMargins.right(),
st::createPollOptionEmojiPositionSkip));
emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) {
if (!shown) {
return;
}
_emojiPanelLifetime.destroy();
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
if (field->hasFocus()) {
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
}
}, _emojiPanelLifetime);
emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
if (field->hasFocus()) {
Data::InsertCustomEmoji(field, data.document);
}
}, _emojiPanelLifetime);
}, emojiToggle->lifetime());
}
field->submits( field->submits(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
const auto index = findField(field); const auto index = findField(field);
@ -697,7 +843,7 @@ void Options::addEmptyOption() {
}); });
_list.back()->removeClicks( _list.back()->removeClicks(
) | rpl::take(1) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
Ui::PostponeCall(crl::guard(field, [=] { Ui::PostponeCall(crl::guard(field, [=] {
Expects(!_list.empty()); Expects(!_list.empty());
@ -789,19 +935,63 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion(
using namespace Settings; using namespace Settings;
const auto session = &_controller->session(); const auto session = &_controller->session();
const auto isPremium = session->user()->isPremium();
Ui::AddSubsectionTitle(container, tr::lng_polls_create_question()); Ui::AddSubsectionTitle(container, tr::lng_polls_create_question());
const auto question = container->add( const auto question = container->add(
object_ptr<Ui::InputField>( object_ptr<Ui::InputField>(
container, container,
st::createPollField, st::createPollField,
Ui::InputField::Mode::MultiLine, Ui::InputField::Mode::MultiLine,
tr::lng_polls_create_question_placeholder()), tr::lng_polls_create_question_placeholder()),
st::createPollFieldPadding); st::createPollFieldPadding
+ (isPremium
? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0)
: QMargins()));
InitField(getDelegate()->outerContainer(), question, session); InitField(getDelegate()->outerContainer(), question, session);
question->setMaxLength(kQuestionLimit + kErrorLimit); question->setMaxLength(kQuestionLimit + kErrorLimit);
question->setSubmitSettings(Ui::InputField::SubmitSettings::Both); question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
question->customTab(true); question->customTab(true);
if (isPremium) {
using Selector = ChatHelpers::TabbedSelector;
const auto outer = getDelegate()->outerContainer();
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
outer,
_controller,
object_ptr<Selector>(
nullptr,
_controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
const auto emojiPanel = _emojiPanel.get();
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(session->user());
const auto emojiToggle = AddEmojiToggleToField(
question,
this,
_controller,
emojiPanel,
st::createPollOptionFieldPremiumEmojiPosition);
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
if (question->hasFocus()) {
Ui::InsertEmojiAtCursor(question->textCursor(), data.emoji);
}
}, emojiToggle->lifetime());
emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
if (question->hasFocus()) {
Data::InsertCustomEmoji(question, data.document);
}
}, emojiToggle->lifetime());
}
const auto warning = CreateWarningLabel( const auto warning = CreateWarningLabel(
container, container,
question, question,
@ -910,9 +1100,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
st::defaultSubsectionTitle), st::defaultSubsectionTitle),
st::createPollFieldTitlePadding); st::createPollFieldTitlePadding);
const auto options = lifetime().make_state<Options>( const auto options = lifetime().make_state<Options>(
getDelegate()->outerContainer(), this,
container, container,
&_controller->session(), _controller,
_emojiPanel ? _emojiPanel.get() : nullptr,
(_chosen & PollData::Flag::Quiz)); (_chosen & PollData::Flag::Quiz));
auto limit = options->usedCount() | rpl::after_next([=](int count) { auto limit = options->usedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count); setCloseByEscape(!count);
@ -1029,9 +1220,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
}; };
const auto collectResult = [=] { const auto collectResult = [=] {
const auto textWithTags = question->getTextWithTags();
using Flag = PollData::Flag; using Flag = PollData::Flag;
auto result = PollData(&_controller->session().data(), id); auto result = PollData(&_controller->session().data(), id);
result.question = question->getLastText().trimmed(); result.question.text = textWithTags.text;
result.question.entities = TextUtilities::ConvertTextTagsToEntities(
textWithTags.tags);
TextUtilities::Trim(result.question);
result.answers = options->toPollAnswers(); result.answers = options->toPollAnswers();
const auto solutionWithTags = quiz->checked() const auto solutionWithTags = quiz->checked()
? solution->getTextWithAppliedMarkdown() ? solution->getTextWithAppliedMarkdown()

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
struct PollData; struct PollData;
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Ui { namespace Ui {
class VerticalLayout; class VerticalLayout;
} // namespace Ui } // namespace Ui
@ -72,6 +76,7 @@ private:
const PollData::Flags _disabled = PollData::Flags(); const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType(); const Api::SendType _sendType = Api::SendType();
const SendMenu::Type _sendMenuType; const SendMenu::Type _sendMenuType;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus; Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue; Fn<rpl::producer<bool>()> _dataIsValidValue;
rpl::event_stream<Result> _submitRequests; rpl::event_stream<Result> _submitRequests;

View file

@ -379,13 +379,7 @@ auto DeleteMessagesBox::revokeText(not_null<PeerData*> peer) const
return result; return result;
} }
const auto items = ranges::views::all( const auto items = peer->owner().idsToItems(_ids);
_ids
) | ranges::views::transform([&](FullMsgId id) {
return peer->owner().message(id);
}) | ranges::views::filter([](HistoryItem *item) {
return (item != nullptr);
}) | ranges::to_vector;
if (items.size() != _ids.size()) { if (items.size() != _ids.size()) {
// We don't have information about all messages. // We don't have information about all messages.

View file

@ -0,0 +1,856 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/moderate_messages_box.h"
#include "api/api_chat_participants.h"
#include "api/api_messages_search.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "base/timer.h"
#include "boxes/delete_messages_box.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_chat_participant_status.h"
#include "data/data_histories.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/toggle_arrow.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rect_part.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
namespace {
using Participants = std::vector<not_null<PeerData*>>;
struct ModerateOptions final {
bool allCanBan = false;
bool allCanDelete = false;
Participants participants;
};
ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
Expects(!items.empty());
auto result = ModerateOptions{
.allCanBan = true,
.allCanDelete = true,
};
const auto peer = items.front()->history()->peer;
for (const auto &item : items) {
if (!result.allCanBan && !result.allCanDelete) {
return {};
}
if (peer != item->history()->peer) {
return {};
}
if (!item->suggestBanReport()) {
result.allCanBan = false;
}
if (!item->suggestDeleteAllReport()) {
result.allCanDelete = false;
}
if (const auto p = item->from()) {
if (!ranges::contains(result.participants, not_null{ p })) {
result.participants.push_back(p);
}
}
}
return result;
}
[[nodiscard]] rpl::producer<int> MessagesCountValue(
not_null<History*> history,
not_null<PeerData*> from) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
auto search = lifetime.make_state<Api::MessagesSearch>(history);
consumer.put_next(0);
search->messagesFounds(
) | rpl::start_with_next([=](const Api::FoundMessages &found) {
consumer.put_next_copy(found.total);
}, lifetime);
search->searchMessages({ .from = from });
return lifetime;
};
}
class Button final : public Ui::RippleButton {
public:
Button(not_null<QWidget*> parent, int count);
void setChecked(bool checked);
[[nodiscard]] bool checked() const;
[[nodiscard]] static QSize ComputeSize(int);
private:
void paintEvent(QPaintEvent *event) override;
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
const int _count;
const QString _text;
bool _checked = false;
Ui::Animations::Simple _animation;
};
Button::Button(not_null<QWidget*> parent, int count)
: RippleButton(parent, st::defaultRippleAnimation)
, _count(count)
, _text(QString::number(std::abs(_count))) {
}
QSize Button::ComputeSize(int count) {
return QSize(
st::moderateBoxExpandHeight
+ st::moderateBoxExpand.width()
+ st::moderateBoxExpandInnerSkip * 4
+ st::moderateBoxExpandFont->width(
QString::number(std::abs(count)))
+ st::moderateBoxExpandToggleSize,
st::moderateBoxExpandHeight);
}
void Button::setChecked(bool checked) {
if (_checked == checked) {
return;
}
_checked = checked;
_animation.stop();
_animation.start(
[=] { update(); },
checked ? 0 : 1,
checked ? 1 : 0,
st::slideWrapDuration);
}
bool Button::checked() const {
return _checked;
}
void Button::paintEvent(QPaintEvent *event) {
auto p = Painter(this);
auto hq = PainterHighQualityEnabler(p);
Ui::RippleButton::paintRipple(p, QPoint());
const auto radius = height() / 2;
p.setPen(Qt::NoPen);
st::moderateBoxExpand.paint(
p,
radius,
(height() - st::moderateBoxExpand.height()) / 2,
width());
const auto innerSkip = st::moderateBoxExpandInnerSkip;
p.setBrush(Qt::NoBrush);
p.setPen(st::boxTextFg);
p.setFont(st::moderateBoxExpandFont);
p.drawText(
QRect(
innerSkip + radius + st::moderateBoxExpand.width(),
0,
width(),
height()),
_text,
style::al_left);
const auto path = Ui::ToggleUpDownArrowPath(
width() - st::moderateBoxExpandToggleSize - radius,
height() / 2,
st::moderateBoxExpandToggleSize,
st::moderateBoxExpandToggleFourStrokes,
_animation.value(_checked ? 1. : 0.));
p.fillPath(path, st::boxTextFg);
}
QImage Button::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(size(), size().height() / 2);
}
QPoint Button::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
} // namespace
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed) {
struct Controller final {
rpl::event_stream<bool> toggleRequestsFromTop;
rpl::event_stream<bool> toggleRequestsFromInner;
rpl::event_stream<bool> checkAllRequests;
Fn<Participants()> collectRequests;
};
const auto [allCanBan, allCanDelete, participants]
= CalculateModerateOptions(items);
const auto inner = box->verticalLayout();
Assert(!participants.empty());
const auto confirms = inner->lifetime().make_state<rpl::event_stream<>>();
const auto isSingle = participants.size() == 1;
const auto buttonPadding = isSingle
? QMargins()
: QMargins(0, 0, Button::ComputeSize(participants.size()).width(), 0);
const auto session = &items.front()->history()->session();
const auto historyPeerId = items.front()->history()->peer->id;
using Request = Fn<void(not_null<PeerData*>, not_null<ChannelData*>)>;
const auto sequentiallyRequest = [=](
Request request,
Participants participants) {
constexpr auto kSmallDelayMs = 5;
const auto participantIds = ranges::views::all(
participants
) | ranges::views::transform([](not_null<PeerData*> peer) {
return peer->id;
}) | ranges::to_vector;
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto counter = lifetime->make_state<int>(0);
const auto timer = lifetime->make_state<base::Timer>();
timer->setCallback(crl::guard(session, [=] {
if ((*counter) < participantIds.size()) {
const auto peer = session->data().peer(historyPeerId);
const auto channel = peer ? peer->asChannel() : nullptr;
const auto from = session->data().peer(
participantIds[*counter]);
if (channel && from) {
request(from, channel);
}
(*counter)++;
} else {
lifetime->destroy();
}
}));
timer->callEach(kSmallDelayMs);
};
const auto handleConfirmation = [=](
not_null<Ui::Checkbox*> checkbox,
not_null<Controller*> controller,
Request request) {
confirms->events() | rpl::start_with_next([=] {
if (checkbox->checked() && controller->collectRequests) {
sequentiallyRequest(request, controller->collectRequests());
}
}, checkbox->lifetime());
};
const auto isEnter = [=](not_null<QEvent*> event) {
if (event->type() == QEvent::KeyPress) {
if (const auto k = static_cast<QKeyEvent*>(event.get())) {
return (k->key() == Qt::Key_Enter)
|| (k->key() == Qt::Key_Return);
}
}
return false;
};
base::install_event_filter(box, [=](not_null<QEvent*> event) {
if (isEnter(event)) {
box->triggerButton(0);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
const auto handleSubmition = [=](not_null<Ui::Checkbox*> checkbox) {
base::install_event_filter(box, [=](not_null<QEvent*> event) {
if (!isEnter(event) || !checkbox->checked()) {
return base::EventFilterResult::Continue;
}
box->uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_gigagroup_warning_title(),
.confirmed = [=](Fn<void()> close) {
box->triggerButton(0);
close();
},
.confirmText = tr::lng_box_yes(),
.cancelText = tr::lng_box_no(),
}));
return base::EventFilterResult::Cancel;
});
};
const auto createParticipantsList = [&](
not_null<Controller*> controller) {
const auto wrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
wrap->toggle(false, anim::type::instant);
controller->toggleRequestsFromTop.events(
) | rpl::start_with_next([=](bool toggled) {
wrap->toggle(toggled, anim::type::normal);
}, wrap->lifetime());
const auto container = wrap->entity();
Ui::AddSkip(container);
auto &lifetime = wrap->lifetime();
const auto clicks = lifetime.make_state<rpl::event_stream<>>();
const auto checkboxes = ranges::views::all(
participants
) | ranges::views::transform([&](not_null<PeerData*> peer) {
const auto line = container->add(
object_ptr<Ui::AbstractButton>(container));
const auto &st = st::moderateBoxUserpic;
line->resize(line->width(), st.size.height());
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
line,
peer,
st);
const auto checkbox = Ui::CreateChild<Ui::Checkbox>(
line,
peer->name(),
false,
st::defaultBoxCheckbox);
line->widthValue(
) | rpl::start_with_next([=](int width) {
userpic->moveToLeft(
st::boxRowPadding.left()
+ checkbox->checkRect().width()
+ st::defaultBoxCheckbox.textPosition.x(),
0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
checkbox->resizeToWidth(width
- rect::right(userpic)
- skip
- st::boxRowPadding.right());
checkbox->moveToLeft(
rect::right(userpic) + skip,
((userpic->height() - checkbox->height()) / 2)
+ st::defaultBoxCheckbox.margin.top());
}, checkbox->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
checkbox->setAttribute(Qt::WA_TransparentForMouseEvents);
line->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
clicks->fire({});
});
return checkbox;
}) | ranges::to_vector;
clicks->events(
) | rpl::start_with_next([=] {
controller->toggleRequestsFromInner.fire_copy(
ranges::any_of(checkboxes, &Ui::Checkbox::checked));
}, container->lifetime());
controller->checkAllRequests.events(
) | rpl::start_with_next([=](bool checked) {
for (const auto &c : checkboxes) {
c->setChecked(checked);
}
}, container->lifetime());
controller->collectRequests = [=] {
auto result = Participants();
for (auto i = 0; i < checkboxes.size(); i++) {
if (checkboxes[i]->checked()) {
result.push_back(participants[i]);
}
}
return result;
};
};
const auto appendList = [&](
not_null<Ui::Checkbox*> checkbox,
not_null<Controller*> controller) {
if (isSingle) {
const auto p = participants.front();
controller->collectRequests = [=] { return Participants{ p }; };
return;
}
const auto count = int(participants.size());
const auto button = Ui::CreateChild<Button>(inner, count);
button->resize(Button::ComputeSize(count));
const auto overlay = Ui::CreateChild<Ui::AbstractButton>(inner);
checkbox->geometryValue(
) | rpl::start_with_next([=](const QRect &rect) {
overlay->setGeometry(rect);
overlay->raise();
button->moveToRight(
st::moderateBoxExpandRight,
rect.top() + (rect.height() - button->height()) / 2,
box->width());
button->raise();
}, button->lifetime());
controller->toggleRequestsFromInner.events(
) | rpl::start_with_next([=](bool toggled) {
checkbox->setChecked(toggled);
}, checkbox->lifetime());
button->setClickedCallback([=] {
button->setChecked(!button->checked());
controller->toggleRequestsFromTop.fire_copy(button->checked());
});
overlay->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
controller->checkAllRequests.fire_copy(checkbox->checked());
});
createParticipantsList(controller);
};
Ui::AddSkip(inner);
const auto title = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(items.size() == 1)
? tr::lng_selected_delete_sure_this()
: tr::lng_selected_delete_sure(
lt_count,
rpl::single(items.size()) | tr::to_count()),
st::boxLabel));
Ui::AddSkip(inner);
Ui::AddSkip(inner);
Ui::AddSkip(inner);
{
const auto report = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_report_spam(tr::now),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>();
appendList(report, controller);
handleSubmition(report);
const auto ids = items.front()->from()->owner().itemsToIds(items);
handleConfirmation(report, controller, [=](
not_null<PeerData*> p,
not_null<ChannelData*> c) {
auto filtered = ranges::views::all(
ids
) | ranges::views::transform([](const FullMsgId &id) {
return MTP_int(id.msg);
}) | ranges::to<QVector<MTPint>>();
c->session().api().request(
MTPchannels_ReportSpam(
c->inputChannel,
p->input,
MTP_vector<MTPint>(std::move(filtered)))
).send();
});
}
if (allCanDelete) {
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto deleteAll = inner->add(
object_ptr<Ui::Checkbox>(
inner,
!(isSingle)
? tr::lng_delete_all_from_users(
tr::now,
Ui::Text::WithEntities)
: tr::lng_delete_all_from_user(
tr::now,
lt_user,
Ui::Text::Bold(items.front()->from()->name()),
Ui::Text::WithEntities),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
if (isSingle) {
const auto history = items.front()->history();
tr::lng_selected_delete_sure(
lt_count,
rpl::combine(
MessagesCountValue(history, participants.front()),
deleteAll->checkedValue()
) | rpl::map([s = items.size()](int all, bool checked) {
return float64((checked && all) ? all : s);
})
) | rpl::start_with_next([=](const QString &text) {
title->setText(text);
title->resizeToWidth(inner->width()
- rect::m::sum::h(st::boxRowPadding));
}, title->lifetime());
}
const auto controller = box->lifetime().make_state<Controller>();
appendList(deleteAll, controller);
handleSubmition(deleteAll);
handleConfirmation(deleteAll, controller, [=](
not_null<PeerData*> p,
not_null<ChannelData*> c) {
p->session().api().deleteAllFromParticipant(c, p);
});
}
if (allCanBan) {
auto ownedWrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner));
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto ban = inner->add(
object_ptr<Ui::Checkbox>(
box,
rpl::conditional(
ownedWrap->toggledValue(),
tr::lng_context_restrict_user(),
rpl::conditional(
rpl::single(isSingle),
tr::lng_ban_user(),
tr::lng_ban_users())),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>();
appendList(ban, controller);
handleSubmition(ban);
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto wrap = inner->add(std::move(ownedWrap));
const auto container = wrap->entity();
wrap->toggle(false, anim::type::instant);
const auto session = &participants.front()->session();
const auto emojiMargin = QMargins(
-st::moderateBoxExpandInnerSkip,
-st::moderateBoxExpandInnerSkip / 2,
0,
0);
const auto emojiUp = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIcon,
emojiMargin,
false));
const auto emojiDown = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIconDown,
emojiMargin,
false));
auto label = object_ptr<Ui::FlatLabel>(
inner,
QString(),
st::moderateBoxDividerLabel);
const auto raw = label.data();
auto &lifetime = wrap->lifetime();
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
label->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
}
wrap->toggle(!wrap->toggled(), anim::type::normal);
{
inner->heightValue() | rpl::start_with_next([=] {
if (!wrap->animating()) {
scrollLifetime->destroy();
Ui::PostponeCall(crl::guard(box, [=] {
box->scrollToY(std::numeric_limits<int>::max());
}));
} else {
box->scrollToY(std::numeric_limits<int>::max());
}
}, *scrollLifetime);
}
return true;
});
wrap->toggledValue(
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
return ((toggled && isSingle)
? tr::lng_restrict_user_part
: (toggled && !isSingle)
? tr::lng_restrict_users_part
: isSingle
? tr::lng_restrict_user_full
: tr::lng_restrict_users_full)(
lt_emoji,
rpl::single(toggled ? emojiUp : emojiDown),
Ui::Text::WithEntities);
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { raw->update(); },
});
}, label->lifetime());
Ui::AddSkip(inner);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(label),
st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom));
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
const auto peer = items.front()->history()->peer;
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareFlags = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
{
const auto disabled = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
result.emplace(
disabled,
tr::lng_rights_restriction_for_all(tr::now));
}
return result;
}();
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
box,
rpl::conditional(
rpl::single(isSingle),
tr::lng_restrict_users_part_single_header(),
tr::lng_restrict_users_part_header(
lt_count,
rpl::single(participants.size()) | tr::to_count())),
prepareFlags,
disabledMessages,
{ .isForum = peer->isForum() });
std::move(changes) | rpl::start_with_next([=] {
ban->setChecked(true);
}, ban->lifetime());
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
container->add(std::move(checkboxes));
// Handle confirmation manually.
confirms->events() | rpl::start_with_next([=] {
if (ban->checked() && controller->collectRequests) {
const auto kick = !wrap->toggled();
const auto restrictions = getRestrictions();
const auto request = [=](
not_null<PeerData*> peer,
not_null<ChannelData*> channel) {
if (!kick) {
Api::ChatParticipants::Restrict(
channel,
peer,
ChatRestrictionsInfo(), // Unused.
ChatRestrictionsInfo(restrictions, 0),
nullptr,
nullptr);
} else {
channel->session().api().chatParticipants().kick(
channel,
peer,
{ channel->restrictions(), 0 });
}
};
sequentiallyRequest(request, controller->collectRequests());
}
}, ban->lifetime());
}
const auto close = crl::guard(box, [=] { box->closeBox(); });
{
const auto data = &participants.front()->session().data();
const auto ids = data->itemsToIds(items);
box->addButton(tr::lng_box_delete(), [=] {
confirms->fire({});
if (confirmed) {
confirmed();
}
data->histories().deleteMessages(ids, true);
data->sendHistoryChangeNotifications();
close();
});
}
box->addButton(tr::lng_cancel(), close);
}
bool CanCreateModerateMessagesBox(const HistoryItemsList &items) {
const auto options = CalculateModerateOptions(items);
return (options.allCanBan || options.allCanDelete)
&& !options.participants.empty();
}
void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) {
const auto container = box->verticalLayout();
const auto maybeUser = peer->asUser();
Ui::AddSkip(container);
Ui::AddSkip(container);
base::install_event_filter(box, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::KeyPress) {
if (const auto k = static_cast<QKeyEvent*>(event.get())) {
if ((k->key() == Qt::Key_Enter)
|| (k->key() == Qt::Key_Return)) {
box->uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_gigagroup_warning_title(),
.confirmed = [=](Fn<void()> close) {
box->triggerButton(0);
close();
},
.confirmText = tr::lng_box_yes(),
.cancelText = tr::lng_box_no(),
}));
}
}
}
return base::EventFilterResult::Continue;
});
const auto line = container->add(object_ptr<Ui::RpWidget>(container));
const auto &st = st::mainMenuUserpic;
line->resize(line->width(), st.size.height());
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
line,
peer,
st);
userpic->showSavedMessagesOnSelf(true);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
line,
peer->isSelf()
? tr::lng_saved_messages() | Ui::Text::ToBold()
: maybeUser
? tr::lng_profile_delete_conversation() | Ui::Text::ToBold()
: rpl::single(Ui::Text::Bold(peer->name())) | rpl::type_erased(),
box->getDelegate()->style().title);
line->widthValue(
) | rpl::start_with_next([=](int width) {
userpic->moveToLeft(st::boxRowPadding.left(), 0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
label->resizeToWidth(width
- rect::right(userpic)
- skip
- st::boxRowPadding.right());
label->moveToLeft(
rect::right(userpic) + skip,
((userpic->height() - label->height()) / 2));
}, label->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
Ui::AddSkip(container);
Ui::AddSkip(container);
box->addRow(
object_ptr<Ui::FlatLabel>(
container,
peer->isSelf()
? tr::lng_sure_delete_saved_messages()
: maybeUser
? tr::lng_sure_delete_history(
lt_contact,
rpl::single(peer->name()))
: (peer->isChannel() && !peer->isMegagroup())
? tr::lng_sure_leave_channel()
: tr::lng_sure_leave_group(),
st::boxLabel));
const auto maybeCheckbox = [&]() -> Ui::Checkbox* {
if (!peer->canRevokeFullHistory()) {
return nullptr;
}
Ui::AddSkip(container);
Ui::AddSkip(container);
return box->addRow(
object_ptr<Ui::Checkbox>(
container,
maybeUser
? tr::lng_delete_for_other_check(
tr::now,
lt_user,
TextWithEntities{ maybeUser->firstName },
Ui::Text::RichLangValue)
: tr::lng_delete_for_everyone_check(
tr::now,
Ui::Text::WithEntities),
false,
st::defaultBoxCheckbox));
}();
Ui::AddSkip(container);
auto buttonText = maybeUser
? tr::lng_box_delete()
: !maybeCheckbox
? tr::lng_box_leave()
: maybeCheckbox->checkedValue() | rpl::map([](bool checked) {
return checked ? tr::lng_box_delete() : tr::lng_box_leave();
}) | rpl::flatten_latest();
const auto close = crl::guard(box, [=] { box->closeBox(); });
box->addButton(std::move(buttonText), [=] {
const auto revoke = maybeCheckbox && maybeCheckbox->checked();
Core::App().closeChatFromWindows(peer);
// Don't delete old history by default,
// because Android app doesn't.
//
//if (const auto from = peer->migrateFrom()) {
// peer->session().api().deleteConversation(from, false);
//}
peer->session().api().deleteConversation(peer, revoke);
close();
}, st::attentionBoxButton);
box->addButton(tr::lng_cancel(), close);
}

View file

@ -0,0 +1,23 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class PeerData;
namespace Ui {
class GenericBox;
} // namespace Ui
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed);
[[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &);
void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer);

View file

@ -734,6 +734,10 @@ auto PeerListRow::generateNameWords() const
return peer()->nameWords(); return peer()->nameWords();
} }
const style::PeerListItem &PeerListRow::computeSt(
const style::PeerListItem &st) const {
return st;
}
void PeerListRow::invalidatePixmapsCache() { void PeerListRow::invalidatePixmapsCache() {
if (_checkbox) { if (_checkbox) {
@ -816,9 +820,14 @@ void PeerListRow::stopLastRipple() {
} }
} }
void PeerListRow::paintRipple(Painter &p, int x, int y, int outerWidth) { void PeerListRow::paintRipple(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
if (_ripple) { if (_ripple) {
_ripple->paint(p, x, y, outerWidth); _ripple->paint(p, x, y, outerWidth, &st.button.ripple.color->c);
if (_ripple->empty()) { if (_ripple->empty()) {
_ripple.reset(); _ripple.reset();
} }
@ -1689,7 +1698,9 @@ crl::time PeerListContent::paintRow(
const auto row = getRow(index); const auto row = getRow(index);
Assert(row != nullptr); Assert(row != nullptr);
row->lazyInitialize(_st.item); const auto &st = row->computeSt(_st.item);
row->lazyInitialize(st);
const auto outerWidth = width(); const auto outerWidth = width();
auto refreshStatusAt = row->refreshStatusTime(); auto refreshStatusAt = row->refreshStatusTime();
@ -1717,8 +1728,8 @@ crl::time PeerListContent::paintRow(
const auto opacity = row->opacity(); const auto opacity = row->opacity();
const auto &bg = selected const auto &bg = selected
? _st.item.button.textBgOver ? st.button.textBgOver
: _st.item.button.textBg; : st.button.textBg;
if (opacity < 1.) { if (opacity < 1.) {
p.setOpacity(opacity); p.setOpacity(opacity);
} }
@ -1729,36 +1740,37 @@ crl::time PeerListContent::paintRow(
}); });
p.fillRect(0, 0, outerWidth, _rowHeight, bg); p.fillRect(0, 0, outerWidth, _rowHeight, bg);
row->paintRipple(p, 0, 0, outerWidth); row->paintRipple(p, st, 0, 0, outerWidth);
row->paintUserpic( row->paintUserpic(
p, p,
_st.item, st,
_st.item.photoPosition.x(), st.photoPosition.x(),
_st.item.photoPosition.y(), st.photoPosition.y(),
outerWidth); outerWidth);
p.setPen(st::contactsNameFg); p.setPen(st::contactsNameFg);
const auto skipRight = _st.item.photoPosition.x(); const auto skipRight = st.photoPosition.x();
const auto rightActionSize = row->rightActionSize(); const auto rightActionSize = row->rightActionSize();
const auto rightActionMargins = rightActionSize.isEmpty() const auto rightActionMargins = rightActionSize.isEmpty()
? QMargins() ? QMargins()
: row->rightActionMargins(); : row->rightActionMargins();
const auto &name = row->name(); const auto &name = row->name();
const auto namex = _st.item.namePosition.x(); const auto namePosition = st.namePosition;
const auto namey = _st.item.namePosition.y(); const auto namex = namePosition.x();
const auto namey = namePosition.y();
auto namew = outerWidth - namex - skipRight; auto namew = outerWidth - namex - skipRight;
if (!rightActionSize.isEmpty() if (!rightActionSize.isEmpty()
&& (namey < rightActionMargins.top() + rightActionSize.height()) && (namey < rightActionMargins.top() + rightActionSize.height())
&& (namey + _st.item.nameStyle.font->height && (namey + st.nameStyle.font->height
> rightActionMargins.top())) { > rightActionMargins.top())) {
namew -= rightActionMargins.left() namew -= rightActionMargins.left()
+ rightActionSize.width() + rightActionSize.width()
+ rightActionMargins.right() + rightActionMargins.right()
- skipRight; - skipRight;
} }
const auto statusx = _st.item.statusPosition.x(); const auto statusx = st.statusPosition.x();
const auto statusy = _st.item.statusPosition.y(); const auto statusy = st.statusPosition.y();
auto statusw = outerWidth - statusx - skipRight; auto statusw = outerWidth - statusx - skipRight;
if (!rightActionSize.isEmpty() if (!rightActionSize.isEmpty()
&& (statusy < rightActionMargins.top() + rightActionSize.height()) && (statusy < rightActionMargins.top() + rightActionSize.height())
@ -1780,7 +1792,7 @@ crl::time PeerListContent::paintRow(
width(), width(),
selected); selected);
auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio(); auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
p.setPen(anim::pen(_st.item.nameFg, _st.item.nameFgChecked, nameCheckedRatio)); p.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));
name.drawLeftElided(p, namex, namey, namew, width()); name.drawLeftElided(p, namex, namey, namew, width());
p.setFont(st::contactsStatusFont); p.setFont(st::contactsStatusFont);
@ -1799,17 +1811,17 @@ crl::time PeerListContent::paintRow(
if (highlightedWidth > availableWidth) { if (highlightedWidth > availableWidth) {
highlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth); highlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth);
} }
p.setPen(_st.item.statusFgActive); p.setPen(st.statusFgActive);
p.drawTextLeft(statusx, statusy, width(), highlightedPart); p.drawTextLeft(statusx, statusy, width(), highlightedPart);
} else { } else {
grayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth); grayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth);
p.setPen(_st.item.statusFgActive); p.setPen(st.statusFgActive);
p.drawTextLeft(statusx, statusy, width(), highlightedPart); p.drawTextLeft(statusx, statusy, width(), highlightedPart);
p.setPen(selected ? _st.item.statusFgOver : _st.item.statusFg); p.setPen(selected ? st.statusFgOver : st.statusFg);
p.drawTextLeft(statusx + highlightedWidth, statusy, width(), grayedPart); p.drawTextLeft(statusx + highlightedWidth, statusy, width(), grayedPart);
} }
} else { } else {
row->paintStatusText(p, _st.item, statusx, statusy, statusw, width(), selected); row->paintStatusText(p, st, statusx, statusy, statusw, width(), selected);
} }
row->elementsPaint( row->elementsPaint(
@ -1905,10 +1917,30 @@ void PeerListContent::selectSkipPage(int height, int direction) {
selectSkip(rowsToSkip * direction); selectSkip(rowsToSkip * direction);
} }
void PeerListContent::selectLast() {
const auto rowsCount = shownRowsCount();
const auto newSelectedIndex = rowsCount - 1;
_selected.index.value = newSelectedIndex;
_selected.element = 0;
if (newSelectedIndex >= 0) {
auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0;
auto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();
_scrollToRequests.fire({ top, bottom });
}
update();
_selectedIndex = _selected.index.value;
}
rpl::producer<int> PeerListContent::selectedIndexValue() const { rpl::producer<int> PeerListContent::selectedIndexValue() const {
return _selectedIndex.value(); return _selectedIndex.value();
} }
int PeerListContent::selectedIndex() const {
return _selectedIndex.current();
}
bool PeerListContent::hasSelection() const { bool PeerListContent::hasSelection() const {
return _selected.index.value >= 0; return _selected.index.value >= 0;
} }

View file

@ -100,6 +100,8 @@ public:
-> const base::flat_set<QChar> &; -> const base::flat_set<QChar> &;
[[nodiscard]] virtual auto generateNameWords() const [[nodiscard]] virtual auto generateNameWords() const
-> const base::flat_set<QString> &; -> const base::flat_set<QString> &;
[[nodiscard]] virtual const style::PeerListItem &computeSt(
const style::PeerListItem &st) const;
virtual void preloadUserpic(); virtual void preloadUserpic();
@ -228,7 +230,12 @@ public:
QPoint point, QPoint point,
UpdateCallback &&updateCallback); UpdateCallback &&updateCallback);
void stopLastRipple(); void stopLastRipple();
void paintRipple(Painter &p, int x, int y, int outerWidth); void paintRipple(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth);
void paintUserpic( void paintUserpic(
Painter &p, Painter &p,
const style::PeerListItem &st, const style::PeerListItem &st,
@ -601,6 +608,7 @@ public:
}; };
SkipResult selectSkip(int direction); SkipResult selectSkip(int direction);
void selectSkipPage(int height, int direction); void selectSkipPage(int height, int direction);
void selectLast();
enum class Mode { enum class Mode {
Default, Default,
@ -609,6 +617,7 @@ public:
void setMode(Mode mode); void setMode(Mode mode);
[[nodiscard]] rpl::producer<int> selectedIndexValue() const; [[nodiscard]] rpl::producer<int> selectedIndexValue() const;
[[nodiscard]] int selectedIndex() const;
[[nodiscard]] bool hasSelection() const; [[nodiscard]] bool hasSelection() const;
[[nodiscard]] bool hasPressed() const; [[nodiscard]] bool hasPressed() const;
void clearSelection(); void clearSelection();

View file

@ -166,33 +166,6 @@ void SaveChannelAdmin(
}).send(); }).send();
} }
void SaveChannelRestriction(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
MTP_chatBannedRights(
MTP_flags(MTPDchatBannedRights::Flags::from_raw(
uint32(newRights.flags))),
MTP_int(newRights.until))
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
void SaveChatParticipantKick( void SaveChatParticipantKick(
not_null<ChatData*> chat, not_null<ChatData*> chat,
not_null<UserData*> user, not_null<UserData*> user,
@ -275,7 +248,7 @@ Fn<void(
ChatRestrictionsInfo newRights) { ChatRestrictionsInfo newRights) {
const auto done = [=] { if (onDone) onDone(newRights); }; const auto done = [=] { if (onDone) onDone(newRights); };
const auto saveForChannel = [=](not_null<ChannelData*> channel) { const auto saveForChannel = [=](not_null<ChannelData*> channel) {
SaveChannelRestriction( Api::ChatParticipants::Restrict(
channel, channel,
participant, participant,
oldRights, oldRights,

View file

@ -312,6 +312,7 @@ PreviewWrap::PreviewWrap(
nullptr, // document nullptr, // document
WebPageCollage(), WebPageCollage(),
nullptr, // iv nullptr, // iv
nullptr, // stickerSet
0, // duration 0, // duration
QString(), // author QString(), // author
false, // hasLargeMedia false, // hasLargeMedia

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_user_names.h" #include "api/api_user_names.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "base/event_filter.h"
#include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_peer_color_box.h" #include "boxes/peers/edit_peer_color_box.h"
#include "boxes/peers/edit_peer_common.h" #include "boxes/peers/edit_peer_common.h"
@ -27,6 +28,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/stickers_box.h" #include "boxes/stickers_box.h"
#include "boxes/username_box.h" #include "boxes/username_box.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "data/data_channel.h" #include "data/data_channel.h"
@ -47,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "settings/settings_common.h" #include "settings/settings_common.h"
#include "ui/boxes/boost_box.h" #include "ui/boxes/boost_box.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/userpic_button.h" #include "ui/controls/userpic_button.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
@ -61,6 +65,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "api/api_invite_links.h" #include "api/api_invite_links.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
@ -533,7 +538,7 @@ object_ptr<Ui::RpWidget> Controller::createTitleEdit() {
_wrap, _wrap,
object_ptr<Ui::InputField>( object_ptr<Ui::InputField>(
_wrap, _wrap,
st::defaultInputField, st::editPeerTitleField,
(_isBot (_isBot
? tr::lng_dlg_new_bot_name ? tr::lng_dlg_new_bot_name
: _isGroup : _isGroup
@ -555,6 +560,76 @@ object_ptr<Ui::RpWidget> Controller::createTitleEdit() {
submitTitle(); submitTitle();
}, result->entity()->lifetime()); }, result->entity()->lifetime());
{
const auto field = result->entity();
const auto container = _box->getDelegate()->outerContainer();
using Selector = ChatHelpers::TabbedSelector;
using PanelPtr = base::unique_qptr<ChatHelpers::TabbedPanel>;
const auto emojiPanelPtr = field->lifetime().make_state<PanelPtr>(
base::make_unique_q<ChatHelpers::TabbedPanel>(
container,
ChatHelpers::TabbedPanelDescriptor{
.ownedSelector = object_ptr<Selector>(
nullptr,
ChatHelpers::TabbedSelectorDescriptor{
.show = _navigation->uiShow(),
.st = st::defaultComposeControls.tabbed,
.level = Window::GifPauseReason::Layer,
.mode = Selector::Mode::PeerTitle,
}),
}));
const auto emojiPanel = emojiPanelPtr->get();
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(_peer);
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
field->setFocus();
}, field->lifetime());
emojiPanel->setDropDown(true);
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
field,
st::defaultComposeControls.files.emoji);
emojiToggle->show();
emojiToggle->installEventFilter(emojiPanel);
emojiToggle->addClickHandler([=] { emojiPanel->toggleAnimated(); });
const auto updateEmojiPanelGeometry = [=] {
const auto parent = emojiPanel->parentWidget();
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
emojiPanel->moveTopRight(
local.y() + emojiToggle->height(),
local.x() + emojiToggle->width() * 3);
};
base::install_event_filter(container, [=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
crl::on_main(field, [=] { updateEmojiPanelGeometry(); });
}
return base::EventFilterResult::Continue;
});
field->widthValue() | rpl::start_with_next([=](int width) {
const auto &p = st::editPeerTitleEmojiPosition;
emojiToggle->moveToRight(p.x(), p.y(), width);
updateEmojiPanelGeometry();
}, emojiToggle->lifetime());
base::install_event_filter(emojiToggle, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter) {
updateEmojiPanelGeometry();
}
return base::EventFilterResult::Continue;
});
}
_controls.title = result->entity(); _controls.title = result->entity();
return result; return result;
} }

View file

@ -7,30 +7,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/peers/edit_peer_reactions.h" #include "boxes/peers/edit_peer_reactions.h"
#include "apiwrap.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "data/data_chat.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_peer_values.h" // UniqueReactionsLimit.
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/reactions/history_view_reactions_selector.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/boost_box.h" #include "ui/boxes/boost_box.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h" #include "window/window_session_controller_link_info.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_settings.h"
#include <QtWidgets/QTextEdit> #include <QtWidgets/QTextEdit>
#include <QtGui/QTextBlock> #include <QtGui/QTextBlock>
@ -705,12 +708,16 @@ void EditAllowedReactionsBox(
} }
}; };
changed(selected.empty() ? DefaultSelected() : std::move(selected), {}); changed(selected.empty() ? DefaultSelected() : std::move(selected), {});
Ui::AddSubsectionTitle(
reactions,
enabled
? tr::lng_manage_peer_reactions_available()
: tr::lng_manage_peer_reactions_some_title(),
st::manageGroupReactionsFieldPadding);
reactions->add(AddReactionsSelector(reactions, { reactions->add(AddReactionsSelector(reactions, {
.outer = box->getDelegate()->outerContainer(), .outer = box->getDelegate()->outerContainer(),
.controller = args.navigation->parentController(), .controller = args.navigation->parentController(),
.title = (enabled .title = tr::lng_manage_peer_reactions_available_ph(),
? tr::lng_manage_peer_reactions_available()
: tr::lng_manage_peer_reactions_some_title()),
.list = all, .list = all,
.selected = state->selected, .selected = state->selected,
.callback = changed, .callback = changed,
@ -726,6 +733,7 @@ void EditAllowedReactionsBox(
} }
}); });
const auto reactionsLimit = container->lifetime().make_state<int>(0);
if (!isGroup) { if (!isGroup) {
AddReactionsText( AddReactionsText(
container, container,
@ -733,9 +741,109 @@ void EditAllowedReactionsBox(
args.allowedCustomReactions, args.allowedCustomReactions,
state->customCount.value(), state->customCount.value(),
args.askForBoosts); args.askForBoosts);
const auto session = &args.navigation->parentController()->session();
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto max = Data::UniqueReactionsLimit(session->user());
const auto inactiveColor = std::make_optional(st::windowSubTextFg->c);
const auto activeColor = std::make_optional(
st::windowActiveTextFg->c);
const auto inner = wrap->entity();
Ui::AddSkip(inner);
Ui::AddSubsectionTitle(
inner,
tr::lng_manage_peer_reactions_max_title(),
st::manageGroupReactionsMaxSubtitlePadding);
Ui::AddSkip(inner);
const auto line = inner->add(
object_ptr<Ui::RpWidget>(inner),
st::boxRowPadding);
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto left = Ui::CreateChild<Ui::FlatLabel>(
line,
QString::number(1),
st::defaultFlatLabel);
const auto center = Ui::CreateChild<Ui::FlatLabel>(
line,
st::defaultFlatLabel);
const auto right = Ui::CreateChild<Ui::FlatLabel>(
line,
QString::number(max),
st::defaultFlatLabel);
const auto slider = Ui::CreateChild<Ui::MediaSlider>(
line,
st::settingsScale);
rpl::combine(
line->sizeValue(),
left->sizeValue(),
center->sizeValue(),
right->sizeValue()
) | rpl::start_with_next([=](
const QSize &s,
const QSize &leftSize,
const QSize &centerSize,
const QSize &rightSize) {
const auto sliderHeight = st::settingsScale.seekSize.height();
line->resize(
line->width(),
leftSize.height() + sliderHeight * 2);
{
const auto r = line->rect();
slider->setGeometry(
0,
r.height() - sliderHeight * 1.5,
r.width(),
sliderHeight);
}
left->moveToLeft(0, 0);
right->moveToRight(0, 0);
center->moveToLeft((s.width() - centerSize.width()) / 2, 0);
}, line->lifetime());
const auto updateLabels = [=](int limit) {
left->setTextColorOverride((limit <= 1)
? activeColor
: inactiveColor);
center->setText(tr::lng_manage_peer_reactions_max_slider(
tr::now,
lt_count,
limit));
center->setTextColorOverride(activeColor);
right->setTextColorOverride((limit >= max)
? activeColor
: inactiveColor);
(*reactionsLimit) = limit;
};
const auto current = args.allowed.maxCount
? std::clamp(1, args.allowed.maxCount, max)
: max / 2;
slider->setPseudoDiscrete(
max,
[=](int index) { return index + 1; },
current,
updateLabels,
updateLabels);
updateLabels(current);
wrap->toggleOn(rpl::single(
optionInitial != Option::None
) | rpl::then(
state->selectorState.value(
) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());
} }
const auto collect = [=] { const auto collect = [=] {
auto result = AllowedReactions(); auto result = AllowedReactions();
result.maxCount = (*reactionsLimit);
if (isGroup if (isGroup
? (state->option.current() == Option::Some) ? (state->option.current() == Option::Some)
: (enabled->toggled())) { : (enabled->toggled())) {
@ -783,6 +891,7 @@ void SaveAllowedReactions(
Data::ReactionToMTP Data::ReactionToMTP
) | ranges::to<QVector<MTPReaction>>; ) | ranges::to<QVector<MTPReaction>>;
using Flag = MTPmessages_SetChatAvailableReactions::Flag;
using Type = Data::AllowedReactionsType; using Type = Data::AllowedReactionsType;
const auto updated = (allowed.type != Type::Some) const auto updated = (allowed.type != Type::Some)
? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default) ? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
@ -792,14 +901,18 @@ void SaveAllowedReactions(
? MTP_chatReactionsNone() ? MTP_chatReactionsNone()
: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids)); : MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
peer->session().api().request(MTPmessages_SetChatAvailableReactions( peer->session().api().request(MTPmessages_SetChatAvailableReactions(
allowed.maxCount ? MTP_flags(Flag::f_reactions_limit) : MTP_flags(0),
peer->input, peer->input,
updated updated,
MTP_int(allowed.maxCount)
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result); peer->session().api().applyUpdates(result);
auto parsed = Data::Parse(updated);
parsed.maxCount = allowed.maxCount;
if (const auto chat = peer->asChat()) { if (const auto chat = peer->asChat()) {
chat->setAllowedReactions(Data::Parse(updated)); chat->setAllowedReactions(parsed);
} else if (const auto channel = peer->asChannel()) { } else if (const auto channel = peer->asChannel()) {
channel->setAllowedReactions(Data::Parse(updated)); channel->setAllowedReactions(parsed);
} else { } else {
Unexpected("Invalid peer type in SaveAllowedReactions."); Unexpected("Invalid peer type in SaveAllowedReactions.");
} }

View file

@ -913,7 +913,7 @@ void PreviewBox(
auto businessOrder = Settings::BusinessFeaturesOrder(&show->session()); auto businessOrder = Settings::BusinessFeaturesOrder(&show->session());
state->order = ranges::contains(businessOrder, descriptor.section) state->order = ranges::contains(businessOrder, descriptor.section)
? std::move(businessOrder) ? std::move(businessOrder)
: ranges::contains(businessOrder, descriptor.section) : ranges::contains(premiumOrder, descriptor.section)
? std::move(premiumOrder) ? std::move(premiumOrder)
: std::vector{ descriptor.section }; : std::vector{ descriptor.section };

View file

@ -239,7 +239,10 @@ void ShareBox::prepareCommentField() {
const auto field = _comment->entity(); const auto field = _comment->entity();
field->submits( field->submits(
) | rpl::start_with_next([=] { submit({}); }, field->lifetime()); ) | rpl::start_with_next([=] {
submit({});
}, field->lifetime());
if (const auto show = uiShow(); show->valid()) { if (const auto show = uiShow(); show->valid()) {
InitMessageFieldHandlers( InitMessageFieldHandlers(
_descriptor.session, _descriptor.session,
@ -251,6 +254,14 @@ void ShareBox::prepareCommentField() {
} }
field->setSubmitSettings(Core::App().settings().sendSubmitWay()); field->setSubmitSettings(Core::App().settings().sendSubmitWay());
field->changes() | rpl::start_with_next([=] {
if (!field->getLastText().isEmpty()) {
setCloseByOutsideClick(false);
} else if (_inner->selected().empty()) {
setCloseByOutsideClick(true);
}
}, field->lifetime());
Ui::SendPendingMoveResizeEvents(_comment); Ui::SendPendingMoveResizeEvents(_comment);
if (_bottomWidget) { if (_bottomWidget) {
Ui::SendPendingMoveResizeEvents(_bottomWidget); Ui::SendPendingMoveResizeEvents(_bottomWidget);
@ -260,8 +271,6 @@ void ShareBox::prepareCommentField() {
void ShareBox::prepare() { void ShareBox::prepare() {
prepareCommentField(); prepareCommentField();
setCloseByOutsideClick(false);
_select->resizeToWidth(st::boxWideWidth); _select->resizeToWidth(st::boxWideWidth);
Ui::SendPendingMoveResizeEvents(_select); Ui::SendPendingMoveResizeEvents(_select);
@ -318,6 +327,12 @@ void ShareBox::prepare() {
not_null<Data::Thread*> thread, not_null<Data::Thread*> thread,
bool checked) { bool checked) {
innerSelectedChanged(thread, checked); innerSelectedChanged(thread, checked);
if (checked) {
setCloseByOutsideClick(false);
} else if (_inner->selected().empty()
&& _comment->entity()->getLastText().isEmpty()) {
setCloseByOutsideClick(true);
}
}); });
Ui::Emoji::SuggestionsController::Init( Ui::Emoji::SuggestionsController::Init(
@ -1690,6 +1705,101 @@ void FastShareMessage(
Ui::LayerOption::CloseOther); Ui::LayerOption::CloseOther);
} }
void FastShareLink(
not_null<Window::SessionController*> controller,
const QString &url) {
FastShareLink(controller->uiShow(), url);
}
void FastShareLink(
std::shared_ptr<Main::SessionShow> show,
const QString &url) {
const auto box = std::make_shared<QPointer<Ui::BoxContent>>();
const auto sending = std::make_shared<bool>();
auto copyCallback = [=] {
QGuiApplication::clipboard()->setText(url);
show->showToast(tr::lng_background_link_copied(tr::now));
};
auto submitCallback = [=](
std::vector<not_null<::Data::Thread*>> &&result,
TextWithTags &&comment,
Api::SendOptions options,
::Data::ForwardOptions) {
if (*sending || result.empty()) {
return;
}
const auto error = [&] {
for (const auto thread : result) {
const auto error = GetErrorTextForSending(
thread,
{ .text = &comment });
if (!error.isEmpty()) {
return std::make_pair(error, thread);
}
}
return std::make_pair(QString(), result.front());
}();
if (!error.first.isEmpty()) {
auto text = TextWithEntities();
if (result.size() > 1) {
text.append(
Ui::Text::Bold(error.second->chatListName())
).append("\n\n");
}
text.append(error.first);
if (const auto weak = *box) {
weak->getDelegate()->show(Ui::MakeConfirmBox({
.text = text,
.inform = true,
}));
}
return;
}
*sending = true;
if (!comment.text.isEmpty()) {
comment.text = url + "\n" + comment.text;
const auto add = url.size() + 1;
for (auto &tag : comment.tags) {
tag.offset += add;
}
} else {
comment.text = url;
}
auto &api = show->session().api();
for (const auto thread : result) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
message.textWithTags = comment;
message.action.clearDraft = false;
api.sendMessage(std::move(message));
}
if (*box) {
(*box)->closeBox();
}
show->showToast(tr::lng_share_done(tr::now));
};
auto filterCallback = [](not_null<::Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
return true;
}
}
return ::Data::CanSend(thread, ChatRestriction::SendOther);
};
*box = show->show(
Box<ShareBox>(ShareBox::Descriptor{
.session = &show->session(),
.copyCallback = std::move(copyCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.premiumRequiredError = SharePremiumRequiredError(),
}),
Ui::LayerOption::KeepOther,
anim::type::normal);
}
auto SharePremiumRequiredError() auto SharePremiumRequiredError()
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> { -> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> {
return WritePremiumRequiredError; return WritePremiumRequiredError;

View file

@ -37,6 +37,7 @@ struct SendOptions;
namespace Main { namespace Main {
class Session; class Session;
class SessionShow;
} // namespace Main } // namespace Main
namespace Dialogs { namespace Dialogs {
@ -68,6 +69,12 @@ void ShareGameScoreByHash(
void FastShareMessage( void FastShareMessage(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item); not_null<HistoryItem*> item);
void FastShareLink(
not_null<Window::SessionController*> controller,
const QString &url);
void FastShareLink(
std::shared_ptr<Main::SessionShow> show,
const QString &url);
struct RecipientPremiumRequiredError; struct RecipientPremiumRequiredError;
[[nodiscard]] auto SharePremiumRequiredError() [[nodiscard]] auto SharePremiumRequiredError()

View file

@ -1076,7 +1076,7 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
_menu.get(), _menu.get(),
type, type,
SendMenu::DefaultSilentCallback(sendSelected), SendMenu::DefaultSilentCallback(sendSelected),
SendMenu::DefaultScheduleCallback(this, type, sendSelected), SendMenu::DefaultScheduleCallback(_show, type, sendSelected),
SendMenu::DefaultWhenOnlineCallback(sendSelected)); SendMenu::DefaultWhenOnlineCallback(sendSelected));
const auto show = _show; const auto show = _show;

View file

@ -7,31 +7,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "calls/calls_call.h" #include "calls/calls_call.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "lang/lang_keys.h"
#include "boxes/abstract_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/rate_call_box.h"
#include "calls/calls_instance.h"
#include "base/battery_saving.h"
#include "base/openssl_help.h" #include "base/openssl_help.h"
#include "base/platform/base_platform_info.h"
#include "base/random.h" #include "base/random.h"
#include "mtproto/mtproto_dh_utils.h" #include "boxes/abstract_box.h"
#include "mtproto/mtproto_config.h" #include "calls/calls_instance.h"
#include "calls/calls_panel.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "window/window_controller.h" #include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "media/audio/media_audio_track.h" #include "media/audio/media_audio_track.h"
#include "base/platform/base_platform_info.h" #include "mtproto/mtproto_config.h"
#include "calls/calls_panel.h" #include "mtproto/mtproto_dh_utils.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/rate_call_box.h"
#include "webrtc/webrtc_create_adm.h"
#include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_environment.h"
#include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_video_track.h"
#include "webrtc/webrtc_create_adm.h" #include "window/window_controller.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include <tgcalls/Instance.h> #include <tgcalls/Instance.h>
#include <tgcalls/VideoCaptureInterface.h> #include <tgcalls/VideoCaptureInterface.h>
@ -1072,6 +1070,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
Core::App().mediaDevices().setCaptureMuted(muted); Core::App().mediaDevices().setCaptureMuted(muted);
}, _instanceLifetime); }, _instanceLifetime);
#if 0
Core::App().batterySaving().value( Core::App().batterySaving().value(
) | rpl::start_with_next([=](bool isSaving) { ) | rpl::start_with_next([=](bool isSaving) {
crl::on_main(weak, [=] { crl::on_main(weak, [=] {
@ -1080,6 +1079,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
} }
}); });
}, _instanceLifetime); }, _instanceLifetime);
#endif
} }
void Call::handleControllerStateChange(tgcalls::State state) { void Call::handleControllerStateChange(tgcalls::State state) {

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h" #include "main/main_session.h"
#include "boxes/abstract_box.h" #include "boxes/abstract_box.h"
#include "base/timer.h" #include "base/timer.h"
#include "styles/style_basic.h"
#include "styles/style_calls.h" #include "styles/style_calls.h"
#include "styles/style_chat_helpers.h" // style::GroupCallUserpics #include "styles/style_chat_helpers.h" // style::GroupCallUserpics
#include "styles/style_layers.h" #include "styles/style_layers.h"
@ -49,7 +50,6 @@ enum class BarState {
namespace { namespace {
constexpr auto kUpdateDebugTimeoutMs = crl::time(500); constexpr auto kUpdateDebugTimeoutMs = crl::time(500);
constexpr auto kSwitchStateDuration = 120;
constexpr auto kMinorBlobAlpha = 76. / 255.; constexpr auto kMinorBlobAlpha = 76. / 255.;
@ -374,7 +374,7 @@ void TopBar::initControls() {
}; };
_switchStateAnimation.stop(); _switchStateAnimation.stop();
const auto duration = (to - from) * kSwitchStateDuration; const auto duration = (to - from) * st::universalDuration;
_switchStateAnimation.start( _switchStateAnimation.start(
_switchStateCallback, _switchStateCallback,
from, from,

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "styles/style_basic.h"
#include "styles/style_calls.h" #include "styles/style_calls.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
@ -26,7 +27,6 @@ namespace {
constexpr auto kRoundRadius = 9; constexpr auto kRoundRadius = 9;
constexpr auto kMaxGroupCallLength = 40; constexpr auto kMaxGroupCallLength = 40;
constexpr auto kSwitchDuration = 200; constexpr auto kSwitchDuration = 200;
constexpr auto kSelectDuration = 120;
class GraphicButton final : public Ui::AbstractButton { class GraphicButton final : public Ui::AbstractButton {
public: public:
@ -103,7 +103,7 @@ void GraphicButton::setToggled(bool value) {
[=] { update(); }, [=] { update(); },
_toggled ? 0. : 1., _toggled ? 0. : 1.,
_toggled ? 1. : 0., _toggled ? 1. : 0.,
kSelectDuration); st::universalDuration);
} }
void GraphicButton::paintEvent(QPaintEvent *e) { void GraphicButton::paintEvent(QPaintEvent *e) {

View file

@ -1108,8 +1108,6 @@ historyRecordVoiceFgOver: historyComposeIconFgOver;
historyRecordVoiceFgInactive: attentionButtonFg; historyRecordVoiceFgInactive: attentionButtonFg;
historyRecordVoiceFgActive: windowBgActive; historyRecordVoiceFgActive: windowBgActive;
historyRecordVoiceFgActiveIcon: windowFgActive; historyRecordVoiceFgActiveIcon: windowFgActive;
historyRecordVoiceShowDuration: 120;
historyRecordVoiceDuration: 120;
historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }};
historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }};
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }}; historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};

View file

@ -468,7 +468,8 @@ EmojiListWidget::EmojiListWidget(
std::move(descriptor.paused)) std::move(descriptor.paused))
, _show(std::move(descriptor.show)) , _show(std::move(descriptor.show))
, _features(descriptor.features) , _features(descriptor.features)
, _mode(descriptor.mode) , _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle)
, _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode)
, _api(&session().mtp()) , _api(&session().mtp())
, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1) , _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)
, _premiumIcon(_mode == Mode::EmojiStatus , _premiumIcon(_mode == Mode::EmojiStatus
@ -490,7 +491,8 @@ EmojiListWidget::EmojiListWidget(
if (_mode != Mode::RecentReactions if (_mode != Mode::RecentReactions
&& _mode != Mode::BackgroundEmoji && _mode != Mode::BackgroundEmoji
&& _mode != Mode::ChannelStatus) { && _mode != Mode::ChannelStatus
&& !_onlyUnicodeEmoji) {
setupSearch(); setupSearch();
} }
@ -571,12 +573,17 @@ EmojiListWidget::~EmojiListWidget() {
void EmojiListWidget::setupSearch() { void EmojiListWidget::setupSearch() {
const auto session = &_show->session(); const auto session = &_show->session();
const auto type = (_mode == Mode::EmojiStatus)
? TabbedSearchType::Status
: (_mode == Mode::UserpicBuilder)
? TabbedSearchType::ProfilePhoto
: TabbedSearchType::Emoji;
_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) { _search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {
_nextSearchQuery = std::move(query); _nextSearchQuery = std::move(query);
InvokeQueued(this, [=] { InvokeQueued(this, [=] {
applyNextSearchQuery(); applyNextSearchQuery();
}); });
}, session, (_mode == Mode::EmojiStatus), _mode == Mode::UserpicBuilder); }, session, type);
} }
void EmojiListWidget::applyNextSearchQuery() { void EmojiListWidget::applyNextSearchQuery() {
@ -1052,7 +1059,7 @@ void EmojiListWidget::fillRecent() {
const auto test = session().isTestMode(); const auto test = session().isTestMode();
for (const auto &one : list) { for (const auto &one : list) {
const auto document = std::get_if<RecentEmojiDocument>(&one.id.data); const auto document = std::get_if<RecentEmojiDocument>(&one.id.data);
if (document && document->test != test) { if (document && ((document->test != test) || _onlyUnicodeEmoji)) {
continue; continue;
} }
_recent.push_back({ _recent.push_back({
@ -2129,7 +2136,9 @@ void EmojiListWidget::refreshCustom() {
auto old = base::take(_custom); auto old = base::take(_custom);
const auto session = &this->session(); const auto session = &this->session();
const auto premiumPossible = session->premiumPossible(); const auto premiumPossible = session->premiumPossible();
const auto premiumMayBeBought = premiumPossible const auto onlyUnicodeEmoji = _onlyUnicodeEmoji || !premiumPossible;
const auto premiumMayBeBought = (!onlyUnicodeEmoji)
&& premiumPossible
&& !session->premium() && !session->premium()
&& !_allowWithoutPremium; && !_allowWithoutPremium;
const auto owner = &session->data(); const auto owner = &session->data();
@ -2189,7 +2198,7 @@ void EmojiListWidget::refreshCustom() {
} }
return true; return true;
}(); }();
if (premium && !premiumPossible) { if (premium && onlyUnicodeEmoji) {
return; return;
} else if (valid) { } else if (valid) {
i->thumbnailDocument = it->second->lookupThumbnailDocument(); i->thumbnailDocument = it->second->lookupThumbnailDocument();
@ -2223,7 +2232,7 @@ void EmojiListWidget::refreshCustom() {
} }
} }
} }
if (premium && !premiumPossible) { if (premium && onlyUnicodeEmoji) {
return; return;
} }
_custom.push_back({ _custom.push_back({

View file

@ -76,6 +76,7 @@ enum class EmojiListMode {
RecentReactions, RecentReactions,
UserpicBuilder, UserpicBuilder,
BackgroundEmoji, BackgroundEmoji,
PeerTitle,
}; };
struct EmojiListDescriptor { struct EmojiListDescriptor {
@ -379,6 +380,7 @@ private:
const std::shared_ptr<Show> _show; const std::shared_ptr<Show> _show;
const ComposeFeatures _features; const ComposeFeatures _features;
const bool _onlyUnicodeEmoji;
Mode _mode = Mode::Full; Mode _mode = Mode::Full;
std::unique_ptr<Ui::TabbedSearch> _search; std::unique_ptr<Ui::TabbedSearch> _search;
MTP::Sender _api; MTP::Sender _api;

View file

@ -38,7 +38,6 @@ namespace {
constexpr auto kShowExactDelay = crl::time(300); constexpr auto kShowExactDelay = crl::time(300);
constexpr auto kMaxNonScrolledEmoji = 7; constexpr auto kMaxNonScrolledEmoji = 7;
constexpr auto kAnimationDuration = crl::time(120);
} // namespace } // namespace
@ -528,7 +527,7 @@ void SuggestionsWidget::setSelected(int selected, anim::type animated) {
[=] { update(); }, [=] { update(); },
_selected, _selected,
selected, selected,
kAnimationDuration, st::universalDuration,
anim::sineInOut); anim::sineInOut);
if (_scrollMax > 0) { if (_scrollMax > 0) {
const auto selectedMax = int(_rows.size()) - 3; const auto selectedMax = int(_rows.size()) - 3;
@ -560,7 +559,7 @@ void SuggestionsWidget::scrollTo(int value, anim::type animated) {
[=] { update(); }, [=] { update(); },
_scrollValue, _scrollValue,
value, value,
kAnimationDuration, st::universalDuration,
anim::sineInOut); anim::sineInOut);
} }
_scrollValue = value; _scrollValue = value;

View file

@ -1379,7 +1379,7 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
_menu, _menu,
type, type,
SendMenu::DefaultSilentCallback(send), SendMenu::DefaultSilentCallback(send),
SendMenu::DefaultScheduleCallback(this, type, send), SendMenu::DefaultScheduleCallback(_show, type, send),
SendMenu::DefaultWhenOnlineCallback(send)); SendMenu::DefaultWhenOnlineCallback(send));
if (!_menu->empty()) { if (!_menu->empty()) {

View file

@ -401,7 +401,7 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
menu, menu,
type, type,
SendMenu::DefaultSilentCallback(send), SendMenu::DefaultSilentCallback(send),
SendMenu::DefaultScheduleCallback(this, type, send), SendMenu::DefaultScheduleCallback(_show, type, send),
SendMenu::DefaultWhenOnlineCallback(send), SendMenu::DefaultWhenOnlineCallback(send),
icons); icons);
@ -849,7 +849,7 @@ void GifsListWidget::setupSearch() {
: SearchEmojiSectionSetId(); : SearchEmojiSectionSetId();
refreshIcons(); refreshIcons();
searchForGifs(accumulated); searchForGifs(accumulated);
}, session); }, session, TabbedSearchType::Emoji);
} }
int32 GifsListWidget::showInlineRows(bool newResults) { int32 GifsListWidget::showInlineRows(bool newResults) {

View file

@ -553,6 +553,15 @@ void StickersListWidget::sendSearchRequest() {
} }
_search->setLoading(true); _search->setLoading(true);
if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) {
_search->setLoading(false);
_searchRequestId = 0;
_searchCache.emplace(_searchQuery, std::vector<uint64>());
showSearchResults();
return;
}
const auto hash = uint64(0); const auto hash = uint64(0);
_searchRequestId = _api.request(MTPmessages_SearchStickerSets( _searchRequestId = _api.request(MTPmessages_SearchStickerSets(
MTP_flags(0), MTP_flags(0),
@ -576,10 +585,14 @@ void StickersListWidget::searchForSets(
return; return;
} }
_filteredStickers = session().data().stickers().getListByEmoji( if (query == Ui::PremiumGroupFakeEmoticon()) {
std::move(emoji), _filteredStickers = session().data().stickers().getPremiumList(0);
0, } else {
true); _filteredStickers = session().data().stickers().getListByEmoji(
std::move(emoji),
0,
true);
}
if (_searchQuery != cleaned) { if (_searchQuery != cleaned) {
_search->setLoading(false); _search->setLoading(false);
if (const auto requestId = base::take(_searchRequestId)) { if (const auto requestId = base::take(_searchRequestId)) {
@ -1660,7 +1673,7 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
menu, menu,
type, type,
SendMenu::DefaultSilentCallback(send), SendMenu::DefaultSilentCallback(send),
SendMenu::DefaultScheduleCallback(this, type, send), SendMenu::DefaultScheduleCallback(_show, type, send),
SendMenu::DefaultWhenOnlineCallback(send), SendMenu::DefaultWhenOnlineCallback(send),
icons); icons);
@ -2636,15 +2649,20 @@ void StickersListWidget::beforeHiding() {
void StickersListWidget::setupSearch() { void StickersListWidget::setupSearch() {
const auto session = &_show->session(); const auto session = &_show->session();
const auto type = (_mode == Mode::UserpicBuilder)
? TabbedSearchType::ProfilePhoto
: (_mode == Mode::ChatIntro)
? TabbedSearchType::Greeting
: TabbedSearchType::Stickers;
_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) { _search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {
auto set = base::flat_set<EmojiPtr>(); auto set = base::flat_set<EmojiPtr>();
auto text = ranges::accumulate(query, QString(), []( auto text = ranges::accumulate(query, QString(), [](
QString a, QString a,
QString b) { QString b) {
return a.isEmpty() ? b : (a + ' ' + b); return a.isEmpty() ? b : (a + ' ' + b);
}); });
searchForSets(std::move(text), SearchEmoji(query, set)); searchForSets(std::move(text), SearchEmoji(query, set));
}, session, false, (_mode == Mode::UserpicBuilder)); }, session, type);
} }
void StickersListWidget::displaySet(uint64 setId) { void StickersListWidget::displaySet(uint64 setId) {

View file

@ -65,6 +65,7 @@ enum class StickersListMode {
Full, Full,
Masks, Masks,
UserpicBuilder, UserpicBuilder,
ChatIntro,
}; };
struct StickersListDescriptor { struct StickersListDescriptor {

View file

@ -302,21 +302,39 @@ void TabbedSelector::Tab::saveScrollTop() {
_scrollTop = widget()->getVisibleTop(); _scrollTop = widget()->getVisibleTop();
} }
[[nodiscard]] rpl::producer<std::vector<Ui::EmojiGroup>> GreetingGroupFirst(
not_null<Data::Session*> owner) {
return owner->emojiStatuses().stickerGroupsValue(
) | rpl::map([](std::vector<Ui::EmojiGroup> &&groups) {
const auto i = ranges::find(
groups,
Ui::EmojiGroupType::Greeting,
&Ui::EmojiGroup::type);
if (i != begin(groups) && i != end(groups)) {
ranges::rotate(begin(groups), i, i + 1);
}
return std::move(groups);
});
}
std::unique_ptr<Ui::TabbedSearch> MakeSearch( std::unique_ptr<Ui::TabbedSearch> MakeSearch(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::EmojiPan &st, const style::EmojiPan &st,
Fn<void(std::vector<QString>&&)> callback, Fn<void(std::vector<QString>&&)> callback,
not_null<Main::Session*> session, not_null<Main::Session*> session,
bool statusCategories, TabbedSearchType type) {
bool profilePhotoCategories) {
using Descriptor = Ui::SearchDescriptor; using Descriptor = Ui::SearchDescriptor;
const auto owner = &session->data(); const auto owner = &session->data();
auto result = std::make_unique<Ui::TabbedSearch>(parent, st, Descriptor{ auto result = std::make_unique<Ui::TabbedSearch>(parent, st, Descriptor{
.st = st.search, .st = st.search,
.groups = (profilePhotoCategories .groups = ((type == TabbedSearchType::ProfilePhoto)
? owner->emojiStatuses().profilePhotoGroupsValue() ? owner->emojiStatuses().profilePhotoGroupsValue()
: statusCategories : (type == TabbedSearchType::Status)
? owner->emojiStatuses().statusGroupsValue() ? owner->emojiStatuses().statusGroupsValue()
: (type == TabbedSearchType::Stickers)
? owner->emojiStatuses().stickerGroupsValue()
: (type == TabbedSearchType::Greeting)
? GreetingGroupFirst(owner)
: owner->emojiStatuses().emojiGroupsValue()), : owner->emojiStatuses().emojiGroupsValue()),
.customEmojiFactory = owner->customEmojiManager().factory( .customEmojiFactory = owner->customEmojiManager().factory(
Data::CustomEmojiManager::SizeTag::SetIcon, Data::CustomEmojiManager::SizeTag::SetIcon,
@ -378,7 +396,7 @@ TabbedSelector::TabbedSelector(
tabs.reserve(2); tabs.reserve(2);
tabs.push_back(createTab(SelectorTab::Stickers, 0)); tabs.push_back(createTab(SelectorTab::Stickers, 0));
tabs.push_back(createTab(SelectorTab::Masks, 1)); tabs.push_back(createTab(SelectorTab::Masks, 1));
} else if (_mode == Mode::StickersOnly) { } else if (_mode == Mode::StickersOnly || _mode == Mode::ChatIntro) {
tabs.reserve(1); tabs.reserve(1);
tabs.push_back(createTab(SelectorTab::Stickers, 0)); tabs.push_back(createTab(SelectorTab::Stickers, 0));
} else { } else {
@ -389,7 +407,9 @@ TabbedSelector::TabbedSelector(
}()) }())
, _currentTabType(full() , _currentTabType(full()
? session().settings().selectorTab() ? session().settings().selectorTab()
: (mediaEditor() || _mode == Mode::StickersOnly) : (mediaEditor()
|| _mode == Mode::StickersOnly
|| _mode == Mode::ChatIntro)
? SelectorTab::Stickers ? SelectorTab::Stickers
: SelectorTab::Emoji) : SelectorTab::Emoji)
, _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type)) , _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type))
@ -540,6 +560,8 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
? EmojiMode::FullReactions ? EmojiMode::FullReactions
: _mode == Mode::RecentReactions : _mode == Mode::RecentReactions
? EmojiMode::RecentReactions ? EmojiMode::RecentReactions
: _mode == Mode::PeerTitle
? EmojiMode::PeerTitle
: EmojiMode::Full), : EmojiMode::Full),
.customTextColor = _customTextColor, .customTextColor = _customTextColor,
.paused = paused, .paused = paused,
@ -552,7 +574,9 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
using Descriptor = StickersListDescriptor; using Descriptor = StickersListDescriptor;
return object_ptr<StickersListWidget>(this, Descriptor{ return object_ptr<StickersListWidget>(this, Descriptor{
.show = _show, .show = _show,
.mode = StickersMode::Full, .mode = (_mode == Mode::ChatIntro
? StickersMode::ChatIntro
: StickersMode::Full),
.paused = paused, .paused = paused,
.st = &_st, .st = &_st,
.features = _features, .features = _features,
@ -958,6 +982,9 @@ void TabbedSelector::beforeHiding() {
_beforeHidingCallback(_currentTabType); _beforeHidingCallback(_currentTabType);
} }
} }
if (Ui::InFocusChain(this)) {
window()->setFocus();
}
} }
void TabbedSelector::afterShown() { void TabbedSelector::afterShown() {

View file

@ -86,6 +86,8 @@ enum class TabbedSelectorMode {
BackgroundEmoji, BackgroundEmoji,
FullReactions, FullReactions,
RecentReactions, RecentReactions,
PeerTitle,
ChatIntro,
}; };
struct TabbedSelectorDescriptor { struct TabbedSelectorDescriptor {
@ -97,13 +99,19 @@ struct TabbedSelectorDescriptor {
ComposeFeatures features; ComposeFeatures features;
}; };
enum class TabbedSearchType {
Emoji,
Status,
ProfilePhoto,
Stickers,
Greeting,
};
[[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch( [[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::EmojiPan &st, const style::EmojiPan &st,
Fn<void(std::vector<QString>&&)> callback, Fn<void(std::vector<QString>&&)> callback,
not_null<Main::Session*> session, not_null<Main::Session*> session,
bool statusCategories = false, TabbedSearchType type);
bool profilePhotoCategories = false);
class TabbedSelector : public Ui::RpWidget { class TabbedSelector : public Ui::RpWidget {
public: public:

View file

@ -230,7 +230,7 @@ PreviewWrap::PreviewWrap(
lt_user, lt_user,
rpl::single( rpl::single(
item->history()->peer->shortName() item->history()->peer->shortName()
) | rpl::map(Ui::Text::RichLangValue), ) | Ui::Text::ToRichLangValue(),
Ui::Text::RichLangValue) Ui::Text::RichLangValue)
: (isRound : (isRound
? settings->saveDeletedMessages ? tr::ayu_ExpiringVideoMessageNote : tr::lng_ttl_round_tooltip_in ? settings->saveDeletedMessages ? tr::ayu_ExpiringVideoMessageNote : tr::lng_ttl_round_tooltip_in

View file

@ -241,23 +241,21 @@ Application::~Application() {
_mediaControlsManager = nullptr; _mediaControlsManager = nullptr;
Media::Player::finish(_audio.get()); Media::Player::finish(_audio.get());
style::stopManager(); style::StopManager();
ThirdParty::finish();
Instance = nullptr; Instance = nullptr;
} }
void Application::run() { void Application::run() {
style::internal::StartFonts();
ThirdParty::start();
// Depends on OpenSSL on macOS, so on ThirdParty::start(). // Depends on OpenSSL on macOS, so on ThirdParty::start().
// Depends on notifications settings. // Depends on notifications settings.
_notifications = std::make_unique<Window::Notifications::System>(); _notifications = std::make_unique<Window::Notifications::System>();
startLocalStorage(); startLocalStorage();
style::SetCustomFont(settings().customFontFamily());
style::internal::StartFonts();
ValidateScale(); ValidateScale();
refreshGlobalProxy(); // Depends on app settings being read. refreshGlobalProxy(); // Depends on app settings being read.
@ -280,7 +278,7 @@ void Application::run() {
_translator = std::make_unique<Lang::Translator>(); _translator = std::make_unique<Lang::Translator>();
QCoreApplication::instance()->installTranslator(_translator.get()); QCoreApplication::instance()->installTranslator(_translator.get());
style::startManager(cScale()); style::StartManager(cScale());
Ui::InitTextOptions(); Ui::InitTextOptions();
Ui::StartCachedCorners(); Ui::StartCachedCorners();
Ui::Emoji::Init(); Ui::Emoji::Init();
@ -1525,14 +1523,14 @@ void Application::closeChatFromWindows(not_null<PeerData*> peer) {
} }
} }
if (const auto window = windowFor(&peer->account())) { if (const auto window = windowFor(&peer->account())) {
const auto primary = window->sessionController(); if (const auto primary = window->sessionController()) {
if ((primary->activeChatCurrent().peer() == peer) if (primary->activeChatCurrent().peer() == peer) {
&& (&primary->session() == &peer->session())) { primary->clearSectionStack();
primary->clearSectionStack(); }
} if (const auto forum = primary->shownForum().current()) {
if (const auto forum = primary->shownForum().current()) { if (peer->forum() == forum) {
if (peer->forum() == forum) { primary->closeForum();
primary->closeForum(); }
} }
} }
} }

View file

@ -221,7 +221,8 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(_callPlaybackDeviceId.current()) + Serialize::stringSize(_callPlaybackDeviceId.current())
+ Serialize::stringSize(_callCaptureDeviceId.current()) + Serialize::stringSize(_callCaptureDeviceId.current())
+ Serialize::bytearraySize(ivPosition) + Serialize::bytearraySize(ivPosition)
+ Serialize::stringSize(noWarningExtensions); + Serialize::stringSize(noWarningExtensions)
+ Serialize::stringSize(_customFontFamily);
auto result = QByteArray(); auto result = QByteArray();
result.reserve(size); result.reserve(size);
@ -367,7 +368,8 @@ QByteArray Settings::serialize() const {
<< _callPlaybackDeviceId.current() << _callPlaybackDeviceId.current()
<< _callCaptureDeviceId.current() << _callCaptureDeviceId.current()
<< ivPosition << ivPosition
<< noWarningExtensions; << noWarningExtensions
<< _customFontFamily;
} }
Ensures(result.size() == size); Ensures(result.size() == size);
@ -487,6 +489,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0); qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);
qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0; qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;
QByteArray ivPosition; QByteArray ivPosition;
QString customFontFamily = _customFontFamily;
stream >> themesAccentColors; stream >> themesAccentColors;
if (!stream.atEnd()) { if (!stream.atEnd()) {
@ -772,6 +775,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
noWarningExtensions = QString(); noWarningExtensions = QString();
stream >> *noWarningExtensions; stream >> *noWarningExtensions;
} }
if (!stream.atEnd()) {
stream >> customFontFamily;
}
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
LOG(("App Error: " LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()")); "Bad data for Core::Settings::constructFromSerialized()"));
@ -979,6 +985,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!ivPosition.isEmpty()) { if (!ivPosition.isEmpty()) {
_ivPosition = Deserialize(ivPosition); _ivPosition = Deserialize(ivPosition);
} }
_customFontFamily = customFontFamily;
} }
QString Settings::getSoundPath(const QString &key) const { QString Settings::getSoundPath(const QString &key) const {

View file

@ -877,6 +877,13 @@ public:
_ivPosition = position; _ivPosition = position;
} }
[[nodiscard]] QString customFontFamily() const {
return _customFontFamily;
}
void setCustomFontFamily(const QString &value) {
_customFontFamily = value;
}
[[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio(); [[nodiscard]] static float64 DefaultDialogsWidthRatio();
@ -1005,6 +1012,7 @@ private:
rpl::variable<bool> _storiesClickTooltipHidden = false; rpl::variable<bool> _storiesClickTooltipHidden = false;
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false; rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
WindowPosition _ivPosition; WindowPosition _ivPosition;
QString _customFontFamily;
bool _tabbedReplacedWithInfo = false; // per-window bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View file

@ -95,9 +95,6 @@ SettingsProxy::SettingsProxy()
} }
QByteArray SettingsProxy::serialize() const { QByteArray SettingsProxy::serialize() const {
auto result = QByteArray();
auto stream = QDataStream(&result, QIODevice::WriteOnly);
const auto serializedSelected = SerializeProxyData(_selected); const auto serializedSelected = SerializeProxyData(_selected);
const auto serializedList = ranges::views::all( const auto serializedList = ranges::views::all(
_list _list
@ -111,9 +108,7 @@ QByteArray SettingsProxy::serialize() const {
0, 0,
ranges::plus(), ranges::plus(),
&Serialize::bytearraySize); &Serialize::bytearraySize);
result.reserve(size); auto stream = Serialize::ByteArrayWriter(size);
stream.setVersion(QDataStream::Qt_5_1);
stream stream
<< qint32(_tryIPv6 ? 1 : 0) << qint32(_tryIPv6 ? 1 : 0)
<< qint32(_useProxyForCalls ? 1 : 0) << qint32(_useProxyForCalls ? 1 : 0)
@ -123,9 +118,7 @@ QByteArray SettingsProxy::serialize() const {
for (const auto &i : serializedList) { for (const auto &i : serializedList) {
stream << i; stream << i;
} }
return std::move(stream).result();
stream.device()->close();
return result;
} }
bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
@ -133,7 +126,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
return true; return true;
} }
auto stream = QDataStream(serialized); auto stream = Serialize::ByteArrayReader(serialized);
auto tryIPv6 = qint32(_tryIPv6 ? 1 : 0); auto tryIPv6 = qint32(_tryIPv6 ? 1 : 0);
auto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0); auto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0);
@ -148,7 +141,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
>> settings >> settings
>> selectedProxy >> selectedProxy
>> listCount; >> listCount;
if (stream.status() == QDataStream::Ok) { if (stream.ok()) {
for (auto i = 0; i != listCount; ++i) { for (auto i = 0; i != listCount; ++i) {
QByteArray data; QByteArray data;
stream >> data; stream >> data;
@ -157,7 +150,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
} }
} }
if (stream.status() != QDataStream::Ok) { if (!stream.ok()) {
LOG(("App Error: " LOG(("App Error: "
"Bad data for Core::SettingsProxy::setFromSerialized()")); "Bad data for Core::SettingsProxy::setFromSerialized()"));
return false; return false;

View file

@ -32,8 +32,6 @@ constexpr auto kDefaultProxyPort = 80;
PreLaunchWindow *PreLaunchWindowInstance = nullptr; PreLaunchWindow *PreLaunchWindowInstance = nullptr;
PreLaunchWindow::PreLaunchWindow(QString title) { PreLaunchWindow::PreLaunchWindow(QString title) {
style::internal::StartFonts();
setWindowIcon(Window::CreateIcon()); setWindowIcon(Window::CreateIcon());
setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
@ -99,7 +97,7 @@ PreLaunchWindow::~PreLaunchWindow() {
PreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) { PreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) {
QFont labelFont(font()); QFont labelFont(font());
labelFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold)); labelFont.setWeight(QFont::DemiBold);
labelFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize()); labelFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
setFont(labelFont); setFont(labelFont);
@ -118,7 +116,6 @@ void PreLaunchLabel::setText(const QString &text) {
PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) { PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {
QFont logFont(font()); QFont logFont(font());
logFont.setFamily(style::internal::GetFontOverride());
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize()); logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
setFont(logFont); setFont(logFont);
@ -139,7 +136,6 @@ PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(paren
PreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) { PreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) {
QFont logFont(font()); QFont logFont(font());
logFont.setFamily(style::internal::GetFontOverride());
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize()); logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
setFont(logFont); setFont(logFont);
@ -162,7 +158,7 @@ PreLaunchButton::PreLaunchButton(QWidget *parent, bool confirm) : QPushButton(pa
setObjectName(confirm ? "confirm" : "cancel"); setObjectName(confirm ? "confirm" : "cancel");
QFont closeFont(font()); QFont closeFont(font());
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold)); closeFont.setWeight(QFont::DemiBold);
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize()); closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
setFont(closeFont); setFont(closeFont);
@ -181,7 +177,7 @@ PreLaunchCheckbox::PreLaunchCheckbox(QWidget *parent) : QCheckBox(parent) {
setCheckState(Qt::Checked); setCheckState(Qt::Checked);
QFont closeFont(font()); QFont closeFont(font());
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold)); closeFont.setWeight(QFont::DemiBold);
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize()); closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
setFont(closeFont); setFont(closeFont);

View file

@ -385,6 +385,7 @@ int Launcher::exec() {
// Must be started before Sandbox is created. // Must be started before Sandbox is created.
Platform::start(); Platform::start();
ThirdParty::start();
auto result = executeApplication(); auto result = executeApplication();
DEBUG_LOG(("Telegram finished, result: %1").arg(result)); DEBUG_LOG(("Telegram finished, result: %1").arg(result));
@ -400,6 +401,7 @@ int Launcher::exec() {
} }
CrashReports::Finish(); CrashReports::Finish();
ThirdParty::finish();
Platform::finish(); Platform::finish();
Logs::finish(); Logs::finish();

View file

@ -244,8 +244,8 @@ QString FileExtension(const QString &filepath) {
NameType DetectNameType(const QString &filepath) { NameType DetectNameType(const QString &filepath) {
static const auto kImage = SplitExtensions(u"\ static const auto kImage = SplitExtensions(u"\
afdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large nef png \ afdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large jxl nef \
png-large psd raw sketch svg tga tif tiff webp"_q); png png-large psd qoi raw sketch svg tga tif tiff webp"_q);
static const auto kVideo = SplitExtensions(u"\ static const auto kVideo = SplitExtensions(u"\
3g2 3gp 3gpp aep avi flv h264 m4s m4v mkv mov mp4 mpeg mpg ogv srt tgs tgv \ 3g2 3gp 3gpp aep avi flv h264 m4s m4v mkv mov mp4 mpeg mpg ogv srt tgs tgv \
vob webm wmv"_q); vob webm wmv"_q);

View file

@ -92,8 +92,6 @@ namespace ThirdParty {
FIPS_mode_set(0); FIPS_mode_set(0);
#endif #endif
CONF_modules_unload(1); CONF_modules_unload(1);
Platform::ThirdParty::finish();
} }
} }

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4016008; constexpr auto AppVersion = 5000001;
constexpr auto AppVersionStr = "4.16.8"; constexpr auto AppVersionStr = "5.0.1";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -0,0 +1,122 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/recent_peers.h"
#include "main/main_session.h"
#include "storage/serialize_common.h"
#include "storage/serialize_peer.h"
#include "storage/storage_account.h"
namespace Data {
namespace {
constexpr auto kLimit = 48;
} // namespace
RecentPeers::RecentPeers(not_null<Main::Session*> session)
: _session(session) {
}
RecentPeers::~RecentPeers() = default;
const std::vector<not_null<PeerData*>> &RecentPeers::list() const {
_session->local().readSearchSuggestions();
return _list;
}
rpl::producer<> RecentPeers::updates() const {
return _updates.events();
}
void RecentPeers::remove(not_null<PeerData*> peer) {
const auto i = ranges::find(_list, peer);
if (i != end(_list)) {
_list.erase(i);
_updates.fire({});
}
_session->local().writeSearchSuggestionsDelayed();
}
void RecentPeers::bump(not_null<PeerData*> peer) {
_session->local().readSearchSuggestions();
if (!_list.empty() && _list.front() == peer) {
return;
}
auto i = ranges::find(_list, peer);
if (i == end(_list)) {
_list.push_back(peer);
i = end(_list) - 1;
}
ranges::rotate(begin(_list), i, i + 1);
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
void RecentPeers::clear() {
_session->local().readSearchSuggestions();
_list.clear();
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
QByteArray RecentPeers::serialize() const {
_session->local().readSearchSuggestions();
if (_list.empty()) {
return {};
}
auto size = 2 * sizeof(quint32); // AppVersion, count
const auto count = std::min(int(_list.size()), kLimit);
auto &&list = _list | ranges::views::take(count);
for (const auto &peer : list) {
size += Serialize::peerSize(peer);
}
auto stream = Serialize::ByteArrayWriter(size);
stream
<< quint32(AppVersion)
<< quint32(_list.size());
for (const auto &peer : list) {
Serialize::writePeer(stream, peer);
}
return std::move(stream).result();
}
void RecentPeers::applyLocal(QByteArray serialized) {
_list.clear();
if (serialized.isEmpty()) {
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
auto streamAppVersion = quint32();
auto count = quint32();
stream >> streamAppVersion >> count;
if (!stream.ok()) {
return;
}
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
stream);
if (stream.ok() && peer) {
_list.push_back(peer);
} else {
_list.clear();
return;
}
}
}
} // namespace Data

View file

@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Data {
class RecentPeers final {
public:
explicit RecentPeers(not_null<Main::Session*> session);
~RecentPeers();
[[nodiscard]] const std::vector<not_null<PeerData*>> &list() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void bump(not_null<PeerData*> peer);
void clear();
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
private:
const not_null<Main::Session*> _session;
std::vector<not_null<PeerData*>> _list;
rpl::event_stream<> _updates;
};
} // namespace Data

View file

@ -9,8 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h" #include "api/api_text_entities.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "data/data_bot_app.h" #include "core/click_handler_types.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_photo.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/history.h" #include "history/history.h"
@ -40,9 +41,20 @@ SponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)
} }
SponsoredMessages::~SponsoredMessages() { SponsoredMessages::~SponsoredMessages() {
for (const auto &request : _requests) { Expects(_data.empty());
Expects(_requests.empty());
Expects(_viewRequests.empty());
}
void SponsoredMessages::clear() {
_lifetime.destroy();
for (const auto &request : base::take(_requests)) {
_session->api().request(request.second.requestId).cancel(); _session->api().request(request.second.requestId).cancel();
} }
for (const auto &request : base::take(_viewRequests)) {
_session->api().request(request.second.requestId).cancel();
}
base::take(_data);
} }
void SponsoredMessages::clearOldRequests() { void SponsoredMessages::clearOldRequests() {
@ -263,94 +275,23 @@ void SponsoredMessages::append(
const MTPSponsoredMessage &message) { const MTPSponsoredMessage &message) {
const auto &data = message.data(); const auto &data = message.data();
const auto randomId = data.vrandom_id().v; const auto randomId = data.vrandom_id().v;
const auto hash = qs(data.vchat_invite_hash().value_or_empty()); const auto from = SponsoredFrom{
const auto makeFrom = [&]( .title = qs(data.vtitle()),
not_null<PeerData*> peer, .link = qs(data.vurl()),
bool exactPost = false) { .buttonText = qs(data.vbutton_text()),
const auto channel = peer->asChannel(); .photoId = data.vphoto()
return SponsoredFrom{ ? history->session().data().processPhoto(*data.vphoto())->id
.peer = peer, : PhotoId(0),
.title = peer->name(), .backgroundEmojiId = data.vcolor().has_value()
.isBroadcast = (channel && channel->isBroadcast()), ? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
.isMegagroup = (channel && channel->isMegagroup()), : uint64(0),
.isChannel = (channel != nullptr), .colorIndex = uint8(data.vcolor().has_value()
.isPublic = (channel && channel->isPublic()), ? data.vcolor()->data().vcolor().value_or_empty()
.isExactPost = exactPost, : 0),
.isRecommended = data.is_recommended(), .isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())),
.isForceUserpicDisplay = data.is_show_peer_photo(), .isRecommended = data.is_recommended(),
.buttonText = qs(data.vbutton_text().value_or_empty()), .canReport = data.is_can_report(),
.canReport = data.is_can_report(),
};
}; };
const auto externalLink = data.vwebpage()
? qs(data.vwebpage()->data().vurl())
: QString();
const auto from = [&]() -> SponsoredFrom {
if (const auto webpage = data.vwebpage()) {
const auto &data = webpage->data();
const auto photoId = data.vphoto()
? _session->data().processPhoto(*data.vphoto())->id
: PhotoId(0);
return SponsoredFrom{
.title = qs(data.vsite_name()),
.externalLink = externalLink,
.webpageOrBotPhotoId = photoId,
.isForceUserpicDisplay = message.data().is_show_peer_photo(),
.canReport = message.data().is_can_report(),
};
} else if (const auto fromId = data.vfrom_id()) {
const auto peerId = peerFromMTP(*fromId);
auto result = makeFrom(
_session->data().peer(peerId),
(data.vchannel_post() != nullptr));
const auto user = result.peer->asUser();
if (user && user->isBot()) {
const auto botAppData = data.vapp()
? _session->data().processBotApp(peerId, *data.vapp())
: nullptr;
result.botLinkInfo = Window::PeerByLinkInfo{
.usernameOrId = user->username(),
.resolveType = botAppData
? Window::ResolveType::BotApp
: data.vstart_param()
? Window::ResolveType::BotStart
: Window::ResolveType::Default,
.startToken = qs(data.vstart_param().value_or_empty()),
.botAppName = botAppData
? botAppData->shortName
: QString(),
};
result.webpageOrBotPhotoId = (botAppData && botAppData->photo)
? botAppData->photo->id
: PhotoId(0);
}
return result;
}
Assert(data.vchat_invite());
return data.vchat_invite()->match([&](const MTPDchatInvite &data) {
return SponsoredFrom{
.title = qs(data.vtitle()),
.isBroadcast = data.is_broadcast(),
.isMegagroup = data.is_megagroup(),
.isChannel = data.is_channel(),
.isPublic = data.is_public(),
.isForceUserpicDisplay = message.data().is_show_peer_photo(),
.canReport = message.data().is_can_report(),
};
}, [&](const MTPDchatInviteAlready &data) {
const auto chat = _session->data().processChat(data.vchat());
if (const auto channel = chat->asChannel()) {
channel->clearInvitePeek();
}
return makeFrom(chat);
}, [&](const MTPDchatInvitePeek &data) {
const auto chat = _session->data().processChat(data.vchat());
if (const auto channel = chat->asChannel()) {
channel->setInvitePeek(hash, data.vexpires().v);
}
return makeFrom(chat);
});
}();
auto sponsorInfo = data.vsponsor_info() auto sponsorInfo = data.vsponsor_info()
? tr::lng_sponsored_info_submenu( ? tr::lng_sponsored_info_submenu(
tr::now, tr::now,
@ -370,9 +311,7 @@ void SponsoredMessages::append(
data.ventities().value_or_empty()), data.ventities().value_or_empty()),
}, },
.history = history, .history = history,
.msgId = data.vchannel_post().value_or_empty(), .link = from.link,
.chatInviteHash = hash,
.externalLink = externalLink,
.sponsorInfo = std::move(sponsorInfo), .sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo), .additionalInfo = std::move(additionalInfo),
}; };
@ -444,7 +383,6 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
return {}; return {};
} }
const auto &data = entryPtr->sponsored; const auto &data = entryPtr->sponsored;
const auto &hash = data.chatInviteHash;
using InfoList = std::vector<TextWithEntities>; using InfoList = std::vector<TextWithEntities>;
auto info = (!data.sponsorInfo.text.isEmpty() auto info = (!data.sponsorInfo.text.isEmpty()
@ -456,20 +394,13 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
? InfoList{ data.additionalInfo } ? InfoList{ data.additionalInfo }
: InfoList{}; : InfoList{};
return { return {
.hash = hash.isEmpty() ? std::nullopt : std::make_optional(hash),
.peer = data.from.peer,
.msgId = data.msgId,
.info = std::move(info), .info = std::move(info),
.externalLink = data.externalLink, .link = data.link,
.isForceUserpicDisplay = data.from.isForceUserpicDisplay, .buttonText = data.from.buttonText,
.buttonText = !data.from.buttonText.isEmpty() .photoId = data.from.photoId,
? data.from.buttonText .backgroundEmojiId = data.from.backgroundEmojiId,
: !data.externalLink.isEmpty() .colorIndex = data.from.colorIndex,
? tr::lng_view_button_external_link(tr::now) .isLinkInternal = data.from.isLinkInternal,
: data.from.botLinkInfo
? tr::lng_view_button_bot(tr::now)
: QString(),
.botLinkInfo = data.from.botLinkInfo,
.canReport = data.from.canReport, .canReport = data.from.canReport,
}; };
} }

View file

@ -40,19 +40,14 @@ struct SponsoredReportResult final {
}; };
struct SponsoredFrom { struct SponsoredFrom {
PeerData *peer = nullptr;
QString title; QString title;
bool isBroadcast = false; QString link;
bool isMegagroup = false;
bool isChannel = false;
bool isPublic = false;
std::optional<Window::PeerByLinkInfo> botLinkInfo;
bool isExactPost = false;
bool isRecommended = false;
QString externalLink;
PhotoId webpageOrBotPhotoId = PhotoId(0);
bool isForceUserpicDisplay = false;
QString buttonText; QString buttonText;
PhotoId photoId = PhotoId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool isRecommended = false;
bool canReport = false; bool canReport = false;
}; };
@ -61,9 +56,7 @@ struct SponsoredMessage {
SponsoredFrom from; SponsoredFrom from;
TextWithEntities textWithEntities; TextWithEntities textWithEntities;
History *history = nullptr; History *history = nullptr;
MsgId msgId; QString link;
QString chatInviteHash;
QString externalLink;
TextWithEntities sponsorInfo; TextWithEntities sponsorInfo;
TextWithEntities additionalInfo; TextWithEntities additionalInfo;
}; };
@ -76,20 +69,17 @@ public:
InjectToMiddle, InjectToMiddle,
}; };
struct Details { struct Details {
std::optional<QString> hash;
PeerData *peer = nullptr;
MsgId msgId;
std::vector<TextWithEntities> info; std::vector<TextWithEntities> info;
QString externalLink; QString link;
bool isForceUserpicDisplay = false;
QString buttonText; QString buttonText;
std::optional<Window::PeerByLinkInfo> botLinkInfo; PhotoId photoId = PhotoId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool canReport = false; bool canReport = false;
}; };
using RandomId = QByteArray; using RandomId = QByteArray;
explicit SponsoredMessages(not_null<Main::Session*> session); explicit SponsoredMessages(not_null<Main::Session*> session);
SponsoredMessages(const SponsoredMessages &other) = delete;
SponsoredMessages &operator=(const SponsoredMessages &other) = delete;
~SponsoredMessages(); ~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const; [[nodiscard]] bool canHaveFor(not_null<History*> history) const;
@ -112,6 +102,8 @@ public:
[[nodiscard]] auto createReportCallback(const FullMsgId &fullId) [[nodiscard]] auto createReportCallback(const FullMsgId &fullId)
-> Fn<void(SponsoredReportResult::Id, Fn<void(SponsoredReportResult)>)>; -> Fn<void(SponsoredReportResult::Id, Fn<void(SponsoredReportResult)>)>;
void clear();
private: private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>; using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct Entry { struct Entry {

View file

@ -0,0 +1,284 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/top_peers.h"
#include "api/api_hash.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "mtproto/mtproto_config.h"
#include "storage/serialize_common.h"
#include "storage/serialize_peer.h"
#include "storage/storage_account.h"
namespace Data {
namespace {
constexpr auto kLimit = 64;
constexpr auto kRequestTimeLimit = 10 * crl::time(1000);
[[nodiscard]] float64 RatingDelta(TimeId now, TimeId was, int decay) {
return std::exp((now - was) * 1. / decay);
}
[[nodiscard]] quint64 SerializeRating(float64 rating) {
return quint64(
base::SafeRound(std::clamp(rating, 0., 1'000'000.) * 1'000'000.));
}
[[nodiscard]] float64 DeserializeRating(quint64 rating) {
return std::clamp(
rating,
quint64(),
quint64(1'000'000'000'000ULL)
) / 1'000'000.;
}
} // namespace
TopPeers::TopPeers(not_null<Main::Session*> session)
: _session(session) {
using namespace rpl::mappers;
crl::on_main(session, [=] {
_session->data().chatsListLoadedEvents(
) | rpl::filter(_1 == nullptr) | rpl::start_with_next([=] {
crl::on_main(_session, [=] {
request();
});
}, _session->lifetime());
});
}
TopPeers::~TopPeers() = default;
std::vector<not_null<PeerData*>> TopPeers::list() const {
_session->local().readSearchSuggestions();
return _list
| ranges::view::transform(&TopPeer::peer)
| ranges::to_vector;
}
bool TopPeers::disabled() const {
_session->local().readSearchSuggestions();
return _disabled;
}
rpl::producer<> TopPeers::updates() const {
return _updates.events();
}
void TopPeers::remove(not_null<PeerData*> peer) {
const auto i = ranges::find(_list, peer, &TopPeer::peer);
if (i != end(_list)) {
_list.erase(i);
updated();
}
_requestId = _session->api().request(MTPcontacts_ResetTopPeerRating(
MTP_topPeerCategoryCorrespondents(),
peer->input
)).send();
}
void TopPeers::increment(not_null<PeerData*> peer, TimeId date) {
_session->local().readSearchSuggestions();
if (_disabled || date <= _lastReceivedDate) {
return;
}
if (const auto user = peer->asUser(); user && !user->isBot()) {
auto changed = false;
auto i = ranges::find(_list, peer, &TopPeer::peer);
if (i == end(_list)) {
_list.push_back({ .peer = peer });
i = end(_list) - 1;
changed = true;
}
const auto &config = peer->session().mtp().config();
const auto decay = config.values().ratingDecay;
i->rating += RatingDelta(date, _lastReceivedDate, decay);
for (; i != begin(_list); --i) {
if (i->rating >= (i - 1)->rating) {
changed = true;
std::swap(*i, *(i - 1));
} else {
break;
}
}
if (changed) {
updated();
} else {
_session->local().writeSearchSuggestionsDelayed();
}
}
}
void TopPeers::reload() {
if (_requestId
|| (_lastReceived
&& _lastReceived + kRequestTimeLimit > crl::now())) {
return;
}
request();
}
void TopPeers::toggleDisabled(bool disabled) {
_session->local().readSearchSuggestions();
if (disabled) {
if (!_disabled || !_list.empty()) {
_disabled = true;
_list.clear();
updated();
}
} else if (_disabled) {
_disabled = false;
updated();
}
_session->api().request(MTPcontacts_ToggleTopPeers(
MTP_bool(!disabled)
)).done([=] {
if (!_disabled) {
request();
}
}).send();
}
void TopPeers::request() {
if (_requestId) {
return;
}
_requestId = _session->api().request(MTPcontacts_GetTopPeers(
MTP_flags(MTPcontacts_GetTopPeers::Flag::f_correspondents),
MTP_int(0),
MTP_int(kLimit),
MTP_long(countHash())
)).done([=](const MTPcontacts_TopPeers &result, const MTP::Response &response) {
_lastReceivedDate = TimeId(response.outerMsgId >> 32);
_lastReceived = crl::now();
_requestId = 0;
result.match([&](const MTPDcontacts_topPeers &data) {
_disabled = false;
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
for (const auto &category : data.vcategories().v) {
const auto &data = category.data();
data.vcategory().match(
[&](const MTPDtopPeerCategoryCorrespondents &) {
_list = ranges::views::all(
data.vpeers().v
) | ranges::views::transform([&](const MTPTopPeer &top) {
return TopPeer{
owner->peer(peerFromMTP(top.data().vpeer())),
top.data().vrating().v,
};
}) | ranges::to_vector;
}, [](const auto &) {
LOG(("API Error: Unexpected top peer category."));
});
}
updated();
}, [&](const MTPDcontacts_topPeersDisabled &) {
if (!_disabled) {
_list.clear();
_disabled = true;
updated();
}
}, [](const MTPDcontacts_topPeersNotModified &) {
});
}).fail([=] {
_lastReceived = crl::now();
_requestId = 0;
}).send();
}
uint64 TopPeers::countHash() const {
using namespace Api;
auto hash = HashInit();
for (const auto &top : _list | ranges::views::take(kLimit)) {
HashUpdate(hash, peerToUser(top.peer->id).bare);
}
return HashFinalize(hash);
}
void TopPeers::updated() {
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
QByteArray TopPeers::serialize() const {
_session->local().readSearchSuggestions();
if (!_disabled && _list.empty()) {
return {};
}
auto size = 3 * sizeof(quint32); // AppVersion, disabled, count
const auto count = std::min(int(_list.size()), kLimit);
auto &&list = _list | ranges::views::take(count);
for (const auto &top : list) {
size += Serialize::peerSize(top.peer) + sizeof(quint64);
}
auto stream = Serialize::ByteArrayWriter(size);
stream
<< quint32(AppVersion)
<< quint32(_disabled ? 1 : 0)
<< quint32(_list.size());
for (const auto &top : list) {
Serialize::writePeer(stream, top.peer);
stream << SerializeRating(top.rating);
}
return std::move(stream).result();
}
void TopPeers::applyLocal(QByteArray serialized) {
if (_lastReceived) {
return;
}
_list.clear();
_disabled = false;
if (serialized.isEmpty()) {
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
auto streamAppVersion = quint32();
auto disabled = quint32();
auto count = quint32();
stream >> streamAppVersion >> disabled >> count;
if (!stream.ok()) {
return;
}
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
auto rating = quint64();
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
stream);
stream >> rating;
if (stream.ok() && peer) {
_list.push_back({
.peer = peer,
.rating = DeserializeRating(rating),
});
} else {
_list.clear();
return;
}
}
_disabled = (disabled == 1);
}
} // namespace Data

View file

@ -0,0 +1,57 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Data {
class TopPeers final {
public:
explicit TopPeers(not_null<Main::Session*> session);
~TopPeers();
[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
[[nodiscard]] bool disabled() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void increment(not_null<PeerData*> peer, TimeId date);
void reload();
void toggleDisabled(bool disabled);
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
private:
struct TopPeer {
not_null<PeerData*> peer;
float64 rating = 0.;
};
void request();
[[nodiscard]] uint64 countHash() const;
void updated();
const not_null<Main::Session*> _session;
std::vector<TopPeer> _list;
rpl::event_stream<> _updates;
crl::time _lastReceived = 0;
TimeId _lastReceivedDate = 0;
mtpRequestId _requestId = 0;
bool _disabled = false;
bool _received = false;
};
} // namespace Data

View file

@ -165,6 +165,7 @@ void ChannelData::setFlags(ChannelDataFlags which) {
const auto taken = ((diff & Flag::Forum) && !(which & Flag::Forum)) const auto taken = ((diff & Flag::Forum) && !(which & Flag::Forum))
? mgInfo->takeForumData() ? mgInfo->takeForumData()
: nullptr; : nullptr;
const auto wasIn = amIn();
if ((diff & Flag::Forum) && (which & Flag::Forum)) { if ((diff & Flag::Forum) && (which & Flag::Forum)) {
mgInfo->ensureForum(this); mgInfo->ensureForum(this);
} }
@ -174,6 +175,14 @@ void ChannelData::setFlags(ChannelDataFlags which) {
session().changes().peerUpdated(chat, UpdateFlag::Migration); session().changes().peerUpdated(chat, UpdateFlag::Migration);
session().changes().peerUpdated(this, UpdateFlag::Migration); session().changes().peerUpdated(this, UpdateFlag::Migration);
} }
if (wasIn && !amIn()) {
crl::on_main(&session(), [=] {
if (!amIn()) {
Core::App().closeChatFromWindows(this);
}
});
}
} }
if (diff & (Flag::Forum | Flag::CallNotEmpty | Flag::SimilarExpanded)) { if (diff & (Flag::Forum | Flag::CallNotEmpty | Flag::SimilarExpanded)) {
if (const auto history = this->owner().historyLoaded(this)) { if (const auto history = this->owner().historyLoaded(this)) {
@ -1198,10 +1207,14 @@ void ApplyChannelUpdate(
} }
channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
channel->setTranslationDisabled(update.is_translations_disabled()); channel->setTranslationDisabled(update.is_translations_disabled());
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) { if (const auto allowed = update.vavailable_reactions()) {
channel->setAllowedReactions(Data::Parse(*allowed)); auto parsed = Data::Parse(*allowed);
parsed.maxCount = reactionsLimit;
channel->setAllowedReactions(std::move(parsed));
} else { } else {
channel->setAllowedReactions({}); channel->setAllowedReactions({ .maxCount = reactionsLimit });
} }
channel->owner().stories().apply(channel, update.vstories()); channel->owner().stories().apply(channel, update.vstories());
channel->fullUpdated(); channel->fullUpdated();

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_chat.h" #include "data/data_chat.h"
#include "core/application.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -132,6 +133,18 @@ void ChatData::invalidateParticipants() {
UpdateFlag::Members | UpdateFlag::Admins); UpdateFlag::Members | UpdateFlag::Admins);
} }
void ChatData::setFlags(ChatDataFlags which) {
const auto wasIn = amIn();
_flags.set(which);
if (wasIn && !amIn()) {
crl::on_main(&session(), [=] {
if (!amIn()) {
Core::App().closeChatFromWindows(this);
}
});
}
}
void ChatData::setInviteLink(const QString &newInviteLink) { void ChatData::setInviteLink(const QString &newInviteLink) {
_inviteLink = newInviteLink; _inviteLink = newInviteLink;
} }
@ -471,10 +484,13 @@ void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
chat->checkFolder(update.vfolder_id().value_or_empty()); chat->checkFolder(update.vfolder_id().value_or_empty());
chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
chat->setTranslationDisabled(update.is_translations_disabled()); chat->setTranslationDisabled(update.is_translations_disabled());
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) { if (const auto allowed = update.vavailable_reactions()) {
chat->setAllowedReactions(Data::Parse(*allowed)); auto parsed = Data::Parse(*allowed);
parsed.maxCount = reactionsLimit;
chat->setAllowedReactions(std::move(parsed));
} else { } else {
chat->setAllowedReactions({}); chat->setAllowedReactions({ .maxCount = reactionsLimit });
} }
chat->fullUpdated(); chat->fullUpdated();
chat->setAbout(qs(update.vabout())); chat->setAbout(qs(update.vabout()));

View file

@ -42,9 +42,7 @@ public:
return (count > 0 || amIn()) && participants.empty(); return (count > 0 || amIn()) && participants.empty();
} }
void setFlags(ChatDataFlags which) { void setFlags(ChatDataFlags which);
_flags.set(which);
}
void addFlags(ChatDataFlags which) { void addFlags(ChatDataFlags which) {
_flags.add(which); _flags.add(which);
} }

View file

@ -129,9 +129,12 @@ void ClearPeerCloudDraft(
history->applyCloudDraft(topicRootId); history->applyCloudDraft(topicRootId);
} }
void SetChatLinkDraft( void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) {
not_null<PeerData*> peer, static const auto kInlineStart = QRegularExpression("^@[a-zA-Z0-9_]");
const TextWithEntities &draft) { if (kInlineStart.match(draft.text).hasMatch()) {
draft = TextWithEntities().append(' ').append(std::move(draft));
}
const auto textWithTags = TextWithTags{ const auto textWithTags = TextWithTags{
draft.text, draft.text,
TextUtilities::ConvertEntitiesToTextTags(draft.entities) TextUtilities::ConvertEntitiesToTextTags(draft.entities)

View file

@ -205,8 +205,6 @@ using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;
&& (a->webpage == b->webpage); && (a->webpage == b->webpage);
} }
void SetChatLinkDraft( void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft);
not_null<PeerData*> peer,
const TextWithEntities &draft);
} // namespace Data } // namespace Data

View file

@ -153,6 +153,11 @@ auto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {
return _statusGroups.data.value(); return _statusGroups.data.value();
} }
auto EmojiStatuses::stickerGroupsValue() const -> rpl::producer<Groups> {
const_cast<EmojiStatuses*>(this)->requestStickerGroups();
return _stickerGroups.data.value();
}
auto EmojiStatuses::profilePhotoGroupsValue() const auto EmojiStatuses::profilePhotoGroupsValue() const
-> rpl::producer<Groups> { -> rpl::producer<Groups> {
const_cast<EmojiStatuses*>(this)->requestProfilePhotoGroups(); const_cast<EmojiStatuses*>(this)->requestProfilePhotoGroups();
@ -172,6 +177,12 @@ void EmojiStatuses::requestStatusGroups() {
MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash))); MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));
} }
void EmojiStatuses::requestStickerGroups() {
requestGroups(
&_stickerGroups,
MTPmessages_GetEmojiStickerGroups(MTP_int(_stickerGroups.hash)));
}
void EmojiStatuses::requestProfilePhotoGroups() { void EmojiStatuses::requestProfilePhotoGroups() {
requestGroups( requestGroups(
&_profilePhotoGroups, &_profilePhotoGroups,
@ -185,15 +196,24 @@ void EmojiStatuses::requestProfilePhotoGroups() {
auto result = std::vector<Ui::EmojiGroup>(); auto result = std::vector<Ui::EmojiGroup>();
result.reserve(list.size()); result.reserve(list.size());
for (const auto &group : list) { for (const auto &group : list) {
const auto &data = group.data(); group.match([&](const MTPDemojiGroupPremium &data) {
auto emoticons = ranges::views::all( result.push_back({
data.vemoticons().v .iconId = QString::number(data.vicon_emoji_id().v),
) | ranges::views::transform([](const MTPstring &emoticon) { .type = Ui::EmojiGroupType::Premium,
return qs(emoticon); });
}) | ranges::to_vector; }, [&](const auto &data) {
result.push_back({ auto emoticons = ranges::views::all(
.iconId = QString::number(data.vicon_emoji_id().v), data.vemoticons().v
.emoticons = std::move(emoticons), ) | ranges::views::transform([](const MTPstring &emoticon) {
return qs(emoticon);
}) | ranges::to_vector;
result.push_back({
.iconId = QString::number(data.vicon_emoji_id().v),
.emoticons = std::move(emoticons),
.type = (MTPDemojiGroupGreeting::Is<decltype(data)>()
? Ui::EmojiGroupType::Greeting
: Ui::EmojiGroupType::Normal),
});
}); });
} }
return result; return result;

View file

@ -60,9 +60,11 @@ public:
using Groups = std::vector<Ui::EmojiGroup>; using Groups = std::vector<Ui::EmojiGroup>;
[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const; [[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;
[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const; [[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;
[[nodiscard]] rpl::producer<Groups> stickerGroupsValue() const;
[[nodiscard]] rpl::producer<Groups> profilePhotoGroupsValue() const; [[nodiscard]] rpl::producer<Groups> profilePhotoGroupsValue() const;
void requestEmojiGroups(); void requestEmojiGroups();
void requestStatusGroups(); void requestStatusGroups();
void requestStickerGroups();
void requestProfilePhotoGroups(); void requestProfilePhotoGroups();
private: private:
@ -124,6 +126,7 @@ private:
GroupsType _emojiGroups; GroupsType _emojiGroups;
GroupsType _statusGroups; GroupsType _statusGroups;
GroupsType _stickerGroups;
GroupsType _profilePhotoGroups; GroupsType _profilePhotoGroups;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -57,6 +57,8 @@ struct FileReferenceAccumulator {
push(data.vstory()); push(data.vstory());
}, [&](const MTPDwebPageAttributeTheme &data) { }, [&](const MTPDwebPageAttributeTheme &data) {
push(data.vdocuments()); push(data.vdocuments());
}, [&](const MTPDwebPageAttributeStickerSet &data) {
push(data.vstickers());
}); });
} }
void push(const MTPWebPage &data) { void push(const MTPWebPage &data) {

View file

@ -1311,8 +1311,9 @@ std::unique_ptr<HistoryView::Media> MediaContact::createView(
MediaLocation::MediaLocation( MediaLocation::MediaLocation(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
const LocationPoint &point) const LocationPoint &point,
: MediaLocation(parent, point, QString(), QString()) { TimeId livePeriod)
: MediaLocation({}, parent, point, livePeriod, QString(), QString()) {
} }
MediaLocation::MediaLocation( MediaLocation::MediaLocation(
@ -1320,17 +1321,30 @@ MediaLocation::MediaLocation(
const LocationPoint &point, const LocationPoint &point,
const QString &title, const QString &title,
const QString &description) const QString &description)
: MediaLocation({}, parent, point, TimeId(), title, description) {
}
MediaLocation::MediaLocation(
PrivateTag,
not_null<HistoryItem*> parent,
const LocationPoint &point,
TimeId livePeriod,
const QString &title,
const QString &description)
: Media(parent) : Media(parent)
, _point(point) , _point(point)
, _location(parent->history()->owner().location(point)) , _location(parent->history()->owner().location(point))
, _livePeriod(livePeriod)
, _title(title) , _title(title)
, _description(description) { , _description(description) {
} }
std::unique_ptr<Media> MediaLocation::clone(not_null<HistoryItem*> parent) { std::unique_ptr<Media> MediaLocation::clone(not_null<HistoryItem*> parent) {
return std::make_unique<MediaLocation>( return std::make_unique<MediaLocation>(
PrivateTag(),
parent, parent,
_point, _point,
_livePeriod,
_title, _title,
_description); _description);
} }
@ -1339,8 +1353,14 @@ CloudImage *MediaLocation::location() const {
return _location; return _location;
} }
QString MediaLocation::typeString() const {
return _livePeriod
? tr::lng_live_location(tr::now)
: tr::lng_maps_point(tr::now);
}
ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const { ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
const auto type = tr::lng_maps_point(tr::now); const auto type = typeString();
const auto hasMiniImages = false; const auto hasMiniImages = false;
const auto text = TextWithEntities{ .text = _title }; const auto text = TextWithEntities{ .text = _title };
return { return {
@ -1349,9 +1369,7 @@ ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
} }
TextWithEntities MediaLocation::notificationText() const { TextWithEntities MediaLocation::notificationText() const {
return WithCaptionNotificationText( return WithCaptionNotificationText(typeString(), { .text = _title });
tr::lng_maps_point(tr::now),
{ .text = _title });
} }
QString MediaLocation::pinnedTextSubstring() const { QString MediaLocation::pinnedTextSubstring() const {
@ -1360,7 +1378,7 @@ QString MediaLocation::pinnedTextSubstring() const {
TextForMimeData MediaLocation::clipboardText() const { TextForMimeData MediaLocation::clipboardText() const {
auto result = TextForMimeData::Simple( auto result = TextForMimeData::Simple(
u"[ "_q + tr::lng_maps_point(tr::now) + u" ]\n"_q); u"[ "_q + typeString() + u" ]\n"_q);
auto titleResult = TextUtilities::ParseEntities( auto titleResult = TextUtilities::ParseEntities(
_title, _title,
Ui::WebpageTextTitleOptions().flags); Ui::WebpageTextTitleOptions().flags);
@ -1389,12 +1407,19 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
not_null<HistoryView::Element*> message, not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) { HistoryView::Element *replacing) {
return std::make_unique<HistoryView::Location>( return _livePeriod
message, ? std::make_unique<HistoryView::Location>(
_location, message,
_point, _location,
_title, _point,
_description); replacing,
_livePeriod)
: std::make_unique<HistoryView::Location>(
message,
_location,
_point,
_title,
_description);
} }
MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call) MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
@ -1857,23 +1882,21 @@ TextWithEntities MediaPoll::notificationText() const {
} }
QString MediaPoll::pinnedTextSubstring() const { QString MediaPoll::pinnedTextSubstring() const {
return QChar(171) + _poll->question + QChar(187); return QChar(171) + _poll->question.text + QChar(187);
} }
TextForMimeData MediaPoll::clipboardText() const { TextForMimeData MediaPoll::clipboardText() const {
const auto text = u"[ "_q auto result = TextWithEntities();
+ tr::lng_in_dlg_poll(tr::now) result
+ u" : "_q .append(u"[ "_q)
+ _poll->question .append(tr::lng_in_dlg_poll(tr::now))
+ u" ]"_q .append(u" : "_q)
+ ranges::accumulate( .append(_poll->question)
ranges::views::all( .append(u" ]"_q);
_poll->answers for (const auto &answer : _poll->answers) {
) | ranges::views::transform([](const PollAnswer &answer) { result.append(u"\n- "_q).append(answer.text);
return "\n- " + answer.text; }
}), return TextForMimeData::Rich(std::move(result));
QString());
return TextForMimeData::Simple(text);
} }
bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) { bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) {
@ -2344,9 +2367,7 @@ const GiveawayResults *MediaGiveawayResults::giveawayResults() const {
} }
TextWithEntities MediaGiveawayResults::notificationText() const { TextWithEntities MediaGiveawayResults::notificationText() const {
return { return Ui::Text::Colorized({ tr::lng_prizes_results_title(tr::now) });
.text = tr::lng_prizes_results_title(tr::now),
};
} }
QString MediaGiveawayResults::pinnedTextSubstring() const { QString MediaGiveawayResults::pinnedTextSubstring() const {

View file

@ -331,10 +331,14 @@ private:
}; };
class MediaLocation final : public Media { class MediaLocation final : public Media {
struct PrivateTag {
};
public: public:
MediaLocation( MediaLocation(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
const LocationPoint &point); const LocationPoint &point,
TimeId livePeriod = 0);
MediaLocation( MediaLocation(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
const LocationPoint &point, const LocationPoint &point,
@ -356,9 +360,21 @@ public:
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
HistoryView::Element *replacing = nullptr) override; HistoryView::Element *replacing = nullptr) override;
MediaLocation(
PrivateTag,
not_null<HistoryItem*> parent,
const LocationPoint &point,
TimeId livePeriod,
const QString &title,
const QString &description);
private: private:
[[nodiscard]] QString typeString() const;
LocationPoint _point; LocationPoint _point;
not_null<CloudImage*> _location; not_null<CloudImage*> _location;
TimeId _livePeriod = 0;
QString _title; QString _title;
QString _description; QString _description;

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/history_item_components.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "main/session/send_as_peers.h" #include "main/session/send_as_peers.h"
@ -142,7 +143,14 @@ PossibleItemReactionsRef LookupPossibleReactions(
return {}; return {};
} }
auto result = PossibleItemReactionsRef(); auto result = PossibleItemReactionsRef();
const auto peer = item->history()->peer; auto peer = item->history()->peer;
if (item->isDiscussionPost()) {
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
if (forwarded->savedFromPeer) {
peer = forwarded->savedFromPeer;
}
}
}
const auto session = &peer->session(); const auto session = &peer->session();
const auto reactions = &session->data().reactions(); const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active); const auto &full = reactions->list(Reactions::Type::Active);

View file

@ -104,7 +104,9 @@ bool operator<(
bool operator==( bool operator==(
const AllowedReactions &a, const AllowedReactions &a,
const AllowedReactions &b) { const AllowedReactions &b) {
return (a.type == b.type) && (a.some == b.some); return (a.type == b.type)
&& (a.some == b.some)
&& (a.maxCount == b.maxCount);
} }
AllowedReactions Parse(const MTPChatReactions &value) { AllowedReactions Parse(const MTPChatReactions &value) {
@ -531,7 +533,9 @@ bool PeerData::canPinMessages() const {
bool PeerData::canCreatePolls() const { bool PeerData::canCreatePolls() const {
if (const auto user = asUser()) { if (const auto user = asUser()) {
return user->isBot() && !user->isSupport(); return user->isBot()
&& !user->isSupport()
&& !user->isRepliesChat();
} }
return Data::CanSend(this, ChatRestriction::SendPolls); return Data::CanSend(this, ChatRestriction::SendPolls);
} }

View file

@ -110,6 +110,7 @@ enum class AllowedReactionsType {
struct AllowedReactions { struct AllowedReactions {
std::vector<ReactionId> some; std::vector<ReactionId> some;
AllowedReactionsType type = AllowedReactionsType::Some; AllowedReactionsType type = AllowedReactionsType::Some;
int maxCount = 0;
}; };
bool operator<(const AllowedReactions &a, const AllowedReactions &b); bool operator<(const AllowedReactions &a, const AllowedReactions &b);

View file

@ -79,6 +79,10 @@ std::optional<QString> OnlineTextCommon(LastseenStatus status, TimeId now) {
return std::nullopt; return std::nullopt;
} }
[[nodiscard]] int UniqueReactionsLimit(not_null<Main::AppConfig*> config) {
return config->get<int>("reactions_uniq_max", 11);
}
} // namespace } // namespace
inline auto AdminRightsValue(not_null<ChannelData*> channel) { inline auto AdminRightsValue(not_null<ChannelData*> channel) {
@ -571,21 +575,45 @@ const AllowedReactions &PeerAllowedReactions(not_null<PeerData*> peer) {
}); });
} }
int UniqueReactionsLimit(not_null<Main::AppConfig*> config) {
return config->get<int>("reactions_uniq_max", 11);
}
int UniqueReactionsLimit(not_null<PeerData*> peer) { int UniqueReactionsLimit(not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
if (const auto limit = channel->allowedReactions().maxCount) {
return limit;
}
} else if (const auto chat = peer->asChat()) {
if (const auto limit = chat->allowedReactions().maxCount) {
return limit;
}
}
return UniqueReactionsLimit(&peer->session().appConfig()); return UniqueReactionsLimit(&peer->session().appConfig());
} }
rpl::producer<int> UniqueReactionsLimitValue( rpl::producer<int> UniqueReactionsLimitValue(
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
const auto config = &peer->session().appConfig(); auto configValue = peer->session().appConfig().value(
return config->value( ) | rpl::map([config = &peer->session().appConfig()] {
) | rpl::map([=] {
return UniqueReactionsLimit(config); return UniqueReactionsLimit(config);
}) | rpl::distinct_until_changed(); }) | rpl::distinct_until_changed();
if (const auto channel = peer->asChannel()) {
return rpl::combine(
PeerAllowedReactionsValue(peer),
std::move(configValue)
) | rpl::map([=](const auto &allowedReactions, int limit) {
return allowedReactions.maxCount
? allowedReactions.maxCount
: limit;
});
} else if (const auto chat = peer->asChat()) {
return rpl::combine(
PeerAllowedReactionsValue(peer),
std::move(configValue)
) | rpl::map([=](const auto &allowedReactions, int limit) {
return allowedReactions.maxCount
? allowedReactions.maxCount
: limit;
});
}
return configValue;
} }
} // namespace Data } // namespace Data

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_poll.h" #include "data/data_poll.h"
#include "api/api_text_entities.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
@ -69,7 +70,12 @@ bool PollData::closeByTimer() {
bool PollData::applyChanges(const MTPDpoll &poll) { bool PollData::applyChanges(const MTPDpoll &poll) {
Expects(poll.vid().v == id); Expects(poll.vid().v == id);
const auto newQuestion = qs(poll.vquestion()); const auto newQuestion = TextWithEntities{
.text = qs(poll.vquestion().data().vtext()),
.entities = Api::EntitiesFromMTP(
&session(),
poll.vquestion().data().ventities().v),
};
const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0)) const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0))
| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0)) | (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))
| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0)) | (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))
@ -78,11 +84,16 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
const auto newClosePeriod = poll.vclose_period().value_or_empty(); const auto newClosePeriod = poll.vclose_period().value_or_empty();
auto newAnswers = ranges::views::all( auto newAnswers = ranges::views::all(
poll.vanswers().v poll.vanswers().v
) | ranges::views::transform([](const MTPPollAnswer &data) { ) | ranges::views::transform([&](const MTPPollAnswer &data) {
return data.match([](const MTPDpollAnswer &answer) { return data.match([&](const MTPDpollAnswer &answer) {
auto result = PollAnswer(); auto result = PollAnswer();
result.option = answer.voption().v; result.option = answer.voption().v;
result.text = qs(answer.vtext()); result.text = TextWithEntities{
.text = qs(answer.vtext().data().vtext()),
.entities = Api::EntitiesFromMTP(
&session(),
answer.vtext().data().ventities().v),
};
return result; return result;
}); });
}) | ranges::views::take( }) | ranges::views::take(
@ -251,9 +262,11 @@ bool PollData::quiz() const {
} }
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) { MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
const auto convert = [](const PollAnswer &answer) { const auto convert = [&](const PollAnswer &answer) {
return MTP_pollAnswer( return MTP_pollAnswer(
MTP_string(answer.text), MTP_textWithEntities(
MTP_string(answer.text.text),
Api::EntitiesToMTP(&poll->session(), answer.text.entities)),
MTP_bytes(answer.option)); MTP_bytes(answer.option));
}; };
auto answers = QVector<MTPPollAnswer>(); auto answers = QVector<MTPPollAnswer>();
@ -272,7 +285,9 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
return MTP_poll( return MTP_poll(
MTP_long(poll->id), MTP_long(poll->id),
MTP_flags(flags), MTP_flags(flags),
MTP_string(poll->question), MTP_textWithEntities(
MTP_string(poll->question.text),
Api::EntitiesToMTP(&poll->session(), poll->question.entities)),
MTP_vector<MTPPollAnswer>(answers), MTP_vector<MTPPollAnswer>(answers),
MTP_int(poll->closePeriod), MTP_int(poll->closePeriod),
MTP_int(poll->closeDate)); MTP_int(poll->closeDate));

View file

@ -16,7 +16,7 @@ class Session;
} // namespace Main } // namespace Main
struct PollAnswer { struct PollAnswer {
QString text; TextWithEntities text;
QByteArray option; QByteArray option;
int votes = 0; int votes = 0;
bool chosen = false; bool chosen = false;
@ -65,7 +65,7 @@ struct PollData {
[[nodiscard]] bool quiz() const; [[nodiscard]] bool quiz() const;
PollId id = 0; PollId id = 0;
QString question; TextWithEntities question;
std::vector<PollAnswer> answers; std::vector<PollAnswer> answers;
std::vector<not_null<PeerData*>> recentVoters; std::vector<not_null<PeerData*>> recentVoters;
std::vector<QByteArray> sendingVotes; std::vector<QByteArray> sendingVotes;

View file

@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/business/data_business_info.h" #include "data/business/data_business_info.h"
#include "data/business/data_shortcut_messages.h" #include "data/business/data_shortcut_messages.h"
#include "data/components/scheduled_messages.h" #include "data/components/scheduled_messages.h"
#include "data/components/sponsored_messages.h"
#include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers.h"
#include "data/notify/data_notify_settings.h" #include "data/notify/data_notify_settings.h"
#include "data/data_bot_app.h" #include "data/data_bot_app.h"
@ -406,6 +407,7 @@ void Session::clear() {
_histories->unloadAll(); _histories->unloadAll();
_shortcutMessages = nullptr; _shortcutMessages = nullptr;
_session->scheduledMessages().clear(); _session->scheduledMessages().clear();
_session->sponsoredMessages().clear();
_dependentMessages.clear(); _dependentMessages.clear();
base::take(_messages); base::take(_messages);
base::take(_nonChannelMessages); base::take(_nonChannelMessages);
@ -757,8 +759,10 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
result->setLoadedStatus(PeerData::LoadedStatus::Normal); result->setLoadedStatus(PeerData::LoadedStatus::Normal);
} }
if (status && !minimal) { if (!minimal) {
const auto lastseen = LastseenFromMTP(*status, result->lastseen()); const auto lastseen = status
? LastseenFromMTP(*status, result->lastseen())
: Data::LastseenStatus::LongAgo(false);
if (result->updateLastseen(lastseen)) { if (result->updateLastseen(lastseen)) {
flags |= UpdateFlag::OnlineStatus; flags |= UpdateFlag::OnlineStatus;
} }
@ -3439,6 +3443,7 @@ not_null<WebPageData*> Session::processWebpage(
nullptr, nullptr,
WebPageCollage(), WebPageCollage(),
nullptr, nullptr,
nullptr,
0, 0,
QString(), QString(),
false, false,
@ -3464,6 +3469,7 @@ not_null<WebPageData*> Session::webpage(
nullptr, nullptr,
WebPageCollage(), WebPageCollage(),
nullptr, nullptr,
nullptr,
0, 0,
QString(), QString(),
false, false,
@ -3482,6 +3488,7 @@ not_null<WebPageData*> Session::webpage(
DocumentData *document, DocumentData *document,
WebPageCollage &&collage, WebPageCollage &&collage,
std::unique_ptr<Iv::Data> iv, std::unique_ptr<Iv::Data> iv,
std::unique_ptr<WebPageStickerSet> stickerSet,
int duration, int duration,
const QString &author, const QString &author,
bool hasLargeMedia, bool hasLargeMedia,
@ -3500,6 +3507,7 @@ not_null<WebPageData*> Session::webpage(
document, document,
std::move(collage), std::move(collage),
std::move(iv), std::move(iv),
std::move(stickerSet),
duration, duration,
author, author,
hasLargeMedia, hasLargeMedia,
@ -3540,7 +3548,9 @@ void Session::webpageApplyFields(
const auto result = attribute.match([&]( const auto result = attribute.match([&](
const MTPDwebPageAttributeTheme &data) { const MTPDwebPageAttributeTheme &data) {
return lookupInAttribute(data); return lookupInAttribute(data);
}, [&](const MTPDwebPageAttributeStory &data) { }, [](const MTPDwebPageAttributeStory &) {
return (DocumentData*)nullptr;
}, [](const MTPDwebPageAttributeStickerSet &) {
return (DocumentData*)nullptr; return (DocumentData*)nullptr;
}); });
if (result) { if (result) {
@ -3550,6 +3560,29 @@ void Session::webpageApplyFields(
} }
return nullptr; return nullptr;
}; };
using WebPageStickerSetPtr = std::unique_ptr<WebPageStickerSet>;
const auto lookupStickerSet = [&]() -> WebPageStickerSetPtr {
if (const auto attributes = data.vattributes()) {
for (const auto &attribute : attributes->v) {
auto result = attribute.match([&](
const MTPDwebPageAttributeStickerSet &data) {
auto result = std::make_unique<WebPageStickerSet>();
result->isEmoji = data.is_emojis();
result->isTextColor = data.is_text_color();
for (const auto &tl : data.vstickers().v) {
result->items.push_back(processDocument(tl));
}
return result;
}, [](const auto &) {
return WebPageStickerSetPtr(nullptr);
});
if (result && !result->items.empty()) {
return result;
}
}
}
return nullptr;
};
auto story = (Data::Story*)nullptr; auto story = (Data::Story*)nullptr;
auto storyId = FullStoryId(); auto storyId = FullStoryId();
if (const auto attributes = data.vattributes()) { if (const auto attributes = data.vattributes()) {
@ -3641,6 +3674,7 @@ void Session::webpageApplyFields(
: lookupThemeDocument()), : lookupThemeDocument()),
WebPageCollage(this, data), WebPageCollage(this, data),
std::move(iv), std::move(iv),
lookupStickerSet(),
data.vduration().value_or_empty(), data.vduration().value_or_empty(),
qs(data.vauthor().value_or_empty()), qs(data.vauthor().value_or_empty()),
data.is_has_large_media(), data.is_has_large_media(),
@ -3660,6 +3694,7 @@ void Session::webpageApplyFields(
DocumentData *document, DocumentData *document,
WebPageCollage &&collage, WebPageCollage &&collage,
std::unique_ptr<Iv::Data> iv, std::unique_ptr<Iv::Data> iv,
std::unique_ptr<WebPageStickerSet> stickerSet,
int duration, int duration,
const QString &author, const QString &author,
bool hasLargeMedia, bool hasLargeMedia,
@ -3677,6 +3712,7 @@ void Session::webpageApplyFields(
document, document,
std::move(collage), std::move(collage),
std::move(iv), std::move(iv),
std::move(stickerSet),
duration, duration,
author, author,
hasLargeMedia, hasLargeMedia,

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class Image; class Image;
class HistoryItem; class HistoryItem;
struct WebPageCollage; struct WebPageCollage;
struct WebPageStickerSet;
enum class WebPageType : uint8; enum class WebPageType : uint8;
enum class NewMessageType; enum class NewMessageType;
@ -579,6 +580,7 @@ public:
DocumentData *document, DocumentData *document,
WebPageCollage &&collage, WebPageCollage &&collage,
std::unique_ptr<Iv::Data> iv, std::unique_ptr<Iv::Data> iv,
std::unique_ptr<WebPageStickerSet> stickerSet,
int duration, int duration,
const QString &author, const QString &author,
bool hasLargeMedia, bool hasLargeMedia,
@ -857,6 +859,7 @@ private:
DocumentData *document, DocumentData *document,
WebPageCollage &&collage, WebPageCollage &&collage,
std::unique_ptr<Iv::Data> iv, std::unique_ptr<Iv::Data> iv,
std::unique_ptr<WebPageStickerSet> stickerSet,
int duration, int duration,
const QString &author, const QString &author,
bool hasLargeMedia, bool hasLargeMedia,

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "ui/layers/show.h" #include "ui/layers/show.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
@ -81,6 +82,21 @@ using UpdateFlag = StoryUpdate::Flag;
} // namespace } // namespace
std::vector<StoryId> RespectingPinned(const StoriesIds &ids) {
if (ids.pinnedToTop.empty()) {
return ids.list | ranges::to_vector;
}
auto result = std::vector<StoryId>();
result.reserve(ids.list.size());
result.insert(end(result), begin(ids.pinnedToTop), end(ids.pinnedToTop));
for (const auto &id : ids.list) {
if (!ranges::contains(ids.pinnedToTop, id)) {
result.push_back(id);
}
}
return result;
}
StoriesSourceInfo StoriesSource::info() const { StoriesSourceInfo StoriesSource::info() const {
return { return {
.id = peer->id, .id = peer->id,
@ -344,7 +360,7 @@ void Stories::clearArchive(not_null<PeerData*> peer) {
_archive.erase(i); _archive.erase(i);
for (const auto &id : archive.ids.list) { for (const auto &id : archive.ids.list) {
if (const auto story = lookup({ peerId, id })) { if (const auto story = lookup({ peerId, id })) {
if ((*story)->expired() && !(*story)->pinned()) { if ((*story)->expired() && !(*story)->inProfile()) {
applyDeleted(peer, id); applyDeleted(peer, id);
} }
} }
@ -562,8 +578,8 @@ void Stories::unregisterDependentMessage(
void Stories::savedStateChanged(not_null<Story*> story) { void Stories::savedStateChanged(not_null<Story*> story) {
const auto id = story->id(); const auto id = story->id();
const auto peer = story->peer()->id; const auto peer = story->peer()->id;
const auto pinned = story->pinned(); const auto inProfile = story->inProfile();
if (pinned) { if (inProfile) {
auto &saved = _saved[peer]; auto &saved = _saved[peer];
const auto added = saved.ids.list.emplace(id).second; const auto added = saved.ids.list.emplace(id).second;
if (added) { if (added) {
@ -798,7 +814,7 @@ void Stories::applyDeleted(not_null<PeerData*> peer, StoryId id) {
} }
} }
} }
if (story->pinned()) { if (story->inProfile()) {
if (const auto k = _saved.find(peerId); k != end(_saved)) { if (const auto k = _saved.find(peerId); k != end(_saved)) {
const auto saved = &k->second; const auto saved = &k->second;
if (saved->ids.list.remove(id)) { if (saved->ids.list.remove(id)) {
@ -836,7 +852,7 @@ void Stories::applyDeleted(not_null<PeerData*> peer, StoryId id) {
void Stories::applyExpired(FullStoryId id) { void Stories::applyExpired(FullStoryId id) {
if (const auto maybeStory = lookup(id)) { if (const auto maybeStory = lookup(id)) {
const auto story = *maybeStory; const auto story = *maybeStory;
if (!hasArchive(story->peer()) && !story->pinned()) { if (!hasArchive(story->peer()) && !story->inProfile()) {
applyDeleted(story->peer(), id.story); applyDeleted(story->peer(), id.story);
return; return;
} }
@ -1117,7 +1133,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
} }
const auto story = *maybeStory; const auto story = *maybeStory;
if (story->expired() && story->pinned()) { if (story->expired() && story->inProfile()) {
_incrementViewsPending[id.peer].emplace(id.story); _incrementViewsPending[id.peer].emplace(id.story);
if (!_incrementViewsTimer.isActive()) { if (!_incrementViewsTimer.isActive()) {
_incrementViewsTimer.callOnce(kIncrementViewsDelay); _incrementViewsTimer.callOnce(kIncrementViewsDelay);
@ -1714,6 +1730,10 @@ void Stories::savedLoadMore(PeerId peerId) {
const auto &data = result.data(); const auto &data = result.data();
const auto now = base::unixtime::now(); const auto now = base::unixtime::now();
auto pinnedToTopIds = data.vpinned_to_top().value_or_empty();
auto pinnedToTop = pinnedToTopIds
| ranges::views::transform(&MTPint::v)
| ranges::to_vector;
saved.total = data.vcount().v; saved.total = data.vcount().v;
for (const auto &story : data.vstories().v) { for (const auto &story : data.vstories().v) {
const auto id = story.match([&](const auto &id) { const auto id = story.match([&](const auto &id) {
@ -1731,6 +1751,7 @@ void Stories::savedLoadMore(PeerId peerId) {
const auto ids = int(saved.ids.list.size()); const auto ids = int(saved.ids.list.size());
saved.loaded = data.vstories().v.empty(); saved.loaded = data.vstories().v.empty();
saved.total = saved.loaded ? ids : std::max(saved.total, ids); saved.total = saved.loaded ? ids : std::max(saved.total, ids);
setPinnedToTop(peerId, std::move(pinnedToTop));
_savedChanged.fire_copy(peerId); _savedChanged.fire_copy(peerId);
}).fail([=] { }).fail([=] {
auto &saved = _saved[peerId]; auto &saved = _saved[peerId];
@ -1741,6 +1762,33 @@ void Stories::savedLoadMore(PeerId peerId) {
}).send(); }).send();
} }
void Stories::setPinnedToTop(
PeerId peerId,
std::vector<StoryId> &&pinnedToTop) {
const auto i = _saved.find(peerId);
if (i == end(_saved) && pinnedToTop.empty()) {
return;
}
auto &saved = (i == end(_saved)) ? _saved[peerId] : i->second;
if (saved.ids.pinnedToTop != pinnedToTop) {
for (const auto id : saved.ids.pinnedToTop) {
if (!ranges::contains(pinnedToTop, id)) {
if (const auto maybeStory = lookup({ peerId, id })) {
(*maybeStory)->setPinnedToTop(false);
}
}
}
for (const auto id : pinnedToTop) {
if (!ranges::contains(saved.ids.pinnedToTop, id)) {
if (const auto maybeStory = lookup({ peerId, id })) {
(*maybeStory)->setPinnedToTop(true);
}
}
}
saved.ids.pinnedToTop = std::move(pinnedToTop);
}
}
void Stories::deleteList(const std::vector<FullStoryId> &ids) { void Stories::deleteList(const std::vector<FullStoryId> &ids) {
if (ids.empty()) { if (ids.empty()) {
return; return;
@ -1764,9 +1812,9 @@ void Stories::deleteList(const std::vector<FullStoryId> &ids) {
}).send(); }).send();
} }
void Stories::togglePinnedList( void Stories::toggleInProfileList(
const std::vector<FullStoryId> &ids, const std::vector<FullStoryId> &ids,
bool pinned) { bool inProfile) {
if (ids.empty()) { if (ids.empty()) {
return; return;
} }
@ -1785,7 +1833,7 @@ void Stories::togglePinnedList(
api->request(MTPstories_TogglePinned( api->request(MTPstories_TogglePinned(
peer->input, peer->input,
MTP_vector<MTPint>(list), MTP_vector<MTPint>(list),
MTP_bool(pinned) MTP_bool(inProfile)
)).done([=](const MTPVector<MTPint> &result) { )).done([=](const MTPVector<MTPint> &result) {
const auto peerId = peer->id; const auto peerId = peer->id;
auto &saved = _saved[peerId]; auto &saved = _saved[peerId];
@ -1799,8 +1847,8 @@ void Stories::togglePinnedList(
for (const auto &id : result.v) { for (const auto &id : result.v) {
if (const auto maybeStory = lookup({ peerId, id.v })) { if (const auto maybeStory = lookup({ peerId, id.v })) {
const auto story = *maybeStory; const auto story = *maybeStory;
story->setPinned(pinned); story->setInProfile(inProfile);
if (pinned) { if (inProfile) {
const auto add = loaded || (id.v >= lastId); const auto add = loaded || (id.v >= lastId);
if (!add) { if (!add) {
dirty = true; dirty = true;
@ -1828,6 +1876,75 @@ void Stories::togglePinnedList(
}).send(); }).send();
} }
bool Stories::canTogglePinnedList(
const std::vector<FullStoryId> &ids,
bool pin) const {
Expects(!ids.empty());
if (!pin) {
return true;
}
const auto peerId = ids.front().peer;
const auto i = _saved.find(peerId);
if (i == end(_saved)) {
return false;
}
auto &already = i->second.ids.pinnedToTop;
auto count = int(already.size());
for (const auto &id : ids) {
if (!ranges::contains(already, id.story)) {
++count;
}
}
return count <= maxPinnedCount();
}
int Stories::maxPinnedCount() const {
const auto appConfig = &_owner->session().appConfig();
return appConfig->get<int>(u"stories_pinned_to_top_count_max"_q, 3);
}
void Stories::togglePinnedList(
const std::vector<FullStoryId> &ids,
bool pin) {
if (ids.empty()) {
return;
}
const auto peerId = ids.front().peer;
auto &saved = _saved[peerId];
auto list = QVector<MTPint>();
list.reserve(maxPinnedCount());
for (const auto &id : saved.ids.pinnedToTop) {
if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) {
list.push_back(MTP_int(id));
}
}
if (pin) {
auto copy = ids;
ranges::sort(copy, ranges::greater());
for (const auto &id : copy) {
if (id.peer == peerId
&& !ranges::contains(saved.ids.pinnedToTop, id.story)) {
list.push_back(MTP_int(id.story));
}
}
}
const auto api = &_owner->session().api();
const auto peer = session().data().peer(peerId);
api->request(MTPstories_TogglePinnedToTop(
peer->input,
MTP_vector<MTPint>(list)
)).done([=] {
setPinnedToTop(peerId, list
| ranges::views::transform(&MTPint::v)
| ranges::to_vector);
_savedChanged.fire_copy(peerId);
}).send();
}
void Stories::report( void Stories::report(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
FullStoryId id, FullStoryId id,

View file

@ -33,12 +33,15 @@ class StoryPreload;
struct StoriesIds { struct StoriesIds {
base::flat_set<StoryId, std::greater<>> list; base::flat_set<StoryId, std::greater<>> list;
std::vector<StoryId> pinnedToTop;
friend inline bool operator==( friend inline bool operator==(
const StoriesIds&, const StoriesIds&,
const StoriesIds&) = default; const StoriesIds&) = default;
}; };
[[nodiscard]] std::vector<StoryId> RespectingPinned(const StoriesIds &ids);
struct StoriesSourceInfo { struct StoriesSourceInfo {
PeerId id = 0; PeerId id = 0;
TimeId last = 0; TimeId last = 0;
@ -131,7 +134,7 @@ public:
explicit Stories(not_null<Session*> owner); explicit Stories(not_null<Session*> owner);
~Stories(); ~Stories();
static constexpr auto kPinnedToastDuration = 4 * crl::time(1000); static constexpr auto kInProfileToastDuration = 4 * crl::time(1000);
[[nodiscard]] Session &owner() const; [[nodiscard]] Session &owner() const;
[[nodiscard]] Main::Session &session() const; [[nodiscard]] Main::Session &session() const;
@ -205,7 +208,14 @@ public:
void savedLoadMore(PeerId peerId); void savedLoadMore(PeerId peerId);
void deleteList(const std::vector<FullStoryId> &ids); void deleteList(const std::vector<FullStoryId> &ids);
void togglePinnedList(const std::vector<FullStoryId> &ids, bool pinned); void toggleInProfileList(
const std::vector<FullStoryId> &ids,
bool inProfile);
[[nodiscard]] bool canTogglePinnedList(
const std::vector<FullStoryId> &ids,
bool pin) const;
[[nodiscard]] int maxPinnedCount() const;
void togglePinnedList(const std::vector<FullStoryId> &ids, bool pin);
void report( void report(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
FullStoryId id, FullStoryId id,
@ -312,6 +322,9 @@ private:
void notifySourcesChanged(StorySourcesList list); void notifySourcesChanged(StorySourcesList list);
void pushHiddenCountsToFolder(); void pushHiddenCountsToFolder();
void setPinnedToTop(
PeerId peerId,
std::vector<StoryId> &&pinnedToTop);
[[nodiscard]] int pollingInterval( [[nodiscard]] int pollingInterval(
const PollingSettings &settings) const; const PollingSettings &settings) const;

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