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
|
@ -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:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
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:
|
||||
if: github.event_name != 'issue_comment'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lee-dohm/no-response@v0.5.0
|
1
.github/workflows/linux.yml
vendored
|
@ -49,7 +49,6 @@ jobs:
|
|||
defines:
|
||||
- ""
|
||||
- "DESKTOP_APP_DISABLE_X11_INTEGRATION"
|
||||
- "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION"
|
||||
|
||||
env:
|
||||
UPLOAD_ARTIFACT: "true"
|
||||
|
|
4
.github/workflows/mac_packaged.yml
vendored
|
@ -49,7 +49,7 @@ jobs:
|
|||
|
||||
env:
|
||||
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"
|
||||
ONLY_CACHE: "false"
|
||||
MANUAL_CACHING: "1"
|
||||
|
@ -69,7 +69,7 @@ jobs:
|
|||
run: |
|
||||
brew update
|
||||
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
|
||||
|
||||
xcodebuild -version > CACHE_KEY.txt
|
||||
|
|
16
.github/workflows/needs-user-action.yml
vendored
Normal 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
|
2
.github/workflows/snap.yml
vendored
|
@ -64,7 +64,7 @@ jobs:
|
|||
uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8
|
||||
|
||||
- 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.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
|
|
16
.github/workflows/waiting-for-answer.yml
vendored
Normal 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
|
@ -82,12 +82,6 @@
|
|||
[submodule "Telegram/ThirdParty/dispatch"]
|
||||
path = Telegram/ThirdParty/dispatch
|
||||
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"]
|
||||
path = Telegram/ThirdParty/kimageformats
|
||||
url = https://github.com/KDE/kimageformats.git
|
||||
|
@ -97,9 +91,6 @@
|
|||
[submodule "Telegram/ThirdParty/cld3"]
|
||||
path = Telegram/ThirdParty/cld3
|
||||
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"]
|
||||
path = Telegram/ThirdParty/libprisma
|
||||
url = https://github.com/desktop-app/libprisma.git
|
||||
|
|
|
@ -62,7 +62,7 @@ if (NOT DESKTOP_APP_USE_PACKAGED)
|
|||
if (WIN32)
|
||||
set(qt_version 5.15.13)
|
||||
elseif (APPLE)
|
||||
set(qt_version 6.2.7)
|
||||
set(qt_version 6.2.8)
|
||||
endif()
|
||||
endif()
|
||||
include(cmake/external/qt/package.cmake)
|
||||
|
|
|
@ -342,6 +342,8 @@ PRIVATE
|
|||
boxes/local_storage_box.h
|
||||
boxes/max_invite_box.cpp
|
||||
boxes/max_invite_box.h
|
||||
boxes/moderate_messages_box.cpp
|
||||
boxes/moderate_messages_box.h
|
||||
boxes/peer_list_box.cpp
|
||||
boxes/peer_list_box.h
|
||||
boxes/peer_list_controllers.cpp
|
||||
|
@ -529,10 +531,14 @@ PRIVATE
|
|||
data/business/data_business_info.h
|
||||
data/business/data_shortcut_messages.cpp
|
||||
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.h
|
||||
data/components/sponsored_messages.cpp
|
||||
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.h
|
||||
data/notify/data_peer_notify_settings.cpp
|
||||
|
@ -677,6 +683,18 @@ PRIVATE
|
|||
data/data_wall_paper.h
|
||||
data/data_web_page.cpp
|
||||
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.h
|
||||
dialogs/dialogs_indexed_list.cpp
|
||||
|
@ -699,16 +717,6 @@ PRIVATE
|
|||
dialogs/dialogs_search_tags.h
|
||||
dialogs/dialogs_widget.cpp
|
||||
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.h
|
||||
editor/controllers/controllers.h
|
||||
|
@ -1268,11 +1276,6 @@ PRIVATE
|
|||
payments/payments_checkout_process.h
|
||||
payments/payments_form.cpp
|
||||
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.h
|
||||
platform/linux/launcher_linux.cpp
|
||||
|
@ -1625,16 +1628,6 @@ if (NOT build_winstore)
|
|||
)
|
||||
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)
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/mac/mac_iconv_helper.c
|
||||
|
@ -1774,19 +1767,6 @@ else()
|
|||
desktop-app::external_xcb
|
||||
)
|
||||
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()
|
||||
|
||||
if (build_macstore)
|
||||
|
|
BIN
Telegram/Resources/animations/noresults.tgs
Normal file
BIN
Telegram/Resources/animations/search.tgs
Normal file
BIN
Telegram/Resources/icons/chat/live_location_long.png
Normal file
After Width: | Height: | Size: 416 B |
BIN
Telegram/Resources/icons/chat/live_location_long@2x.png
Normal file
After Width: | Height: | Size: 913 B |
BIN
Telegram/Resources/icons/chat/live_location_long@3x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/dialogs/dialogs_pinned_shadow.png
Normal file
After Width: | Height: | Size: 754 B |
BIN
Telegram/Resources/icons/dialogs/dialogs_pinned_shadow@2x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/dialogs/dialogs_pinned_shadow@3x.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
Telegram/Resources/icons/menu/fonts.png
Normal file
After Width: | Height: | Size: 563 B |
BIN
Telegram/Resources/icons/menu/fonts@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/menu/fonts@3x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
|
@ -134,9 +134,32 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
|
|||
.page-slide {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin-left: 0%;
|
||||
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-right {
|
||||
pointer-events: none;
|
||||
|
@ -148,7 +171,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
|
|||
margin-left: 100%;
|
||||
}
|
||||
article {
|
||||
padding-bottom: 12px;
|
||||
padding-bottom: 40px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
|
@ -893,6 +916,9 @@ section.related a.related-link:after {
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
section.related a.related-link:last-child:after {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
section.related .related-link-url {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
|
@ -1027,6 +1053,9 @@ section.channel > a > h4 {
|
|||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.media-outer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.photo-wrap,
|
||||
.video-wrap {
|
||||
width: 100%;
|
||||
|
|
|
@ -26,7 +26,7 @@ var IV = {
|
|||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
if (!target || !target.hasAttribute('href')) {
|
||||
if (!target || (context === '' && !target.hasAttribute('href'))) {
|
||||
return;
|
||||
}
|
||||
var base = document.createElement('A');
|
||||
|
@ -413,9 +413,12 @@ var IV = {
|
|||
var article = function (el) {
|
||||
return el.getElementsByTagName('article')[0];
|
||||
};
|
||||
var from = article(IV.findPageScroll());
|
||||
var to = article(IV.makeScrolledContent(data.html));
|
||||
morphdom(from, to, {
|
||||
var footer = function (el) {
|
||||
return el.getElementsByClassName('page-footer')[0];
|
||||
};
|
||||
var from = IV.findPageScroll();
|
||||
var to = IV.makeScrolledContent(data.html);
|
||||
morphdom(article(from), article(to), {
|
||||
onBeforeElUpdated: function (fromEl, toEl) {
|
||||
if (fromEl.classList.contains('video')
|
||||
&& toEl.classList.contains('video')
|
||||
|
@ -439,6 +442,7 @@ var IV = {
|
|||
return !fromEl.isEqualNode(toEl);
|
||||
}
|
||||
});
|
||||
morphdom(footer(from), footer(to));
|
||||
IV.initMedia();
|
||||
eval(data.js);
|
||||
},
|
||||
|
@ -477,9 +481,7 @@ var IV = {
|
|||
var result = document.createElement('div');
|
||||
result.className = 'page-scroll';
|
||||
result.tabIndex = '-1';
|
||||
result.innerHTML = '<div class="page-slide"><article>'
|
||||
+ html
|
||||
+ '</article></div>';
|
||||
result.innerHTML = html.trim();
|
||||
result.onscroll = IV.frameScrolled;
|
||||
return result;
|
||||
},
|
||||
|
|
|
@ -730,6 +730,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_settings_angle_backend_d3d11on12" = "D3D11on12";
|
||||
"lng_settings_angle_backend_opengl" = "OpenGL";
|
||||
"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_disable_filtering" = "Disable filtering";
|
||||
"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_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_font_family" = "Font family";
|
||||
|
||||
"lng_settings_color_title" = "Color preview";
|
||||
"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_some_title" = "Only allow these 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_link" = "create your own";
|
||||
"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_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_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**.";
|
||||
|
@ -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_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_subtitle_sponsored" = "Ads in Channels";
|
||||
"lng_business_button_sponsored" = "Do Not Hide Ads";
|
||||
"lng_business_about_sponsored" = "As a Premium subscriber, you don’t 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_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_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_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_and_leave" = "Report spam and leave";
|
||||
"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_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_video" = "Save video";
|
||||
"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_delete" = "Delete";
|
||||
"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_photos_all" = "View all photos";
|
||||
"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_giftcode" = "Open";
|
||||
"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_title" = "What are sponsored messages?";
|
||||
|
@ -5073,6 +5120,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_iv_share" = "Share";
|
||||
"lng_iv_join_channel" = "Join";
|
||||
"lng_iv_window_title" = "Instant View";
|
||||
"lng_iv_wrong_layout" = "Wrong layout?";
|
||||
|
||||
"lng_limit_download_title" = "Download speed limited";
|
||||
"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_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
|
||||
|
||||
"lng_wnd_choose_program_menu" = "Choose Default Program...";
|
||||
|
|
|
@ -24,5 +24,7 @@
|
|||
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
|
||||
<file alias="collectible_username.tgs">../../animations/collectible_username.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>
|
||||
</RCC>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="4.16.8.0" />
|
||||
Version="5.0.1.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
|
|
@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
|||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 4,16,8,0
|
||||
PRODUCTVERSION 4,16,8,0
|
||||
FILEVERSION 5,0,1,0
|
||||
PRODUCTVERSION 5,0,1,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
|
@ -62,10 +62,10 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Radolyn Labs"
|
||||
VALUE "FileDescription", "AyuGram Desktop"
|
||||
VALUE "FileVersion", "4.16.8.0"
|
||||
VALUE "FileVersion", "5.0.1.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "AyuGram Desktop"
|
||||
VALUE "ProductVersion", "4.16.8.0"
|
||||
VALUE "ProductVersion", "5.0.1.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 4,16,8,0
|
||||
PRODUCTVERSION 4,16,8,0
|
||||
FILEVERSION 5,0,1,0
|
||||
PRODUCTVERSION 5,0,1,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
|
@ -53,10 +53,10 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Radolyn Labs"
|
||||
VALUE "FileDescription", "AyuGram Desktop Updater"
|
||||
VALUE "FileVersion", "4.16.8.0"
|
||||
VALUE "FileVersion", "5.0.1.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "AyuGram Desktop"
|
||||
VALUE "ProductVersion", "4.16.8.0"
|
||||
VALUE "ProductVersion", "5.0.1.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
@ -212,7 +212,7 @@ void ApplyBotsList(
|
|||
}
|
||||
|
||||
[[nodiscard]] ChatParticipants::Channels ParseSimilar(
|
||||
not_null<ChannelData*> channel,
|
||||
not_null<Main::Session*> session,
|
||||
const MTPmessages_Chats &chats) {
|
||||
auto result = ChatParticipants::Channels();
|
||||
std::vector<not_null<ChannelData*>>();
|
||||
|
@ -220,13 +220,13 @@ void ApplyBotsList(
|
|||
const auto &list = data.vchats().v;
|
||||
result.list.reserve(list.size());
|
||||
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()) {
|
||||
result.list.push_back(channel);
|
||||
}
|
||||
}
|
||||
if constexpr (MTPDmessages_chatsSlice::Is<decltype(data)>()) {
|
||||
if (channel->session().premiumPossible()) {
|
||||
if (session->premiumPossible()) {
|
||||
result.more = data.vcount().v - data.vchats().v.size();
|
||||
}
|
||||
}
|
||||
|
@ -234,6 +234,12 @@ void ApplyBotsList(
|
|||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] ChatParticipants::Channels ParseSimilar(
|
||||
not_null<ChannelData*> channel,
|
||||
const MTPmessages_Chats &chats) {
|
||||
return ParseSimilar(&channel->session(), chats);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatParticipant::ChatParticipant(
|
||||
|
@ -351,7 +357,8 @@ QString ChatParticipant::rank() const {
|
|||
}
|
||||
|
||||
ChatParticipants::ChatParticipants(not_null<ApiWrap*> api)
|
||||
: _api(&api->instance()) {
|
||||
: _session(&api->session())
|
||||
, _api(&api->instance()) {
|
||||
}
|
||||
|
||||
void ChatParticipants::requestForAdd(
|
||||
|
@ -585,6 +592,33 @@ ChatParticipants::Parsed ChatParticipants::ParseRecent(
|
|||
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) {
|
||||
if (_selfParticipantRequests.contains(channel)) {
|
||||
return;
|
||||
|
@ -730,8 +764,11 @@ void ChatParticipants::loadSimilarChannels(not_null<ChannelData*> channel) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
using Flag = MTPchannels_GetChannelRecommendations::Flag;
|
||||
_similar[channel].requestId = _api.request(
|
||||
MTPchannels_GetChannelRecommendations(channel->inputChannel)
|
||||
MTPchannels_GetChannelRecommendations(
|
||||
MTP_flags(Flag::f_channel),
|
||||
channel->inputChannel)
|
||||
).done([=](const MTPmessages_Chats &result) {
|
||||
auto &similar = _similar[channel];
|
||||
similar.requestId = 0;
|
||||
|
@ -766,4 +803,29 @@ auto ChatParticipants::similarLoaded() const
|
|||
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
|
||||
|
|
|
@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
class ApiWrap;
|
||||
class ChannelData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
@ -96,6 +100,13 @@ public:
|
|||
static Parsed ParseRecent(
|
||||
not_null<ChannelData*> channel,
|
||||
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(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
|
@ -134,12 +145,18 @@ public:
|
|||
[[nodiscard]] auto similarLoaded() const
|
||||
-> rpl::producer<not_null<ChannelData*>>;
|
||||
|
||||
void loadRecommendations();
|
||||
[[nodiscard]] const Channels &recommendations() const;
|
||||
[[nodiscard]] rpl::producer<> recommendationsLoaded() const;
|
||||
|
||||
private:
|
||||
struct SimilarChannels {
|
||||
Channels channels;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
|
||||
|
@ -165,6 +182,9 @@ private:
|
|||
base::flat_map<not_null<ChannelData*>, SimilarChannels> _similar;
|
||||
rpl::event_stream<not_null<ChannelData*>> _similarLoaded;
|
||||
|
||||
SimilarChannels _recommendations;
|
||||
rpl::variable<bool> _recommendationsLoaded = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Api
|
||||
|
|
|
@ -62,6 +62,10 @@ void ConfirmPhone::resolve(
|
|||
return bad("FirebaseSms");
|
||||
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
|
||||
return bad("EmailCode");
|
||||
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
|
||||
return bad("SmsWord");
|
||||
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
|
||||
return bad("SmsPhrase");
|
||||
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
|
||||
return bad("SetUpEmailRequired");
|
||||
});
|
||||
|
|
|
@ -602,6 +602,41 @@ bool PremiumGiftCodeOptions::giveawayGiftsPurchaseAvailable() const {
|
|||
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(
|
||||
not_null<PeerData*> peer,
|
||||
History *maybeHistory) {
|
||||
|
|
|
@ -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 {
|
||||
Unknown,
|
||||
Yes,
|
||||
|
|
|
@ -760,14 +760,14 @@ rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
|
|||
channel()->inputChannel
|
||||
)).done([=](const MTPstats_BroadcastRevenueStats &result) {
|
||||
const auto &data = result.data();
|
||||
|
||||
const auto &balances = data.vbalances().data();
|
||||
_data = Data::EarnStatistics{
|
||||
.topHoursGraph = StatisticalGraphFromTL(
|
||||
data.vtop_hours_graph()),
|
||||
.revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()),
|
||||
.currentBalance = data.vcurrent_balance().v,
|
||||
.availableBalance = data.vavailable_balance().v,
|
||||
.overallRevenue = data.voverall_revenue().v,
|
||||
.currentBalance = balances.vcurrent_balance().v,
|
||||
.availableBalance = balances.vavailable_balance().v,
|
||||
.overallRevenue = balances.voverall_revenue().v,
|
||||
.usdRate = data.vusd_rate().v,
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "mtproto/mtproto_dc_options.h"
|
||||
#include "data/business/data_shortcut_messages.h"
|
||||
#include "data/components/scheduled_messages.h"
|
||||
#include "data/components/top_peers.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_saved_messages.h"
|
||||
|
@ -1585,6 +1586,11 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
|||
} else {
|
||||
if (existing) {
|
||||
existing->destroy();
|
||||
} else {
|
||||
// Not the server-side date, but close enough.
|
||||
session().topPeers().increment(
|
||||
local->history()->peer,
|
||||
local->date());
|
||||
}
|
||||
local->setRealId(d.vid().v);
|
||||
}
|
||||
|
|
|
@ -940,7 +940,7 @@ int BackgroundPreviewBox::textsTop() const {
|
|||
- st::historyPaddingBottom
|
||||
- (_service ? _service->height() : 0)
|
||||
- _text1->height()
|
||||
- (forChannel() ? _text2->height() : 0);
|
||||
- (forChannel() ? 0 : _text2->height());
|
||||
}
|
||||
|
||||
QRect BackgroundPreviewBox::radialRect() const {
|
||||
|
|
|
@ -546,7 +546,7 @@ rightsToggle: Toggle(defaultToggle) {
|
|||
vsize: 0px;
|
||||
vshift: 1px;
|
||||
stroke: 2px;
|
||||
duration: 120;
|
||||
duration: universalDuration;
|
||||
}
|
||||
|
||||
rightsButton: SettingsButton(defaultSettingsButton) {
|
||||
|
@ -691,6 +691,10 @@ createPollOptionField: InputField(createPollField) {
|
|||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
||||
heightMax: 68px;
|
||||
}
|
||||
createPollOptionFieldPremium: InputField(createPollOptionField) {
|
||||
textMargins: margins(22px, 11px, 68px, 11px);
|
||||
}
|
||||
createPollOptionFieldPremiumEmojiPosition: point(15px, -1px);
|
||||
createPollSolutionField: InputField(createPollField) {
|
||||
textMargins: margins(0px, 4px, 0px, 4px);
|
||||
border: 1px;
|
||||
|
@ -704,7 +708,7 @@ createPollOptionRemove: CrossButton {
|
|||
cross: CrossAnimation {
|
||||
size: 22px;
|
||||
skip: 6px;
|
||||
stroke: 1.;
|
||||
stroke: 1.5;
|
||||
minScale: 0.3;
|
||||
}
|
||||
crossFg: boxTitleCloseFg;
|
||||
|
@ -718,6 +722,7 @@ createPollOptionRemove: CrossButton {
|
|||
}
|
||||
}
|
||||
createPollOptionRemovePosition: point(11px, 9px);
|
||||
createPollOptionEmojiPositionSkip: 4px;
|
||||
createPollWarning: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
palette: TextPalette(defaultTextPalette) {
|
||||
|
@ -1074,3 +1079,23 @@ collectibleBox: Box(defaultBox) {
|
|||
buttonHeight: 36px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,33 +7,41 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "boxes/create_poll_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/widgets/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 "base/call_delayed.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.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 "base/unique_qptr.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/random.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "menu/menu_send.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 "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h" // defaultComposeFiles.
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
@ -46,12 +54,107 @@ constexpr auto kSolutionLimit = 200;
|
|||
constexpr auto kWarnSolutionLimit = 60;
|
||||
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 {
|
||||
public:
|
||||
Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::BoxContent*> box,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
not_null<Window::SessionController*> controller,
|
||||
ChatHelpers::TabbedPanel *emojiPanel,
|
||||
bool chooseCorrectEnabled);
|
||||
|
||||
[[nodiscard]] bool hasOptions() const;
|
||||
|
@ -140,9 +243,10 @@ private:
|
|||
[[nodiscard]] auto createChooseCorrectGroup()
|
||||
-> std::shared_ptr<Ui::RadiobuttonGroup>;
|
||||
|
||||
not_null<QWidget*> _outer;
|
||||
not_null<Ui::BoxContent*> _box;
|
||||
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;
|
||||
int _position = 0;
|
||||
std::vector<std::unique_ptr<Option>> _list;
|
||||
|
@ -154,6 +258,7 @@ private:
|
|||
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
|
||||
rpl::event_stream<> _backspaceInFront;
|
||||
rpl::event_stream<> _tabbed;
|
||||
rpl::lifetime _emojiPanelLifetime;
|
||||
|
||||
};
|
||||
|
||||
|
@ -193,8 +298,9 @@ not_null<Ui::FlatLabel*> CreateWarningLabel(
|
|||
if (value >= 0) {
|
||||
result->setText(QString::number(value));
|
||||
} else {
|
||||
constexpr auto kMinus = QChar(0x2212);
|
||||
result->setMarkedText(Ui::Text::Colorized(
|
||||
QString::number(value)));
|
||||
kMinus + QString::number(std::abs(value))));
|
||||
}
|
||||
result->setVisible(shown);
|
||||
}));
|
||||
|
@ -223,7 +329,9 @@ Options::Option::Option(
|
|||
, _field(
|
||||
Ui::CreateChild<Ui::InputField>(
|
||||
_content.get(),
|
||||
st::createPollOptionField,
|
||||
session->user()->isPremium()
|
||||
? st::createPollOptionFieldPremium
|
||||
: st::createPollOptionField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_polls_create_option_add())) {
|
||||
InitField(outer, _field, session);
|
||||
|
@ -299,7 +407,7 @@ void Options::Option::createRemove() {
|
|||
const auto remove = Ui::CreateChild<Ui::CrossButton>(
|
||||
field.get(),
|
||||
st::createPollOptionRemove);
|
||||
remove->hide(anim::type::instant);
|
||||
remove->show(anim::type::instant);
|
||||
|
||||
const auto toggle = 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.
|
||||
*toggle = !field->getLastText().isEmpty();
|
||||
}, field->lifetime());
|
||||
#if 0
|
||||
rpl::combine(
|
||||
toggle->value(),
|
||||
_removeAlways->value(),
|
||||
|
@ -316,6 +425,7 @@ void Options::Option::createRemove() {
|
|||
) | rpl::start_with_next([=](bool shown) {
|
||||
remove->toggle(shown, anim::type::normal);
|
||||
}, remove->lifetime());
|
||||
#endif
|
||||
|
||||
field->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
|
@ -456,10 +566,16 @@ void Options::Option::removePlaceholder() const {
|
|||
PollAnswer Options::Option::toPollAnswer(int index) const {
|
||||
Expects(index >= 0 && index < kMaxOptionsCount);
|
||||
|
||||
const auto text = field()->getTextWithTags();
|
||||
|
||||
auto result = PollAnswer{
|
||||
field()->getLastText().trimmed(),
|
||||
QByteArray(1, ('0' + index))
|
||||
TextWithEntities{
|
||||
.text = text.text,
|
||||
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
|
||||
},
|
||||
QByteArray(1, ('0' + index)),
|
||||
};
|
||||
TextUtilities::Trim(result.text);
|
||||
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
|
||||
return result;
|
||||
}
|
||||
|
@ -469,13 +585,15 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
|
|||
}
|
||||
|
||||
Options::Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::BoxContent*> box,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
not_null<Window::SessionController*> controller,
|
||||
ChatHelpers::TabbedPanel *emojiPanel,
|
||||
bool chooseCorrectEnabled)
|
||||
: _outer(outer)
|
||||
: _box(box)
|
||||
, _container(container)
|
||||
, _session(session)
|
||||
, _controller(controller)
|
||||
, _emojiPanel(emojiPanel)
|
||||
, _chooseCorrectGroup(chooseCorrectEnabled
|
||||
? createChooseCorrectGroup()
|
||||
: nullptr)
|
||||
|
@ -645,12 +763,40 @@ void Options::addEmptyOption() {
|
|||
(*(_list.end() - 2))->toggleRemoveAlways(true);
|
||||
}
|
||||
_list.push_back(std::make_unique<Option>(
|
||||
_outer,
|
||||
_box,
|
||||
_container,
|
||||
_session,
|
||||
&_controller->session(),
|
||||
_position + _list.size() + _destroyed.size(),
|
||||
_chooseCorrectGroup));
|
||||
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(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto index = findField(field);
|
||||
|
@ -697,7 +843,7 @@ void Options::addEmptyOption() {
|
|||
});
|
||||
|
||||
_list.back()->removeClicks(
|
||||
) | rpl::take(1) | rpl::start_with_next([=] {
|
||||
) | rpl::start_with_next([=] {
|
||||
Ui::PostponeCall(crl::guard(field, [=] {
|
||||
Expects(!_list.empty());
|
||||
|
||||
|
@ -789,19 +935,63 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion(
|
|||
using namespace Settings;
|
||||
|
||||
const auto session = &_controller->session();
|
||||
const auto isPremium = session->user()->isPremium();
|
||||
Ui::AddSubsectionTitle(container, tr::lng_polls_create_question());
|
||||
|
||||
const auto question = container->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::createPollField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
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);
|
||||
question->setMaxLength(kQuestionLimit + kErrorLimit);
|
||||
question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
|
||||
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(
|
||||
container,
|
||||
question,
|
||||
|
@ -910,9 +1100,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
st::defaultSubsectionTitle),
|
||||
st::createPollFieldTitlePadding);
|
||||
const auto options = lifetime().make_state<Options>(
|
||||
getDelegate()->outerContainer(),
|
||||
this,
|
||||
container,
|
||||
&_controller->session(),
|
||||
_controller,
|
||||
_emojiPanel ? _emojiPanel.get() : nullptr,
|
||||
(_chosen & PollData::Flag::Quiz));
|
||||
auto limit = options->usedCount() | rpl::after_next([=](int count) {
|
||||
setCloseByEscape(!count);
|
||||
|
@ -1029,9 +1220,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
};
|
||||
|
||||
const auto collectResult = [=] {
|
||||
const auto textWithTags = question->getTextWithTags();
|
||||
using Flag = PollData::Flag;
|
||||
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();
|
||||
const auto solutionWithTags = quiz->checked()
|
||||
? solution->getTextWithAppliedMarkdown()
|
||||
|
|
|
@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
struct PollData;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
@ -72,6 +76,7 @@ private:
|
|||
const PollData::Flags _disabled = PollData::Flags();
|
||||
const Api::SendType _sendType = Api::SendType();
|
||||
const SendMenu::Type _sendMenuType;
|
||||
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
Fn<void()> _setInnerFocus;
|
||||
Fn<rpl::producer<bool>()> _dataIsValidValue;
|
||||
rpl::event_stream<Result> _submitRequests;
|
||||
|
|
|
@ -379,13 +379,7 @@ auto DeleteMessagesBox::revokeText(not_null<PeerData*> peer) const
|
|||
return result;
|
||||
}
|
||||
|
||||
const auto items = ranges::views::all(
|
||||
_ids
|
||||
) | ranges::views::transform([&](FullMsgId id) {
|
||||
return peer->owner().message(id);
|
||||
}) | ranges::views::filter([](HistoryItem *item) {
|
||||
return (item != nullptr);
|
||||
}) | ranges::to_vector;
|
||||
const auto items = peer->owner().idsToItems(_ids);
|
||||
|
||||
if (items.size() != _ids.size()) {
|
||||
// We don't have information about all messages.
|
||||
|
|
856
Telegram/SourceFiles/boxes/moderate_messages_box.cpp
Normal 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);
|
||||
}
|
23
Telegram/SourceFiles/boxes/moderate_messages_box.h
Normal 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);
|
|
@ -734,6 +734,10 @@ auto PeerListRow::generateNameWords() const
|
|||
return peer()->nameWords();
|
||||
}
|
||||
|
||||
const style::PeerListItem &PeerListRow::computeSt(
|
||||
const style::PeerListItem &st) const {
|
||||
return st;
|
||||
}
|
||||
|
||||
void PeerListRow::invalidatePixmapsCache() {
|
||||
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) {
|
||||
_ripple->paint(p, x, y, outerWidth);
|
||||
_ripple->paint(p, x, y, outerWidth, &st.button.ripple.color->c);
|
||||
if (_ripple->empty()) {
|
||||
_ripple.reset();
|
||||
}
|
||||
|
@ -1689,7 +1698,9 @@ crl::time PeerListContent::paintRow(
|
|||
const auto row = getRow(index);
|
||||
Assert(row != nullptr);
|
||||
|
||||
row->lazyInitialize(_st.item);
|
||||
const auto &st = row->computeSt(_st.item);
|
||||
|
||||
row->lazyInitialize(st);
|
||||
const auto outerWidth = width();
|
||||
|
||||
auto refreshStatusAt = row->refreshStatusTime();
|
||||
|
@ -1717,8 +1728,8 @@ crl::time PeerListContent::paintRow(
|
|||
|
||||
const auto opacity = row->opacity();
|
||||
const auto &bg = selected
|
||||
? _st.item.button.textBgOver
|
||||
: _st.item.button.textBg;
|
||||
? st.button.textBgOver
|
||||
: st.button.textBg;
|
||||
if (opacity < 1.) {
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
@ -1729,36 +1740,37 @@ crl::time PeerListContent::paintRow(
|
|||
});
|
||||
|
||||
p.fillRect(0, 0, outerWidth, _rowHeight, bg);
|
||||
row->paintRipple(p, 0, 0, outerWidth);
|
||||
row->paintRipple(p, st, 0, 0, outerWidth);
|
||||
row->paintUserpic(
|
||||
p,
|
||||
_st.item,
|
||||
_st.item.photoPosition.x(),
|
||||
_st.item.photoPosition.y(),
|
||||
st,
|
||||
st.photoPosition.x(),
|
||||
st.photoPosition.y(),
|
||||
outerWidth);
|
||||
|
||||
p.setPen(st::contactsNameFg);
|
||||
|
||||
const auto skipRight = _st.item.photoPosition.x();
|
||||
const auto skipRight = st.photoPosition.x();
|
||||
const auto rightActionSize = row->rightActionSize();
|
||||
const auto rightActionMargins = rightActionSize.isEmpty()
|
||||
? QMargins()
|
||||
: row->rightActionMargins();
|
||||
const auto &name = row->name();
|
||||
const auto namex = _st.item.namePosition.x();
|
||||
const auto namey = _st.item.namePosition.y();
|
||||
const auto namePosition = st.namePosition;
|
||||
const auto namex = namePosition.x();
|
||||
const auto namey = namePosition.y();
|
||||
auto namew = outerWidth - namex - skipRight;
|
||||
if (!rightActionSize.isEmpty()
|
||||
&& (namey < rightActionMargins.top() + rightActionSize.height())
|
||||
&& (namey + _st.item.nameStyle.font->height
|
||||
&& (namey + st.nameStyle.font->height
|
||||
> rightActionMargins.top())) {
|
||||
namew -= rightActionMargins.left()
|
||||
+ rightActionSize.width()
|
||||
+ rightActionMargins.right()
|
||||
- skipRight;
|
||||
}
|
||||
const auto statusx = _st.item.statusPosition.x();
|
||||
const auto statusy = _st.item.statusPosition.y();
|
||||
const auto statusx = st.statusPosition.x();
|
||||
const auto statusy = st.statusPosition.y();
|
||||
auto statusw = outerWidth - statusx - skipRight;
|
||||
if (!rightActionSize.isEmpty()
|
||||
&& (statusy < rightActionMargins.top() + rightActionSize.height())
|
||||
|
@ -1780,7 +1792,7 @@ crl::time PeerListContent::paintRow(
|
|||
width(),
|
||||
selected);
|
||||
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());
|
||||
|
||||
p.setFont(st::contactsStatusFont);
|
||||
|
@ -1799,17 +1811,17 @@ crl::time PeerListContent::paintRow(
|
|||
if (highlightedWidth > availableWidth) {
|
||||
highlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth);
|
||||
}
|
||||
p.setPen(_st.item.statusFgActive);
|
||||
p.setPen(st.statusFgActive);
|
||||
p.drawTextLeft(statusx, statusy, width(), highlightedPart);
|
||||
} else {
|
||||
grayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth);
|
||||
p.setPen(_st.item.statusFgActive);
|
||||
p.setPen(st.statusFgActive);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
row->paintStatusText(p, _st.item, statusx, statusy, statusw, width(), selected);
|
||||
row->paintStatusText(p, st, statusx, statusy, statusw, width(), selected);
|
||||
}
|
||||
|
||||
row->elementsPaint(
|
||||
|
@ -1905,10 +1917,30 @@ void PeerListContent::selectSkipPage(int height, int 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 {
|
||||
return _selectedIndex.value();
|
||||
}
|
||||
|
||||
int PeerListContent::selectedIndex() const {
|
||||
return _selectedIndex.current();
|
||||
}
|
||||
|
||||
bool PeerListContent::hasSelection() const {
|
||||
return _selected.index.value >= 0;
|
||||
}
|
||||
|
|
|
@ -100,6 +100,8 @@ public:
|
|||
-> const base::flat_set<QChar> &;
|
||||
[[nodiscard]] virtual auto generateNameWords() const
|
||||
-> const base::flat_set<QString> &;
|
||||
[[nodiscard]] virtual const style::PeerListItem &computeSt(
|
||||
const style::PeerListItem &st) const;
|
||||
|
||||
virtual void preloadUserpic();
|
||||
|
||||
|
@ -228,7 +230,12 @@ public:
|
|||
QPoint point,
|
||||
UpdateCallback &&updateCallback);
|
||||
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(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
|
@ -601,6 +608,7 @@ public:
|
|||
};
|
||||
SkipResult selectSkip(int direction);
|
||||
void selectSkipPage(int height, int direction);
|
||||
void selectLast();
|
||||
|
||||
enum class Mode {
|
||||
Default,
|
||||
|
@ -609,6 +617,7 @@ public:
|
|||
void setMode(Mode mode);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> selectedIndexValue() const;
|
||||
[[nodiscard]] int selectedIndex() const;
|
||||
[[nodiscard]] bool hasSelection() const;
|
||||
[[nodiscard]] bool hasPressed() const;
|
||||
void clearSelection();
|
||||
|
|
|
@ -166,33 +166,6 @@ void SaveChannelAdmin(
|
|||
}).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(
|
||||
not_null<ChatData*> chat,
|
||||
not_null<UserData*> user,
|
||||
|
@ -275,7 +248,7 @@ Fn<void(
|
|||
ChatRestrictionsInfo newRights) {
|
||||
const auto done = [=] { if (onDone) onDone(newRights); };
|
||||
const auto saveForChannel = [=](not_null<ChannelData*> channel) {
|
||||
SaveChannelRestriction(
|
||||
Api::ChatParticipants::Restrict(
|
||||
channel,
|
||||
participant,
|
||||
oldRights,
|
||||
|
|
|
@ -312,6 +312,7 @@ PreviewWrap::PreviewWrap(
|
|||
nullptr, // document
|
||||
WebPageCollage(),
|
||||
nullptr, // iv
|
||||
nullptr, // stickerSet
|
||||
0, // duration
|
||||
QString(), // author
|
||||
false, // hasLargeMedia
|
||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "api/api_user_names.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/edit_peer_color_box.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/username_box.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/core_settings.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 "settings/settings_common.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/rp_widget.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 "window/window_session_controller.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
@ -533,7 +538,7 @@ object_ptr<Ui::RpWidget> Controller::createTitleEdit() {
|
|||
_wrap,
|
||||
object_ptr<Ui::InputField>(
|
||||
_wrap,
|
||||
st::defaultInputField,
|
||||
st::editPeerTitleField,
|
||||
(_isBot
|
||||
? tr::lng_dlg_new_bot_name
|
||||
: _isGroup
|
||||
|
@ -555,6 +560,76 @@ object_ptr<Ui::RpWidget> Controller::createTitleEdit() {
|
|||
submitTitle();
|
||||
}, 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();
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -7,30 +7,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "boxes/peers/edit_peer_reactions.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer_values.h" // UniqueReactionsLimit.
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/layers/generic_box.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/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_link_info.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
#include <QtGui/QTextBlock>
|
||||
|
@ -705,12 +708,16 @@ void EditAllowedReactionsBox(
|
|||
}
|
||||
};
|
||||
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, {
|
||||
.outer = box->getDelegate()->outerContainer(),
|
||||
.controller = args.navigation->parentController(),
|
||||
.title = (enabled
|
||||
? tr::lng_manage_peer_reactions_available()
|
||||
: tr::lng_manage_peer_reactions_some_title()),
|
||||
.title = tr::lng_manage_peer_reactions_available_ph(),
|
||||
.list = all,
|
||||
.selected = state->selected,
|
||||
.callback = changed,
|
||||
|
@ -726,6 +733,7 @@ void EditAllowedReactionsBox(
|
|||
}
|
||||
});
|
||||
|
||||
const auto reactionsLimit = container->lifetime().make_state<int>(0);
|
||||
if (!isGroup) {
|
||||
AddReactionsText(
|
||||
container,
|
||||
|
@ -733,9 +741,109 @@ void EditAllowedReactionsBox(
|
|||
args.allowedCustomReactions,
|
||||
state->customCount.value(),
|
||||
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 ¢erSize,
|
||||
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 = [=] {
|
||||
auto result = AllowedReactions();
|
||||
result.maxCount = (*reactionsLimit);
|
||||
if (isGroup
|
||||
? (state->option.current() == Option::Some)
|
||||
: (enabled->toggled())) {
|
||||
|
@ -783,6 +891,7 @@ void SaveAllowedReactions(
|
|||
Data::ReactionToMTP
|
||||
) | ranges::to<QVector<MTPReaction>>;
|
||||
|
||||
using Flag = MTPmessages_SetChatAvailableReactions::Flag;
|
||||
using Type = Data::AllowedReactionsType;
|
||||
const auto updated = (allowed.type != Type::Some)
|
||||
? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
|
||||
|
@ -792,14 +901,18 @@ void SaveAllowedReactions(
|
|||
? MTP_chatReactionsNone()
|
||||
: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
|
||||
peer->session().api().request(MTPmessages_SetChatAvailableReactions(
|
||||
allowed.maxCount ? MTP_flags(Flag::f_reactions_limit) : MTP_flags(0),
|
||||
peer->input,
|
||||
updated
|
||||
updated,
|
||||
MTP_int(allowed.maxCount)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
peer->session().api().applyUpdates(result);
|
||||
auto parsed = Data::Parse(updated);
|
||||
parsed.maxCount = allowed.maxCount;
|
||||
if (const auto chat = peer->asChat()) {
|
||||
chat->setAllowedReactions(Data::Parse(updated));
|
||||
chat->setAllowedReactions(parsed);
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
channel->setAllowedReactions(Data::Parse(updated));
|
||||
channel->setAllowedReactions(parsed);
|
||||
} else {
|
||||
Unexpected("Invalid peer type in SaveAllowedReactions.");
|
||||
}
|
||||
|
|
|
@ -913,7 +913,7 @@ void PreviewBox(
|
|||
auto businessOrder = Settings::BusinessFeaturesOrder(&show->session());
|
||||
state->order = ranges::contains(businessOrder, descriptor.section)
|
||||
? std::move(businessOrder)
|
||||
: ranges::contains(businessOrder, descriptor.section)
|
||||
: ranges::contains(premiumOrder, descriptor.section)
|
||||
? std::move(premiumOrder)
|
||||
: std::vector{ descriptor.section };
|
||||
|
||||
|
|
|
@ -239,7 +239,10 @@ void ShareBox::prepareCommentField() {
|
|||
const auto field = _comment->entity();
|
||||
|
||||
field->submits(
|
||||
) | rpl::start_with_next([=] { submit({}); }, field->lifetime());
|
||||
) | rpl::start_with_next([=] {
|
||||
submit({});
|
||||
}, field->lifetime());
|
||||
|
||||
if (const auto show = uiShow(); show->valid()) {
|
||||
InitMessageFieldHandlers(
|
||||
_descriptor.session,
|
||||
|
@ -251,6 +254,14 @@ void ShareBox::prepareCommentField() {
|
|||
}
|
||||
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);
|
||||
if (_bottomWidget) {
|
||||
Ui::SendPendingMoveResizeEvents(_bottomWidget);
|
||||
|
@ -260,8 +271,6 @@ void ShareBox::prepareCommentField() {
|
|||
void ShareBox::prepare() {
|
||||
prepareCommentField();
|
||||
|
||||
setCloseByOutsideClick(false);
|
||||
|
||||
_select->resizeToWidth(st::boxWideWidth);
|
||||
Ui::SendPendingMoveResizeEvents(_select);
|
||||
|
||||
|
@ -318,6 +327,12 @@ void ShareBox::prepare() {
|
|||
not_null<Data::Thread*> thread,
|
||||
bool checked) {
|
||||
innerSelectedChanged(thread, checked);
|
||||
if (checked) {
|
||||
setCloseByOutsideClick(false);
|
||||
} else if (_inner->selected().empty()
|
||||
&& _comment->entity()->getLastText().isEmpty()) {
|
||||
setCloseByOutsideClick(true);
|
||||
}
|
||||
});
|
||||
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
|
@ -1690,6 +1705,101 @@ void FastShareMessage(
|
|||
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()
|
||||
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> {
|
||||
return WritePremiumRequiredError;
|
||||
|
|
|
@ -37,6 +37,7 @@ struct SendOptions;
|
|||
|
||||
namespace Main {
|
||||
class Session;
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Dialogs {
|
||||
|
@ -68,6 +69,12 @@ void ShareGameScoreByHash(
|
|||
void FastShareMessage(
|
||||
not_null<Window::SessionController*> controller,
|
||||
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;
|
||||
[[nodiscard]] auto SharePremiumRequiredError()
|
||||
|
|
|
@ -1076,7 +1076,7 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
|||
_menu.get(),
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(sendSelected),
|
||||
SendMenu::DefaultScheduleCallback(this, type, sendSelected),
|
||||
SendMenu::DefaultScheduleCallback(_show, type, sendSelected),
|
||||
SendMenu::DefaultWhenOnlineCallback(sendSelected));
|
||||
|
||||
const auto show = _show;
|
||||
|
|
|
@ -7,31 +7,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#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 "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/platform/base_platform_info.h"
|
||||
#include "base/random.h"
|
||||
#include "mtproto/mtproto_dh_utils.h"
|
||||
#include "mtproto/mtproto_config.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_panel.h"
|
||||
#include "core/application.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 "base/platform/base_platform_info.h"
|
||||
#include "calls/calls_panel.h"
|
||||
#include "mtproto/mtproto_config.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_video_track.h"
|
||||
#include "webrtc/webrtc_create_adm.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "window/window_controller.h"
|
||||
|
||||
#include <tgcalls/Instance.h>
|
||||
#include <tgcalls/VideoCaptureInterface.h>
|
||||
|
@ -1072,6 +1070,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
|||
Core::App().mediaDevices().setCaptureMuted(muted);
|
||||
}, _instanceLifetime);
|
||||
|
||||
#if 0
|
||||
Core::App().batterySaving().value(
|
||||
) | rpl::start_with_next([=](bool isSaving) {
|
||||
crl::on_main(weak, [=] {
|
||||
|
@ -1080,6 +1079,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
|||
}
|
||||
});
|
||||
}, _instanceLifetime);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Call::handleControllerStateChange(tgcalls::State state) {
|
||||
|
|
|
@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "main/main_session.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "base/timer.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h" // style::GroupCallUserpics
|
||||
#include "styles/style_layers.h"
|
||||
|
@ -49,7 +50,6 @@ enum class BarState {
|
|||
namespace {
|
||||
|
||||
constexpr auto kUpdateDebugTimeoutMs = crl::time(500);
|
||||
constexpr auto kSwitchStateDuration = 120;
|
||||
|
||||
constexpr auto kMinorBlobAlpha = 76. / 255.;
|
||||
|
||||
|
@ -374,7 +374,7 @@ void TopBar::initControls() {
|
|||
};
|
||||
|
||||
_switchStateAnimation.stop();
|
||||
const auto duration = (to - from) * kSwitchStateDuration;
|
||||
const auto duration = (to - from) * st::universalDuration;
|
||||
_switchStateAnimation.start(
|
||||
_switchStateCallback,
|
||||
from,
|
||||
|
|
|
@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
@ -26,7 +27,6 @@ namespace {
|
|||
constexpr auto kRoundRadius = 9;
|
||||
constexpr auto kMaxGroupCallLength = 40;
|
||||
constexpr auto kSwitchDuration = 200;
|
||||
constexpr auto kSelectDuration = 120;
|
||||
|
||||
class GraphicButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
|
@ -103,7 +103,7 @@ void GraphicButton::setToggled(bool value) {
|
|||
[=] { update(); },
|
||||
_toggled ? 0. : 1.,
|
||||
_toggled ? 1. : 0.,
|
||||
kSelectDuration);
|
||||
st::universalDuration);
|
||||
}
|
||||
|
||||
void GraphicButton::paintEvent(QPaintEvent *e) {
|
||||
|
|
|
@ -1108,8 +1108,6 @@ historyRecordVoiceFgOver: historyComposeIconFgOver;
|
|||
historyRecordVoiceFgInactive: attentionButtonFg;
|
||||
historyRecordVoiceFgActive: windowBgActive;
|
||||
historyRecordVoiceFgActiveIcon: windowFgActive;
|
||||
historyRecordVoiceShowDuration: 120;
|
||||
historyRecordVoiceDuration: 120;
|
||||
historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }};
|
||||
historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }};
|
||||
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};
|
||||
|
|
|
@ -468,7 +468,8 @@ EmojiListWidget::EmojiListWidget(
|
|||
std::move(descriptor.paused))
|
||||
, _show(std::move(descriptor.show))
|
||||
, _features(descriptor.features)
|
||||
, _mode(descriptor.mode)
|
||||
, _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle)
|
||||
, _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode)
|
||||
, _api(&session().mtp())
|
||||
, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)
|
||||
, _premiumIcon(_mode == Mode::EmojiStatus
|
||||
|
@ -490,7 +491,8 @@ EmojiListWidget::EmojiListWidget(
|
|||
|
||||
if (_mode != Mode::RecentReactions
|
||||
&& _mode != Mode::BackgroundEmoji
|
||||
&& _mode != Mode::ChannelStatus) {
|
||||
&& _mode != Mode::ChannelStatus
|
||||
&& !_onlyUnicodeEmoji) {
|
||||
setupSearch();
|
||||
}
|
||||
|
||||
|
@ -571,12 +573,17 @@ EmojiListWidget::~EmojiListWidget() {
|
|||
|
||||
void EmojiListWidget::setupSearch() {
|
||||
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) {
|
||||
_nextSearchQuery = std::move(query);
|
||||
InvokeQueued(this, [=] {
|
||||
applyNextSearchQuery();
|
||||
});
|
||||
}, session, (_mode == Mode::EmojiStatus), _mode == Mode::UserpicBuilder);
|
||||
}, session, type);
|
||||
}
|
||||
|
||||
void EmojiListWidget::applyNextSearchQuery() {
|
||||
|
@ -1052,7 +1059,7 @@ void EmojiListWidget::fillRecent() {
|
|||
const auto test = session().isTestMode();
|
||||
for (const auto &one : list) {
|
||||
const auto document = std::get_if<RecentEmojiDocument>(&one.id.data);
|
||||
if (document && document->test != test) {
|
||||
if (document && ((document->test != test) || _onlyUnicodeEmoji)) {
|
||||
continue;
|
||||
}
|
||||
_recent.push_back({
|
||||
|
@ -2129,7 +2136,9 @@ void EmojiListWidget::refreshCustom() {
|
|||
auto old = base::take(_custom);
|
||||
const auto session = &this->session();
|
||||
const auto premiumPossible = session->premiumPossible();
|
||||
const auto premiumMayBeBought = premiumPossible
|
||||
const auto onlyUnicodeEmoji = _onlyUnicodeEmoji || !premiumPossible;
|
||||
const auto premiumMayBeBought = (!onlyUnicodeEmoji)
|
||||
&& premiumPossible
|
||||
&& !session->premium()
|
||||
&& !_allowWithoutPremium;
|
||||
const auto owner = &session->data();
|
||||
|
@ -2189,7 +2198,7 @@ void EmojiListWidget::refreshCustom() {
|
|||
}
|
||||
return true;
|
||||
}();
|
||||
if (premium && !premiumPossible) {
|
||||
if (premium && onlyUnicodeEmoji) {
|
||||
return;
|
||||
} else if (valid) {
|
||||
i->thumbnailDocument = it->second->lookupThumbnailDocument();
|
||||
|
@ -2223,7 +2232,7 @@ void EmojiListWidget::refreshCustom() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (premium && !premiumPossible) {
|
||||
if (premium && onlyUnicodeEmoji) {
|
||||
return;
|
||||
}
|
||||
_custom.push_back({
|
||||
|
|
|
@ -76,6 +76,7 @@ enum class EmojiListMode {
|
|||
RecentReactions,
|
||||
UserpicBuilder,
|
||||
BackgroundEmoji,
|
||||
PeerTitle,
|
||||
};
|
||||
|
||||
struct EmojiListDescriptor {
|
||||
|
@ -379,6 +380,7 @@ private:
|
|||
|
||||
const std::shared_ptr<Show> _show;
|
||||
const ComposeFeatures _features;
|
||||
const bool _onlyUnicodeEmoji;
|
||||
Mode _mode = Mode::Full;
|
||||
std::unique_ptr<Ui::TabbedSearch> _search;
|
||||
MTP::Sender _api;
|
||||
|
|
|
@ -38,7 +38,6 @@ namespace {
|
|||
|
||||
constexpr auto kShowExactDelay = crl::time(300);
|
||||
constexpr auto kMaxNonScrolledEmoji = 7;
|
||||
constexpr auto kAnimationDuration = crl::time(120);
|
||||
|
||||
} // namespace
|
||||
|
||||
|
@ -528,7 +527,7 @@ void SuggestionsWidget::setSelected(int selected, anim::type animated) {
|
|||
[=] { update(); },
|
||||
_selected,
|
||||
selected,
|
||||
kAnimationDuration,
|
||||
st::universalDuration,
|
||||
anim::sineInOut);
|
||||
if (_scrollMax > 0) {
|
||||
const auto selectedMax = int(_rows.size()) - 3;
|
||||
|
@ -560,7 +559,7 @@ void SuggestionsWidget::scrollTo(int value, anim::type animated) {
|
|||
[=] { update(); },
|
||||
_scrollValue,
|
||||
value,
|
||||
kAnimationDuration,
|
||||
st::universalDuration,
|
||||
anim::sineInOut);
|
||||
}
|
||||
_scrollValue = value;
|
||||
|
|
|
@ -1379,7 +1379,7 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
|||
_menu,
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send),
|
||||
SendMenu::DefaultScheduleCallback(_show, type, send),
|
||||
SendMenu::DefaultWhenOnlineCallback(send));
|
||||
|
||||
if (!_menu->empty()) {
|
||||
|
|
|
@ -401,7 +401,7 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
|
|||
menu,
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send),
|
||||
SendMenu::DefaultScheduleCallback(_show, type, send),
|
||||
SendMenu::DefaultWhenOnlineCallback(send),
|
||||
icons);
|
||||
|
||||
|
@ -849,7 +849,7 @@ void GifsListWidget::setupSearch() {
|
|||
: SearchEmojiSectionSetId();
|
||||
refreshIcons();
|
||||
searchForGifs(accumulated);
|
||||
}, session);
|
||||
}, session, TabbedSearchType::Emoji);
|
||||
}
|
||||
|
||||
int32 GifsListWidget::showInlineRows(bool newResults) {
|
||||
|
|
|
@ -553,6 +553,15 @@ void StickersListWidget::sendSearchRequest() {
|
|||
}
|
||||
|
||||
_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);
|
||||
_searchRequestId = _api.request(MTPmessages_SearchStickerSets(
|
||||
MTP_flags(0),
|
||||
|
@ -576,10 +585,14 @@ void StickersListWidget::searchForSets(
|
|||
return;
|
||||
}
|
||||
|
||||
_filteredStickers = session().data().stickers().getListByEmoji(
|
||||
std::move(emoji),
|
||||
0,
|
||||
true);
|
||||
if (query == Ui::PremiumGroupFakeEmoticon()) {
|
||||
_filteredStickers = session().data().stickers().getPremiumList(0);
|
||||
} else {
|
||||
_filteredStickers = session().data().stickers().getListByEmoji(
|
||||
std::move(emoji),
|
||||
0,
|
||||
true);
|
||||
}
|
||||
if (_searchQuery != cleaned) {
|
||||
_search->setLoading(false);
|
||||
if (const auto requestId = base::take(_searchRequestId)) {
|
||||
|
@ -1660,7 +1673,7 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
|
|||
menu,
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send),
|
||||
SendMenu::DefaultScheduleCallback(_show, type, send),
|
||||
SendMenu::DefaultWhenOnlineCallback(send),
|
||||
icons);
|
||||
|
||||
|
@ -2636,15 +2649,20 @@ void StickersListWidget::beforeHiding() {
|
|||
|
||||
void StickersListWidget::setupSearch() {
|
||||
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) {
|
||||
auto set = base::flat_set<EmojiPtr>();
|
||||
auto text = ranges::accumulate(query, QString(), [](
|
||||
QString a,
|
||||
QString b) {
|
||||
QString a,
|
||||
QString b) {
|
||||
return a.isEmpty() ? b : (a + ' ' + b);
|
||||
});
|
||||
searchForSets(std::move(text), SearchEmoji(query, set));
|
||||
}, session, false, (_mode == Mode::UserpicBuilder));
|
||||
}, session, type);
|
||||
}
|
||||
|
||||
void StickersListWidget::displaySet(uint64 setId) {
|
||||
|
|
|
@ -65,6 +65,7 @@ enum class StickersListMode {
|
|||
Full,
|
||||
Masks,
|
||||
UserpicBuilder,
|
||||
ChatIntro,
|
||||
};
|
||||
|
||||
struct StickersListDescriptor {
|
||||
|
|
|
@ -302,21 +302,39 @@ void TabbedSelector::Tab::saveScrollTop() {
|
|||
_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(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::EmojiPan &st,
|
||||
Fn<void(std::vector<QString>&&)> callback,
|
||||
not_null<Main::Session*> session,
|
||||
bool statusCategories,
|
||||
bool profilePhotoCategories) {
|
||||
TabbedSearchType type) {
|
||||
using Descriptor = Ui::SearchDescriptor;
|
||||
const auto owner = &session->data();
|
||||
auto result = std::make_unique<Ui::TabbedSearch>(parent, st, Descriptor{
|
||||
.st = st.search,
|
||||
.groups = (profilePhotoCategories
|
||||
.groups = ((type == TabbedSearchType::ProfilePhoto)
|
||||
? owner->emojiStatuses().profilePhotoGroupsValue()
|
||||
: statusCategories
|
||||
: (type == TabbedSearchType::Status)
|
||||
? owner->emojiStatuses().statusGroupsValue()
|
||||
: (type == TabbedSearchType::Stickers)
|
||||
? owner->emojiStatuses().stickerGroupsValue()
|
||||
: (type == TabbedSearchType::Greeting)
|
||||
? GreetingGroupFirst(owner)
|
||||
: owner->emojiStatuses().emojiGroupsValue()),
|
||||
.customEmojiFactory = owner->customEmojiManager().factory(
|
||||
Data::CustomEmojiManager::SizeTag::SetIcon,
|
||||
|
@ -378,7 +396,7 @@ TabbedSelector::TabbedSelector(
|
|||
tabs.reserve(2);
|
||||
tabs.push_back(createTab(SelectorTab::Stickers, 0));
|
||||
tabs.push_back(createTab(SelectorTab::Masks, 1));
|
||||
} else if (_mode == Mode::StickersOnly) {
|
||||
} else if (_mode == Mode::StickersOnly || _mode == Mode::ChatIntro) {
|
||||
tabs.reserve(1);
|
||||
tabs.push_back(createTab(SelectorTab::Stickers, 0));
|
||||
} else {
|
||||
|
@ -389,7 +407,9 @@ TabbedSelector::TabbedSelector(
|
|||
}())
|
||||
, _currentTabType(full()
|
||||
? session().settings().selectorTab()
|
||||
: (mediaEditor() || _mode == Mode::StickersOnly)
|
||||
: (mediaEditor()
|
||||
|| _mode == Mode::StickersOnly
|
||||
|| _mode == Mode::ChatIntro)
|
||||
? SelectorTab::Stickers
|
||||
: SelectorTab::Emoji)
|
||||
, _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type))
|
||||
|
@ -540,6 +560,8 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
|
|||
? EmojiMode::FullReactions
|
||||
: _mode == Mode::RecentReactions
|
||||
? EmojiMode::RecentReactions
|
||||
: _mode == Mode::PeerTitle
|
||||
? EmojiMode::PeerTitle
|
||||
: EmojiMode::Full),
|
||||
.customTextColor = _customTextColor,
|
||||
.paused = paused,
|
||||
|
@ -552,7 +574,9 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
|
|||
using Descriptor = StickersListDescriptor;
|
||||
return object_ptr<StickersListWidget>(this, Descriptor{
|
||||
.show = _show,
|
||||
.mode = StickersMode::Full,
|
||||
.mode = (_mode == Mode::ChatIntro
|
||||
? StickersMode::ChatIntro
|
||||
: StickersMode::Full),
|
||||
.paused = paused,
|
||||
.st = &_st,
|
||||
.features = _features,
|
||||
|
@ -958,6 +982,9 @@ void TabbedSelector::beforeHiding() {
|
|||
_beforeHidingCallback(_currentTabType);
|
||||
}
|
||||
}
|
||||
if (Ui::InFocusChain(this)) {
|
||||
window()->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedSelector::afterShown() {
|
||||
|
|
|
@ -86,6 +86,8 @@ enum class TabbedSelectorMode {
|
|||
BackgroundEmoji,
|
||||
FullReactions,
|
||||
RecentReactions,
|
||||
PeerTitle,
|
||||
ChatIntro,
|
||||
};
|
||||
|
||||
struct TabbedSelectorDescriptor {
|
||||
|
@ -97,13 +99,19 @@ struct TabbedSelectorDescriptor {
|
|||
ComposeFeatures features;
|
||||
};
|
||||
|
||||
enum class TabbedSearchType {
|
||||
Emoji,
|
||||
Status,
|
||||
ProfilePhoto,
|
||||
Stickers,
|
||||
Greeting,
|
||||
};
|
||||
[[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::EmojiPan &st,
|
||||
Fn<void(std::vector<QString>&&)> callback,
|
||||
not_null<Main::Session*> session,
|
||||
bool statusCategories = false,
|
||||
bool profilePhotoCategories = false);
|
||||
TabbedSearchType type);
|
||||
|
||||
class TabbedSelector : public Ui::RpWidget {
|
||||
public:
|
||||
|
|
|
@ -230,7 +230,7 @@ PreviewWrap::PreviewWrap(
|
|||
lt_user,
|
||||
rpl::single(
|
||||
item->history()->peer->shortName()
|
||||
) | rpl::map(Ui::Text::RichLangValue),
|
||||
) | Ui::Text::ToRichLangValue(),
|
||||
Ui::Text::RichLangValue)
|
||||
: (isRound
|
||||
? settings->saveDeletedMessages ? tr::ayu_ExpiringVideoMessageNote : tr::lng_ttl_round_tooltip_in
|
||||
|
|
|
@ -241,23 +241,21 @@ Application::~Application() {
|
|||
_mediaControlsManager = nullptr;
|
||||
|
||||
Media::Player::finish(_audio.get());
|
||||
style::stopManager();
|
||||
|
||||
ThirdParty::finish();
|
||||
style::StopManager();
|
||||
|
||||
Instance = nullptr;
|
||||
}
|
||||
|
||||
void Application::run() {
|
||||
style::internal::StartFonts();
|
||||
|
||||
ThirdParty::start();
|
||||
|
||||
// Depends on OpenSSL on macOS, so on ThirdParty::start().
|
||||
// Depends on notifications settings.
|
||||
_notifications = std::make_unique<Window::Notifications::System>();
|
||||
|
||||
startLocalStorage();
|
||||
|
||||
style::SetCustomFont(settings().customFontFamily());
|
||||
style::internal::StartFonts();
|
||||
|
||||
ValidateScale();
|
||||
|
||||
refreshGlobalProxy(); // Depends on app settings being read.
|
||||
|
@ -280,7 +278,7 @@ void Application::run() {
|
|||
_translator = std::make_unique<Lang::Translator>();
|
||||
QCoreApplication::instance()->installTranslator(_translator.get());
|
||||
|
||||
style::startManager(cScale());
|
||||
style::StartManager(cScale());
|
||||
Ui::InitTextOptions();
|
||||
Ui::StartCachedCorners();
|
||||
Ui::Emoji::Init();
|
||||
|
@ -1525,14 +1523,14 @@ void Application::closeChatFromWindows(not_null<PeerData*> peer) {
|
|||
}
|
||||
}
|
||||
if (const auto window = windowFor(&peer->account())) {
|
||||
const auto primary = window->sessionController();
|
||||
if ((primary->activeChatCurrent().peer() == peer)
|
||||
&& (&primary->session() == &peer->session())) {
|
||||
primary->clearSectionStack();
|
||||
}
|
||||
if (const auto forum = primary->shownForum().current()) {
|
||||
if (peer->forum() == forum) {
|
||||
primary->closeForum();
|
||||
if (const auto primary = window->sessionController()) {
|
||||
if (primary->activeChatCurrent().peer() == peer) {
|
||||
primary->clearSectionStack();
|
||||
}
|
||||
if (const auto forum = primary->shownForum().current()) {
|
||||
if (peer->forum() == forum) {
|
||||
primary->closeForum();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,7 +221,8 @@ QByteArray Settings::serialize() const {
|
|||
+ Serialize::stringSize(_callPlaybackDeviceId.current())
|
||||
+ Serialize::stringSize(_callCaptureDeviceId.current())
|
||||
+ Serialize::bytearraySize(ivPosition)
|
||||
+ Serialize::stringSize(noWarningExtensions);
|
||||
+ Serialize::stringSize(noWarningExtensions)
|
||||
+ Serialize::stringSize(_customFontFamily);
|
||||
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
|
@ -367,7 +368,8 @@ QByteArray Settings::serialize() const {
|
|||
<< _callPlaybackDeviceId.current()
|
||||
<< _callCaptureDeviceId.current()
|
||||
<< ivPosition
|
||||
<< noWarningExtensions;
|
||||
<< noWarningExtensions
|
||||
<< _customFontFamily;
|
||||
}
|
||||
|
||||
Ensures(result.size() == size);
|
||||
|
@ -487,6 +489,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
|||
qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);
|
||||
qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;
|
||||
QByteArray ivPosition;
|
||||
QString customFontFamily = _customFontFamily;
|
||||
|
||||
stream >> themesAccentColors;
|
||||
if (!stream.atEnd()) {
|
||||
|
@ -772,6 +775,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
|||
noWarningExtensions = QString();
|
||||
stream >> *noWarningExtensions;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> customFontFamily;
|
||||
}
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
LOG(("App Error: "
|
||||
"Bad data for Core::Settings::constructFromSerialized()"));
|
||||
|
@ -979,6 +985,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
|||
if (!ivPosition.isEmpty()) {
|
||||
_ivPosition = Deserialize(ivPosition);
|
||||
}
|
||||
_customFontFamily = customFontFamily;
|
||||
}
|
||||
|
||||
QString Settings::getSoundPath(const QString &key) const {
|
||||
|
|
|
@ -877,6 +877,13 @@ public:
|
|||
_ivPosition = position;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString customFontFamily() const {
|
||||
return _customFontFamily;
|
||||
}
|
||||
void setCustomFontFamily(const QString &value) {
|
||||
_customFontFamily = value;
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool ThirdColumnByDefault();
|
||||
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
|
||||
|
||||
|
@ -1005,6 +1012,7 @@ private:
|
|||
rpl::variable<bool> _storiesClickTooltipHidden = false;
|
||||
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
|
||||
WindowPosition _ivPosition;
|
||||
QString _customFontFamily;
|
||||
|
||||
bool _tabbedReplacedWithInfo = false; // per-window
|
||||
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
|
||||
|
|
|
@ -95,9 +95,6 @@ SettingsProxy::SettingsProxy()
|
|||
}
|
||||
|
||||
QByteArray SettingsProxy::serialize() const {
|
||||
auto result = QByteArray();
|
||||
auto stream = QDataStream(&result, QIODevice::WriteOnly);
|
||||
|
||||
const auto serializedSelected = SerializeProxyData(_selected);
|
||||
const auto serializedList = ranges::views::all(
|
||||
_list
|
||||
|
@ -111,9 +108,7 @@ QByteArray SettingsProxy::serialize() const {
|
|||
0,
|
||||
ranges::plus(),
|
||||
&Serialize::bytearraySize);
|
||||
result.reserve(size);
|
||||
|
||||
stream.setVersion(QDataStream::Qt_5_1);
|
||||
auto stream = Serialize::ByteArrayWriter(size);
|
||||
stream
|
||||
<< qint32(_tryIPv6 ? 1 : 0)
|
||||
<< qint32(_useProxyForCalls ? 1 : 0)
|
||||
|
@ -123,9 +118,7 @@ QByteArray SettingsProxy::serialize() const {
|
|||
for (const auto &i : serializedList) {
|
||||
stream << i;
|
||||
}
|
||||
|
||||
stream.device()->close();
|
||||
return result;
|
||||
return std::move(stream).result();
|
||||
}
|
||||
|
||||
bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
||||
|
@ -133,7 +126,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||
return true;
|
||||
}
|
||||
|
||||
auto stream = QDataStream(serialized);
|
||||
auto stream = Serialize::ByteArrayReader(serialized);
|
||||
|
||||
auto tryIPv6 = qint32(_tryIPv6 ? 1 : 0);
|
||||
auto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0);
|
||||
|
@ -148,7 +141,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||
>> settings
|
||||
>> selectedProxy
|
||||
>> listCount;
|
||||
if (stream.status() == QDataStream::Ok) {
|
||||
if (stream.ok()) {
|
||||
for (auto i = 0; i != listCount; ++i) {
|
||||
QByteArray data;
|
||||
stream >> data;
|
||||
|
@ -157,7 +150,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||
}
|
||||
}
|
||||
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
if (!stream.ok()) {
|
||||
LOG(("App Error: "
|
||||
"Bad data for Core::SettingsProxy::setFromSerialized()"));
|
||||
return false;
|
||||
|
|
|
@ -32,8 +32,6 @@ constexpr auto kDefaultProxyPort = 80;
|
|||
PreLaunchWindow *PreLaunchWindowInstance = nullptr;
|
||||
|
||||
PreLaunchWindow::PreLaunchWindow(QString title) {
|
||||
style::internal::StartFonts();
|
||||
|
||||
setWindowIcon(Window::CreateIcon());
|
||||
setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
|
||||
|
||||
|
@ -99,7 +97,7 @@ PreLaunchWindow::~PreLaunchWindow() {
|
|||
|
||||
PreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) {
|
||||
QFont labelFont(font());
|
||||
labelFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
labelFont.setWeight(QFont::DemiBold);
|
||||
labelFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(labelFont);
|
||||
|
||||
|
@ -118,7 +116,6 @@ void PreLaunchLabel::setText(const QString &text) {
|
|||
|
||||
PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {
|
||||
QFont logFont(font());
|
||||
logFont.setFamily(style::internal::GetFontOverride());
|
||||
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(logFont);
|
||||
|
||||
|
@ -139,7 +136,6 @@ PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(paren
|
|||
|
||||
PreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) {
|
||||
QFont logFont(font());
|
||||
logFont.setFamily(style::internal::GetFontOverride());
|
||||
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(logFont);
|
||||
|
||||
|
@ -162,7 +158,7 @@ PreLaunchButton::PreLaunchButton(QWidget *parent, bool confirm) : QPushButton(pa
|
|||
setObjectName(confirm ? "confirm" : "cancel");
|
||||
|
||||
QFont closeFont(font());
|
||||
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
closeFont.setWeight(QFont::DemiBold);
|
||||
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(closeFont);
|
||||
|
||||
|
@ -181,7 +177,7 @@ PreLaunchCheckbox::PreLaunchCheckbox(QWidget *parent) : QCheckBox(parent) {
|
|||
setCheckState(Qt::Checked);
|
||||
|
||||
QFont closeFont(font());
|
||||
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
closeFont.setWeight(QFont::DemiBold);
|
||||
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(closeFont);
|
||||
|
||||
|
|
|
@ -385,6 +385,7 @@ int Launcher::exec() {
|
|||
|
||||
// Must be started before Sandbox is created.
|
||||
Platform::start();
|
||||
ThirdParty::start();
|
||||
auto result = executeApplication();
|
||||
|
||||
DEBUG_LOG(("Telegram finished, result: %1").arg(result));
|
||||
|
@ -400,6 +401,7 @@ int Launcher::exec() {
|
|||
}
|
||||
|
||||
CrashReports::Finish();
|
||||
ThirdParty::finish();
|
||||
Platform::finish();
|
||||
Logs::finish();
|
||||
|
||||
|
|
|
@ -244,8 +244,8 @@ QString FileExtension(const QString &filepath) {
|
|||
|
||||
NameType DetectNameType(const QString &filepath) {
|
||||
static const auto kImage = SplitExtensions(u"\
|
||||
afdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large nef png \
|
||||
png-large psd raw sketch svg tga tif tiff webp"_q);
|
||||
afdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large jxl nef \
|
||||
png png-large psd qoi raw sketch svg tga tif tiff webp"_q);
|
||||
static const auto kVideo = SplitExtensions(u"\
|
||||
3g2 3gp 3gpp aep avi flv h264 m4s m4v mkv mov mp4 mpeg mpg ogv srt tgs tgv \
|
||||
vob webm wmv"_q);
|
||||
|
|
|
@ -92,8 +92,6 @@ namespace ThirdParty {
|
|||
FIPS_mode_set(0);
|
||||
#endif
|
||||
CONF_modules_unload(1);
|
||||
|
||||
Platform::ThirdParty::finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
|
|||
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
|
||||
constexpr auto AppName = "AyuGram Desktop"_cs;
|
||||
constexpr auto AppFile = "AyuGram"_cs;
|
||||
constexpr auto AppVersion = 4016008;
|
||||
constexpr auto AppVersionStr = "4.16.8";
|
||||
constexpr auto AppVersion = 5000001;
|
||||
constexpr auto AppVersionStr = "5.0.1";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
|
122
Telegram/SourceFiles/data/components/recent_peers.cpp
Normal 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
|
39
Telegram/SourceFiles/data/components/recent_peers.h
Normal 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
|
|
@ -9,8 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_bot_app.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
|
@ -40,9 +41,20 @@ SponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
for (const auto &request : base::take(_viewRequests)) {
|
||||
_session->api().request(request.second.requestId).cancel();
|
||||
}
|
||||
base::take(_data);
|
||||
}
|
||||
|
||||
void SponsoredMessages::clearOldRequests() {
|
||||
|
@ -263,94 +275,23 @@ void SponsoredMessages::append(
|
|||
const MTPSponsoredMessage &message) {
|
||||
const auto &data = message.data();
|
||||
const auto randomId = data.vrandom_id().v;
|
||||
const auto hash = qs(data.vchat_invite_hash().value_or_empty());
|
||||
const auto makeFrom = [&](
|
||||
not_null<PeerData*> peer,
|
||||
bool exactPost = false) {
|
||||
const auto channel = peer->asChannel();
|
||||
return SponsoredFrom{
|
||||
.peer = peer,
|
||||
.title = peer->name(),
|
||||
.isBroadcast = (channel && channel->isBroadcast()),
|
||||
.isMegagroup = (channel && channel->isMegagroup()),
|
||||
.isChannel = (channel != nullptr),
|
||||
.isPublic = (channel && channel->isPublic()),
|
||||
.isExactPost = exactPost,
|
||||
.isRecommended = data.is_recommended(),
|
||||
.isForceUserpicDisplay = data.is_show_peer_photo(),
|
||||
.buttonText = qs(data.vbutton_text().value_or_empty()),
|
||||
.canReport = data.is_can_report(),
|
||||
};
|
||||
const auto from = SponsoredFrom{
|
||||
.title = qs(data.vtitle()),
|
||||
.link = qs(data.vurl()),
|
||||
.buttonText = qs(data.vbutton_text()),
|
||||
.photoId = data.vphoto()
|
||||
? history->session().data().processPhoto(*data.vphoto())->id
|
||||
: PhotoId(0),
|
||||
.backgroundEmojiId = data.vcolor().has_value()
|
||||
? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
|
||||
: uint64(0),
|
||||
.colorIndex = uint8(data.vcolor().has_value()
|
||||
? data.vcolor()->data().vcolor().value_or_empty()
|
||||
: 0),
|
||||
.isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())),
|
||||
.isRecommended = data.is_recommended(),
|
||||
.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()
|
||||
? tr::lng_sponsored_info_submenu(
|
||||
tr::now,
|
||||
|
@ -370,9 +311,7 @@ void SponsoredMessages::append(
|
|||
data.ventities().value_or_empty()),
|
||||
},
|
||||
.history = history,
|
||||
.msgId = data.vchannel_post().value_or_empty(),
|
||||
.chatInviteHash = hash,
|
||||
.externalLink = externalLink,
|
||||
.link = from.link,
|
||||
.sponsorInfo = std::move(sponsorInfo),
|
||||
.additionalInfo = std::move(additionalInfo),
|
||||
};
|
||||
|
@ -444,7 +383,6 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
|
|||
return {};
|
||||
}
|
||||
const auto &data = entryPtr->sponsored;
|
||||
const auto &hash = data.chatInviteHash;
|
||||
|
||||
using InfoList = std::vector<TextWithEntities>;
|
||||
auto info = (!data.sponsorInfo.text.isEmpty()
|
||||
|
@ -456,20 +394,13 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
|
|||
? InfoList{ data.additionalInfo }
|
||||
: InfoList{};
|
||||
return {
|
||||
.hash = hash.isEmpty() ? std::nullopt : std::make_optional(hash),
|
||||
.peer = data.from.peer,
|
||||
.msgId = data.msgId,
|
||||
.info = std::move(info),
|
||||
.externalLink = data.externalLink,
|
||||
.isForceUserpicDisplay = data.from.isForceUserpicDisplay,
|
||||
.buttonText = !data.from.buttonText.isEmpty()
|
||||
? data.from.buttonText
|
||||
: !data.externalLink.isEmpty()
|
||||
? tr::lng_view_button_external_link(tr::now)
|
||||
: data.from.botLinkInfo
|
||||
? tr::lng_view_button_bot(tr::now)
|
||||
: QString(),
|
||||
.botLinkInfo = data.from.botLinkInfo,
|
||||
.link = data.link,
|
||||
.buttonText = data.from.buttonText,
|
||||
.photoId = data.from.photoId,
|
||||
.backgroundEmojiId = data.from.backgroundEmojiId,
|
||||
.colorIndex = data.from.colorIndex,
|
||||
.isLinkInternal = data.from.isLinkInternal,
|
||||
.canReport = data.from.canReport,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -40,19 +40,14 @@ struct SponsoredReportResult final {
|
|||
};
|
||||
|
||||
struct SponsoredFrom {
|
||||
PeerData *peer = nullptr;
|
||||
QString title;
|
||||
bool isBroadcast = false;
|
||||
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 link;
|
||||
QString buttonText;
|
||||
PhotoId photoId = PhotoId(0);
|
||||
uint64 backgroundEmojiId = 0;
|
||||
uint8 colorIndex : 6 = 0;
|
||||
bool isLinkInternal = false;
|
||||
bool isRecommended = false;
|
||||
bool canReport = false;
|
||||
};
|
||||
|
||||
|
@ -61,9 +56,7 @@ struct SponsoredMessage {
|
|||
SponsoredFrom from;
|
||||
TextWithEntities textWithEntities;
|
||||
History *history = nullptr;
|
||||
MsgId msgId;
|
||||
QString chatInviteHash;
|
||||
QString externalLink;
|
||||
QString link;
|
||||
TextWithEntities sponsorInfo;
|
||||
TextWithEntities additionalInfo;
|
||||
};
|
||||
|
@ -76,20 +69,17 @@ public:
|
|||
InjectToMiddle,
|
||||
};
|
||||
struct Details {
|
||||
std::optional<QString> hash;
|
||||
PeerData *peer = nullptr;
|
||||
MsgId msgId;
|
||||
std::vector<TextWithEntities> info;
|
||||
QString externalLink;
|
||||
bool isForceUserpicDisplay = false;
|
||||
QString link;
|
||||
QString buttonText;
|
||||
std::optional<Window::PeerByLinkInfo> botLinkInfo;
|
||||
PhotoId photoId = PhotoId(0);
|
||||
uint64 backgroundEmojiId = 0;
|
||||
uint8 colorIndex : 6 = 0;
|
||||
bool isLinkInternal = false;
|
||||
bool canReport = false;
|
||||
};
|
||||
using RandomId = QByteArray;
|
||||
explicit SponsoredMessages(not_null<Main::Session*> session);
|
||||
SponsoredMessages(const SponsoredMessages &other) = delete;
|
||||
SponsoredMessages &operator=(const SponsoredMessages &other) = delete;
|
||||
~SponsoredMessages();
|
||||
|
||||
[[nodiscard]] bool canHaveFor(not_null<History*> history) const;
|
||||
|
@ -112,6 +102,8 @@ public:
|
|||
[[nodiscard]] auto createReportCallback(const FullMsgId &fullId)
|
||||
-> Fn<void(SponsoredReportResult::Id, Fn<void(SponsoredReportResult)>)>;
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
|
||||
struct Entry {
|
||||
|
|
284
Telegram/SourceFiles/data/components/top_peers.cpp
Normal 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
|
57
Telegram/SourceFiles/data/components/top_peers.h
Normal 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
|
|
@ -165,6 +165,7 @@ void ChannelData::setFlags(ChannelDataFlags which) {
|
|||
const auto taken = ((diff & Flag::Forum) && !(which & Flag::Forum))
|
||||
? mgInfo->takeForumData()
|
||||
: nullptr;
|
||||
const auto wasIn = amIn();
|
||||
if ((diff & Flag::Forum) && (which & Flag::Forum)) {
|
||||
mgInfo->ensureForum(this);
|
||||
}
|
||||
|
@ -174,6 +175,14 @@ void ChannelData::setFlags(ChannelDataFlags which) {
|
|||
session().changes().peerUpdated(chat, 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 (const auto history = this->owner().historyLoaded(this)) {
|
||||
|
@ -1198,10 +1207,14 @@ void ApplyChannelUpdate(
|
|||
}
|
||||
channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
|
||||
channel->setTranslationDisabled(update.is_translations_disabled());
|
||||
|
||||
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
|
||||
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 {
|
||||
channel->setAllowedReactions({});
|
||||
channel->setAllowedReactions({ .maxCount = reactionsLimit });
|
||||
}
|
||||
channel->owner().stories().apply(channel, update.vstories());
|
||||
channel->fullUpdated();
|
||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "data/data_chat.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
|
@ -132,6 +133,18 @@ void ChatData::invalidateParticipants() {
|
|||
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) {
|
||||
_inviteLink = newInviteLink;
|
||||
}
|
||||
|
@ -471,10 +484,13 @@ void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
|
|||
chat->checkFolder(update.vfolder_id().value_or_empty());
|
||||
chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
|
||||
chat->setTranslationDisabled(update.is_translations_disabled());
|
||||
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
|
||||
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 {
|
||||
chat->setAllowedReactions({});
|
||||
chat->setAllowedReactions({ .maxCount = reactionsLimit });
|
||||
}
|
||||
chat->fullUpdated();
|
||||
chat->setAbout(qs(update.vabout()));
|
||||
|
|
|
@ -42,9 +42,7 @@ public:
|
|||
return (count > 0 || amIn()) && participants.empty();
|
||||
}
|
||||
|
||||
void setFlags(ChatDataFlags which) {
|
||||
_flags.set(which);
|
||||
}
|
||||
void setFlags(ChatDataFlags which);
|
||||
void addFlags(ChatDataFlags which) {
|
||||
_flags.add(which);
|
||||
}
|
||||
|
|
|
@ -129,9 +129,12 @@ void ClearPeerCloudDraft(
|
|||
history->applyCloudDraft(topicRootId);
|
||||
}
|
||||
|
||||
void SetChatLinkDraft(
|
||||
not_null<PeerData*> peer,
|
||||
const TextWithEntities &draft) {
|
||||
void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) {
|
||||
static const auto kInlineStart = QRegularExpression("^@[a-zA-Z0-9_]");
|
||||
if (kInlineStart.match(draft.text).hasMatch()) {
|
||||
draft = TextWithEntities().append(' ').append(std::move(draft));
|
||||
}
|
||||
|
||||
const auto textWithTags = TextWithTags{
|
||||
draft.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(draft.entities)
|
||||
|
|
|
@ -205,8 +205,6 @@ using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;
|
|||
&& (a->webpage == b->webpage);
|
||||
}
|
||||
|
||||
void SetChatLinkDraft(
|
||||
not_null<PeerData*> peer,
|
||||
const TextWithEntities &draft);
|
||||
void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft);
|
||||
|
||||
} // namespace Data
|
||||
|
|
|
@ -153,6 +153,11 @@ auto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {
|
|||
return _statusGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::stickerGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestStickerGroups();
|
||||
return _stickerGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::profilePhotoGroupsValue() const
|
||||
-> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestProfilePhotoGroups();
|
||||
|
@ -172,6 +177,12 @@ void EmojiStatuses::requestStatusGroups() {
|
|||
MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestStickerGroups() {
|
||||
requestGroups(
|
||||
&_stickerGroups,
|
||||
MTPmessages_GetEmojiStickerGroups(MTP_int(_stickerGroups.hash)));
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestProfilePhotoGroups() {
|
||||
requestGroups(
|
||||
&_profilePhotoGroups,
|
||||
|
@ -185,15 +196,24 @@ void EmojiStatuses::requestProfilePhotoGroups() {
|
|||
auto result = std::vector<Ui::EmojiGroup>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &group : list) {
|
||||
const auto &data = group.data();
|
||||
auto emoticons = ranges::views::all(
|
||||
data.vemoticons().v
|
||||
) | 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),
|
||||
group.match([&](const MTPDemojiGroupPremium &data) {
|
||||
result.push_back({
|
||||
.iconId = QString::number(data.vicon_emoji_id().v),
|
||||
.type = Ui::EmojiGroupType::Premium,
|
||||
});
|
||||
}, [&](const auto &data) {
|
||||
auto emoticons = ranges::views::all(
|
||||
data.vemoticons().v
|
||||
) | 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;
|
||||
|
|
|
@ -60,9 +60,11 @@ public:
|
|||
using Groups = std::vector<Ui::EmojiGroup>;
|
||||
[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> stickerGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> profilePhotoGroupsValue() const;
|
||||
void requestEmojiGroups();
|
||||
void requestStatusGroups();
|
||||
void requestStickerGroups();
|
||||
void requestProfilePhotoGroups();
|
||||
|
||||
private:
|
||||
|
@ -124,6 +126,7 @@ private:
|
|||
|
||||
GroupsType _emojiGroups;
|
||||
GroupsType _statusGroups;
|
||||
GroupsType _stickerGroups;
|
||||
GroupsType _profilePhotoGroups;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
|
|
@ -57,6 +57,8 @@ struct FileReferenceAccumulator {
|
|||
push(data.vstory());
|
||||
}, [&](const MTPDwebPageAttributeTheme &data) {
|
||||
push(data.vdocuments());
|
||||
}, [&](const MTPDwebPageAttributeStickerSet &data) {
|
||||
push(data.vstickers());
|
||||
});
|
||||
}
|
||||
void push(const MTPWebPage &data) {
|
||||
|
|
|
@ -1311,8 +1311,9 @@ std::unique_ptr<HistoryView::Media> MediaContact::createView(
|
|||
|
||||
MediaLocation::MediaLocation(
|
||||
not_null<HistoryItem*> parent,
|
||||
const LocationPoint &point)
|
||||
: MediaLocation(parent, point, QString(), QString()) {
|
||||
const LocationPoint &point,
|
||||
TimeId livePeriod)
|
||||
: MediaLocation({}, parent, point, livePeriod, QString(), QString()) {
|
||||
}
|
||||
|
||||
MediaLocation::MediaLocation(
|
||||
|
@ -1320,17 +1321,30 @@ MediaLocation::MediaLocation(
|
|||
const LocationPoint &point,
|
||||
const QString &title,
|
||||
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)
|
||||
, _point(point)
|
||||
, _location(parent->history()->owner().location(point))
|
||||
, _livePeriod(livePeriod)
|
||||
, _title(title)
|
||||
, _description(description) {
|
||||
}
|
||||
|
||||
std::unique_ptr<Media> MediaLocation::clone(not_null<HistoryItem*> parent) {
|
||||
return std::make_unique<MediaLocation>(
|
||||
PrivateTag(),
|
||||
parent,
|
||||
_point,
|
||||
_livePeriod,
|
||||
_title,
|
||||
_description);
|
||||
}
|
||||
|
@ -1339,8 +1353,14 @@ CloudImage *MediaLocation::location() const {
|
|||
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 {
|
||||
const auto type = tr::lng_maps_point(tr::now);
|
||||
const auto type = typeString();
|
||||
const auto hasMiniImages = false;
|
||||
const auto text = TextWithEntities{ .text = _title };
|
||||
return {
|
||||
|
@ -1349,9 +1369,7 @@ ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
|
|||
}
|
||||
|
||||
TextWithEntities MediaLocation::notificationText() const {
|
||||
return WithCaptionNotificationText(
|
||||
tr::lng_maps_point(tr::now),
|
||||
{ .text = _title });
|
||||
return WithCaptionNotificationText(typeString(), { .text = _title });
|
||||
}
|
||||
|
||||
QString MediaLocation::pinnedTextSubstring() const {
|
||||
|
@ -1360,7 +1378,7 @@ QString MediaLocation::pinnedTextSubstring() const {
|
|||
|
||||
TextForMimeData MediaLocation::clipboardText() const {
|
||||
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(
|
||||
_title,
|
||||
Ui::WebpageTextTitleOptions().flags);
|
||||
|
@ -1389,12 +1407,19 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
|
|||
not_null<HistoryView::Element*> message,
|
||||
not_null<HistoryItem*> realParent,
|
||||
HistoryView::Element *replacing) {
|
||||
return std::make_unique<HistoryView::Location>(
|
||||
message,
|
||||
_location,
|
||||
_point,
|
||||
_title,
|
||||
_description);
|
||||
return _livePeriod
|
||||
? std::make_unique<HistoryView::Location>(
|
||||
message,
|
||||
_location,
|
||||
_point,
|
||||
replacing,
|
||||
_livePeriod)
|
||||
: std::make_unique<HistoryView::Location>(
|
||||
message,
|
||||
_location,
|
||||
_point,
|
||||
_title,
|
||||
_description);
|
||||
}
|
||||
|
||||
MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
|
||||
|
@ -1857,23 +1882,21 @@ TextWithEntities MediaPoll::notificationText() const {
|
|||
}
|
||||
|
||||
QString MediaPoll::pinnedTextSubstring() const {
|
||||
return QChar(171) + _poll->question + QChar(187);
|
||||
return QChar(171) + _poll->question.text + QChar(187);
|
||||
}
|
||||
|
||||
TextForMimeData MediaPoll::clipboardText() const {
|
||||
const auto text = u"[ "_q
|
||||
+ tr::lng_in_dlg_poll(tr::now)
|
||||
+ u" : "_q
|
||||
+ _poll->question
|
||||
+ u" ]"_q
|
||||
+ ranges::accumulate(
|
||||
ranges::views::all(
|
||||
_poll->answers
|
||||
) | ranges::views::transform([](const PollAnswer &answer) {
|
||||
return "\n- " + answer.text;
|
||||
}),
|
||||
QString());
|
||||
return TextForMimeData::Simple(text);
|
||||
auto result = TextWithEntities();
|
||||
result
|
||||
.append(u"[ "_q)
|
||||
.append(tr::lng_in_dlg_poll(tr::now))
|
||||
.append(u" : "_q)
|
||||
.append(_poll->question)
|
||||
.append(u" ]"_q);
|
||||
for (const auto &answer : _poll->answers) {
|
||||
result.append(u"\n- "_q).append(answer.text);
|
||||
}
|
||||
return TextForMimeData::Rich(std::move(result));
|
||||
}
|
||||
|
||||
bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) {
|
||||
|
@ -2344,9 +2367,7 @@ const GiveawayResults *MediaGiveawayResults::giveawayResults() const {
|
|||
}
|
||||
|
||||
TextWithEntities MediaGiveawayResults::notificationText() const {
|
||||
return {
|
||||
.text = tr::lng_prizes_results_title(tr::now),
|
||||
};
|
||||
return Ui::Text::Colorized({ tr::lng_prizes_results_title(tr::now) });
|
||||
}
|
||||
|
||||
QString MediaGiveawayResults::pinnedTextSubstring() const {
|
||||
|
|
|
@ -331,10 +331,14 @@ private:
|
|||
};
|
||||
|
||||
class MediaLocation final : public Media {
|
||||
struct PrivateTag {
|
||||
};
|
||||
|
||||
public:
|
||||
MediaLocation(
|
||||
not_null<HistoryItem*> parent,
|
||||
const LocationPoint &point);
|
||||
const LocationPoint &point,
|
||||
TimeId livePeriod = 0);
|
||||
MediaLocation(
|
||||
not_null<HistoryItem*> parent,
|
||||
const LocationPoint &point,
|
||||
|
@ -356,9 +360,21 @@ public:
|
|||
not_null<HistoryItem*> realParent,
|
||||
HistoryView::Element *replacing = nullptr) override;
|
||||
|
||||
MediaLocation(
|
||||
PrivateTag,
|
||||
not_null<HistoryItem*> parent,
|
||||
const LocationPoint &point,
|
||||
TimeId livePeriod,
|
||||
const QString &title,
|
||||
const QString &description);
|
||||
|
||||
private:
|
||||
|
||||
[[nodiscard]] QString typeString() const;
|
||||
|
||||
LocationPoint _point;
|
||||
not_null<CloudImage*> _location;
|
||||
TimeId _livePeriod = 0;
|
||||
QString _title;
|
||||
QString _description;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/session/send_as_peers.h"
|
||||
|
@ -142,7 +143,14 @@ PossibleItemReactionsRef LookupPossibleReactions(
|
|||
return {};
|
||||
}
|
||||
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 reactions = &session->data().reactions();
|
||||
const auto &full = reactions->list(Reactions::Type::Active);
|
||||
|
|
|
@ -104,7 +104,9 @@ bool operator<(
|
|||
bool operator==(
|
||||
const AllowedReactions &a,
|
||||
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) {
|
||||
|
@ -531,7 +533,9 @@ bool PeerData::canPinMessages() const {
|
|||
|
||||
bool PeerData::canCreatePolls() const {
|
||||
if (const auto user = asUser()) {
|
||||
return user->isBot() && !user->isSupport();
|
||||
return user->isBot()
|
||||
&& !user->isSupport()
|
||||
&& !user->isRepliesChat();
|
||||
}
|
||||
return Data::CanSend(this, ChatRestriction::SendPolls);
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ enum class AllowedReactionsType {
|
|||
struct AllowedReactions {
|
||||
std::vector<ReactionId> some;
|
||||
AllowedReactionsType type = AllowedReactionsType::Some;
|
||||
int maxCount = 0;
|
||||
};
|
||||
|
||||
bool operator<(const AllowedReactions &a, const AllowedReactions &b);
|
||||
|
|
|
@ -79,6 +79,10 @@ std::optional<QString> OnlineTextCommon(LastseenStatus status, TimeId now) {
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] int UniqueReactionsLimit(not_null<Main::AppConfig*> config) {
|
||||
return config->get<int>("reactions_uniq_max", 11);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
|
||||
rpl::producer<int> UniqueReactionsLimitValue(
|
||||
not_null<PeerData*> peer) {
|
||||
const auto config = &peer->session().appConfig();
|
||||
return config->value(
|
||||
) | rpl::map([=] {
|
||||
auto configValue = peer->session().appConfig().value(
|
||||
) | rpl::map([config = &peer->session().appConfig()] {
|
||||
return UniqueReactionsLimit(config);
|
||||
}) | 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
|
||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "data/data_poll.h"
|
||||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/call_delayed.h"
|
||||
|
@ -69,7 +70,12 @@ bool PollData::closeByTimer() {
|
|||
bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
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))
|
||||
| (poll.is_public_voters() ? Flag::PublicVotes : 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();
|
||||
auto newAnswers = ranges::views::all(
|
||||
poll.vanswers().v
|
||||
) | ranges::views::transform([](const MTPPollAnswer &data) {
|
||||
return data.match([](const MTPDpollAnswer &answer) {
|
||||
) | ranges::views::transform([&](const MTPPollAnswer &data) {
|
||||
return data.match([&](const MTPDpollAnswer &answer) {
|
||||
auto result = PollAnswer();
|
||||
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;
|
||||
});
|
||||
}) | ranges::views::take(
|
||||
|
@ -251,9 +262,11 @@ bool PollData::quiz() const {
|
|||
}
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
||||
const auto convert = [](const PollAnswer &answer) {
|
||||
const auto convert = [&](const PollAnswer &answer) {
|
||||
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));
|
||||
};
|
||||
auto answers = QVector<MTPPollAnswer>();
|
||||
|
@ -272,7 +285,9 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
|||
return MTP_poll(
|
||||
MTP_long(poll->id),
|
||||
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_int(poll->closePeriod),
|
||||
MTP_int(poll->closeDate));
|
||||
|
|
|
@ -16,7 +16,7 @@ class Session;
|
|||
} // namespace Main
|
||||
|
||||
struct PollAnswer {
|
||||
QString text;
|
||||
TextWithEntities text;
|
||||
QByteArray option;
|
||||
int votes = 0;
|
||||
bool chosen = false;
|
||||
|
@ -65,7 +65,7 @@ struct PollData {
|
|||
[[nodiscard]] bool quiz() const;
|
||||
|
||||
PollId id = 0;
|
||||
QString question;
|
||||
TextWithEntities question;
|
||||
std::vector<PollAnswer> answers;
|
||||
std::vector<not_null<PeerData*>> recentVoters;
|
||||
std::vector<QByteArray> sendingVotes;
|
||||
|
|
|
@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/business/data_business_info.h"
|
||||
#include "data/business/data_shortcut_messages.h"
|
||||
#include "data/components/scheduled_messages.h"
|
||||
#include "data/components/sponsored_messages.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "data/data_bot_app.h"
|
||||
|
@ -406,6 +407,7 @@ void Session::clear() {
|
|||
_histories->unloadAll();
|
||||
_shortcutMessages = nullptr;
|
||||
_session->scheduledMessages().clear();
|
||||
_session->sponsoredMessages().clear();
|
||||
_dependentMessages.clear();
|
||||
base::take(_messages);
|
||||
base::take(_nonChannelMessages);
|
||||
|
@ -757,8 +759,10 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
|
|||
result->setLoadedStatus(PeerData::LoadedStatus::Normal);
|
||||
}
|
||||
|
||||
if (status && !minimal) {
|
||||
const auto lastseen = LastseenFromMTP(*status, result->lastseen());
|
||||
if (!minimal) {
|
||||
const auto lastseen = status
|
||||
? LastseenFromMTP(*status, result->lastseen())
|
||||
: Data::LastseenStatus::LongAgo(false);
|
||||
if (result->updateLastseen(lastseen)) {
|
||||
flags |= UpdateFlag::OnlineStatus;
|
||||
}
|
||||
|
@ -3439,6 +3443,7 @@ not_null<WebPageData*> Session::processWebpage(
|
|||
nullptr,
|
||||
WebPageCollage(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
0,
|
||||
QString(),
|
||||
false,
|
||||
|
@ -3464,6 +3469,7 @@ not_null<WebPageData*> Session::webpage(
|
|||
nullptr,
|
||||
WebPageCollage(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
0,
|
||||
QString(),
|
||||
false,
|
||||
|
@ -3482,6 +3488,7 @@ not_null<WebPageData*> Session::webpage(
|
|||
DocumentData *document,
|
||||
WebPageCollage &&collage,
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
|
@ -3500,6 +3507,7 @@ not_null<WebPageData*> Session::webpage(
|
|||
document,
|
||||
std::move(collage),
|
||||
std::move(iv),
|
||||
std::move(stickerSet),
|
||||
duration,
|
||||
author,
|
||||
hasLargeMedia,
|
||||
|
@ -3540,7 +3548,9 @@ void Session::webpageApplyFields(
|
|||
const auto result = attribute.match([&](
|
||||
const MTPDwebPageAttributeTheme &data) {
|
||||
return lookupInAttribute(data);
|
||||
}, [&](const MTPDwebPageAttributeStory &data) {
|
||||
}, [](const MTPDwebPageAttributeStory &) {
|
||||
return (DocumentData*)nullptr;
|
||||
}, [](const MTPDwebPageAttributeStickerSet &) {
|
||||
return (DocumentData*)nullptr;
|
||||
});
|
||||
if (result) {
|
||||
|
@ -3550,6 +3560,29 @@ void Session::webpageApplyFields(
|
|||
}
|
||||
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 storyId = FullStoryId();
|
||||
if (const auto attributes = data.vattributes()) {
|
||||
|
@ -3641,6 +3674,7 @@ void Session::webpageApplyFields(
|
|||
: lookupThemeDocument()),
|
||||
WebPageCollage(this, data),
|
||||
std::move(iv),
|
||||
lookupStickerSet(),
|
||||
data.vduration().value_or_empty(),
|
||||
qs(data.vauthor().value_or_empty()),
|
||||
data.is_has_large_media(),
|
||||
|
@ -3660,6 +3694,7 @@ void Session::webpageApplyFields(
|
|||
DocumentData *document,
|
||||
WebPageCollage &&collage,
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
|
@ -3677,6 +3712,7 @@ void Session::webpageApplyFields(
|
|||
document,
|
||||
std::move(collage),
|
||||
std::move(iv),
|
||||
std::move(stickerSet),
|
||||
duration,
|
||||
author,
|
||||
hasLargeMedia,
|
||||
|
|
|
@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
class Image;
|
||||
class HistoryItem;
|
||||
struct WebPageCollage;
|
||||
struct WebPageStickerSet;
|
||||
enum class WebPageType : uint8;
|
||||
enum class NewMessageType;
|
||||
|
||||
|
@ -579,6 +580,7 @@ public:
|
|||
DocumentData *document,
|
||||
WebPageCollage &&collage,
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
|
@ -857,6 +859,7 @@ private:
|
|||
DocumentData *document,
|
||||
WebPageCollage &&collage,
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
|
|
|
@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
|
@ -81,6 +82,21 @@ using UpdateFlag = StoryUpdate::Flag;
|
|||
|
||||
} // 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 {
|
||||
return {
|
||||
.id = peer->id,
|
||||
|
@ -344,7 +360,7 @@ void Stories::clearArchive(not_null<PeerData*> peer) {
|
|||
_archive.erase(i);
|
||||
for (const auto &id : archive.ids.list) {
|
||||
if (const auto story = lookup({ peerId, id })) {
|
||||
if ((*story)->expired() && !(*story)->pinned()) {
|
||||
if ((*story)->expired() && !(*story)->inProfile()) {
|
||||
applyDeleted(peer, id);
|
||||
}
|
||||
}
|
||||
|
@ -562,8 +578,8 @@ void Stories::unregisterDependentMessage(
|
|||
void Stories::savedStateChanged(not_null<Story*> story) {
|
||||
const auto id = story->id();
|
||||
const auto peer = story->peer()->id;
|
||||
const auto pinned = story->pinned();
|
||||
if (pinned) {
|
||||
const auto inProfile = story->inProfile();
|
||||
if (inProfile) {
|
||||
auto &saved = _saved[peer];
|
||||
const auto added = saved.ids.list.emplace(id).second;
|
||||
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)) {
|
||||
const auto saved = &k->second;
|
||||
if (saved->ids.list.remove(id)) {
|
||||
|
@ -836,7 +852,7 @@ void Stories::applyDeleted(not_null<PeerData*> peer, StoryId id) {
|
|||
void Stories::applyExpired(FullStoryId id) {
|
||||
if (const auto maybeStory = lookup(id)) {
|
||||
const auto story = *maybeStory;
|
||||
if (!hasArchive(story->peer()) && !story->pinned()) {
|
||||
if (!hasArchive(story->peer()) && !story->inProfile()) {
|
||||
applyDeleted(story->peer(), id.story);
|
||||
return;
|
||||
}
|
||||
|
@ -1117,7 +1133,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
|
|||
}
|
||||
|
||||
const auto story = *maybeStory;
|
||||
if (story->expired() && story->pinned()) {
|
||||
if (story->expired() && story->inProfile()) {
|
||||
_incrementViewsPending[id.peer].emplace(id.story);
|
||||
if (!_incrementViewsTimer.isActive()) {
|
||||
_incrementViewsTimer.callOnce(kIncrementViewsDelay);
|
||||
|
@ -1714,6 +1730,10 @@ void Stories::savedLoadMore(PeerId peerId) {
|
|||
|
||||
const auto &data = result.data();
|
||||
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;
|
||||
for (const auto &story : data.vstories().v) {
|
||||
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());
|
||||
saved.loaded = data.vstories().v.empty();
|
||||
saved.total = saved.loaded ? ids : std::max(saved.total, ids);
|
||||
setPinnedToTop(peerId, std::move(pinnedToTop));
|
||||
_savedChanged.fire_copy(peerId);
|
||||
}).fail([=] {
|
||||
auto &saved = _saved[peerId];
|
||||
|
@ -1741,6 +1762,33 @@ void Stories::savedLoadMore(PeerId peerId) {
|
|||
}).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) {
|
||||
if (ids.empty()) {
|
||||
return;
|
||||
|
@ -1764,9 +1812,9 @@ void Stories::deleteList(const std::vector<FullStoryId> &ids) {
|
|||
}).send();
|
||||
}
|
||||
|
||||
void Stories::togglePinnedList(
|
||||
void Stories::toggleInProfileList(
|
||||
const std::vector<FullStoryId> &ids,
|
||||
bool pinned) {
|
||||
bool inProfile) {
|
||||
if (ids.empty()) {
|
||||
return;
|
||||
}
|
||||
|
@ -1785,7 +1833,7 @@ void Stories::togglePinnedList(
|
|||
api->request(MTPstories_TogglePinned(
|
||||
peer->input,
|
||||
MTP_vector<MTPint>(list),
|
||||
MTP_bool(pinned)
|
||||
MTP_bool(inProfile)
|
||||
)).done([=](const MTPVector<MTPint> &result) {
|
||||
const auto peerId = peer->id;
|
||||
auto &saved = _saved[peerId];
|
||||
|
@ -1799,8 +1847,8 @@ void Stories::togglePinnedList(
|
|||
for (const auto &id : result.v) {
|
||||
if (const auto maybeStory = lookup({ peerId, id.v })) {
|
||||
const auto story = *maybeStory;
|
||||
story->setPinned(pinned);
|
||||
if (pinned) {
|
||||
story->setInProfile(inProfile);
|
||||
if (inProfile) {
|
||||
const auto add = loaded || (id.v >= lastId);
|
||||
if (!add) {
|
||||
dirty = true;
|
||||
|
@ -1828,6 +1876,75 @@ void Stories::togglePinnedList(
|
|||
}).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(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
FullStoryId id,
|
||||
|
|
|
@ -33,12 +33,15 @@ class StoryPreload;
|
|||
|
||||
struct StoriesIds {
|
||||
base::flat_set<StoryId, std::greater<>> list;
|
||||
std::vector<StoryId> pinnedToTop;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StoriesIds&,
|
||||
const StoriesIds&) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<StoryId> RespectingPinned(const StoriesIds &ids);
|
||||
|
||||
struct StoriesSourceInfo {
|
||||
PeerId id = 0;
|
||||
TimeId last = 0;
|
||||
|
@ -131,7 +134,7 @@ public:
|
|||
explicit Stories(not_null<Session*> owner);
|
||||
~Stories();
|
||||
|
||||
static constexpr auto kPinnedToastDuration = 4 * crl::time(1000);
|
||||
static constexpr auto kInProfileToastDuration = 4 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
@ -205,7 +208,14 @@ public:
|
|||
void savedLoadMore(PeerId peerId);
|
||||
|
||||
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(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
FullStoryId id,
|
||||
|
@ -312,6 +322,9 @@ private:
|
|||
|
||||
void notifySourcesChanged(StorySourcesList list);
|
||||
void pushHiddenCountsToFolder();
|
||||
void setPinnedToTop(
|
||||
PeerId peerId,
|
||||
std::vector<StoryId> &&pinnedToTop);
|
||||
|
||||
[[nodiscard]] int pollingInterval(
|
||||
const PollingSettings &settings) const;
|
||||
|
|