diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index ca0c7bf09..6de53f12d 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -169,6 +169,8 @@ jobs: %TDESKTOP_BUILD_GENERATOR% ^ %TDESKTOP_BUILD_ARCH% ^ %TDESKTOP_BUILD_API% ^ + -D CMAKE_C_FLAGS="/WX" ^ + -D CMAKE_CXX_FLAGS="/WX" ^ -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^ -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^ -D DESKTOP_APP_NO_PDB=ON ^ diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c4dc6043d..91f9a4bed 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -341,6 +341,8 @@ PRIVATE boxes/edit_caption_box.h boxes/edit_privacy_box.cpp boxes/edit_privacy_box.h + boxes/gift_credits_box.cpp + boxes/gift_credits_box.h boxes/gift_premium_box.cpp boxes/gift_premium_box.h boxes/language_box.cpp @@ -544,6 +546,8 @@ PRIVATE data/business/data_shortcut_messages.h data/components/factchecks.cpp data/components/factchecks.h + data/components/location_pickers.cpp + data/components/location_pickers.h data/components/recent_peers.cpp data/components/recent_peers.h data/components/scheduled_messages.cpp @@ -1541,6 +1545,8 @@ PRIVATE ui/chat/choose_send_as.h ui/chat/choose_theme_controller.cpp ui/chat/choose_theme_controller.h + ui/controls/location_picker.cpp + ui/controls/location_picker.h ui/controls/silent_toggle.cpp ui/controls/silent_toggle.h ui/controls/userpic_button.cpp @@ -1562,6 +1568,10 @@ PRIVATE ui/image/image_location.h ui/image/image_location_factory.cpp ui/image/image_location_factory.h + ui/text/format_song_document_name.cpp + ui/text/format_song_document_name.h + ui/widgets/label_with_custom_emoji.cpp + ui/widgets/label_with_custom_emoji.h ui/countryinput.cpp ui/countryinput.h ui/dynamic_thumbnails.cpp @@ -1575,10 +1585,6 @@ PRIVATE ui/resize_area.h ui/search_field_controller.cpp ui/search_field_controller.h - ui/text/format_song_document_name.cpp - ui/text/format_song_document_name.h - ui/widgets/label_with_custom_emoji.cpp - ui/widgets/label_with_custom_emoji.h ui/unread_badge.cpp ui/unread_badge.h window/main_window.cpp @@ -1685,6 +1691,7 @@ PRIVATE qrc/telegram/animations.qrc qrc/telegram/export.qrc qrc/telegram/iv.qrc + qrc/telegram/picker.qrc qrc/telegram/telegram.qrc qrc/telegram/sounds.qrc winrc/Telegram.rc @@ -1915,9 +1922,14 @@ if (WIN32) /DELAYLOAD:propsys.dll ) if (QT_VERSION GREATER 6) + if (NOT build_winarm) + target_link_options(Telegram PRIVATE + /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll + ) + endif() + target_link_options(Telegram PRIVATE - /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll @@ -1934,7 +1946,7 @@ if (WIN32) /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll /DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll - /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll + # /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor /DELAYLOAD:authz.dll # Authz.lib /DELAYLOAD:comdlg32.dll /DELAYLOAD:dwrite.dll # DWrite.lib diff --git a/Telegram/Resources/icons/chat/filled_location.png b/Telegram/Resources/icons/chat/filled_location.png new file mode 100644 index 000000000..12cd2dcc8 Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location.png differ diff --git a/Telegram/Resources/icons/chat/filled_location@2x.png b/Telegram/Resources/icons/chat/filled_location@2x.png new file mode 100644 index 000000000..cdef3f274 Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location@2x.png differ diff --git a/Telegram/Resources/icons/chat/filled_location@3x.png b/Telegram/Resources/icons/chat/filled_location@3x.png new file mode 100644 index 000000000..11caf17ca Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png new file mode 100644 index 000000000..06a33dc3c Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png new file mode 100644 index 000000000..996851dbc Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png new file mode 100644 index 000000000..0863b33e8 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png differ diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js index bae02fe48..fda34772f 100644 --- a/Telegram/Resources/iv_html/page.js +++ b/Telegram/Resources/iv_html/page.js @@ -618,9 +618,6 @@ var IV = { element.getAnimations().forEach( (animation) => animation.finish()); }, - back: function () { - window.history.back(); - }, menuShown: function (shown) { var already = document.getElementById('menu_page_blocker'); if (already && shown) { diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9213a3203..9eb209ceb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1115,6 +1115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_faq" = "Telegram FAQ"; "lng_settings_faq_link" = "https://telegram.org/faq#general-questions"; "lng_settings_features" = "Telegram Features"; +"lng_settings_credits" = "Your Stars"; "lng_settings_logout" = "Log Out"; "lng_sure_logout" = "Are you sure you want to log out?"; @@ -1447,6 +1448,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_info_topic_title" = "Topic Info"; "lng_profile_enable_notifications" = "Notifications"; "lng_profile_send_message" = "Send Message"; +"lng_profile_open_app" = "Open App"; +"lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; +"lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; "lng_info_add_as_contact" = "Add to contacts"; "lng_profile_shared_media" = "Shared media"; "lng_profile_suggest_photo" = "Suggest Profile Photo"; @@ -1844,6 +1848,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_webview_data_done" = "You have just successfully transferred data from the «{text}» button to the bot."; "lng_action_gift_received" = "{user} sent you a gift for {cost}"; "lng_action_gift_received_me" = "You sent to {user} a gift for {cost}"; +"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; "lng_action_suggested_photo_me" = "You suggested {user} to use this profile photo."; "lng_action_suggested_photo" = "{user} suggests you to use this profile photo."; "lng_action_suggested_photo_button" = "View Photo"; @@ -1888,6 +1893,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_boost_apply#one" = "{from} boosted the group"; "lng_action_boost_apply#other" = "{from} boosted the group {count} times"; "lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?"; +"lng_action_payment_refunded" = "{peer} refunded back {amount}"; "lng_similar_channels_title" = "Similar channels"; "lng_similar_channels_view_all" = "View all"; @@ -2340,6 +2346,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_history_tab_out" = "Outgoing"; "lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; "lng_credits_summary_balance" = "Balance"; +"lng_credits_gift_button" = "Gift Stars to Friends"; "lng_credits_box_out_title" = "Confirm Your Purchase"; "lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?"; "lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?"; @@ -2355,6 +2362,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star"; "lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars"; "lng_credits_box_out_about" = "Review the {link} for Stars."; +"lng_credits_box_out_about_link" = "https://telegram.org/tos/stars"; "lng_credits_media_done_title" = "Media Unlocked"; "lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}."; "lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}."; @@ -2362,11 +2370,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance."; "lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance."; "lng_credits_box_history_entry_peer" = "Recipient"; +"lng_credits_box_history_entry_peer_in" = "From"; "lng_credits_box_history_entry_via" = "Via"; "lng_credits_box_history_entry_play_market" = "Play Market"; "lng_credits_box_history_entry_app_store" = "App Store"; "lng_credits_box_history_entry_fragment" = "Fragment"; +"lng_credits_box_history_entry_anonymous" = "Unknown User"; +"lng_credits_box_history_entry_gift_name" = "Received Gift"; +"lng_credits_box_history_entry_gift_sent" = "Sent Gift"; +"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}"; +"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}"; +"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}"; +"lng_credits_box_history_entry_gift_about_url" = "https://telegram.org/blog/telegram-stars"; "lng_credits_box_history_entry_ads" = "Ads Platform"; +"lng_credits_box_history_entry_premium_bot" = "Stars Top-Up"; +"lng_credits_box_history_entry_via_premium_bot" = "Premium Bot"; "lng_credits_box_history_entry_id" = "Transaction ID"; "lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard."; "lng_credits_box_history_entry_success_date" = "Transaction date"; @@ -2379,9 +2397,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; +"lng_credits_gift_title" = "Gift Telegram Stars"; + "lng_location_title" = "Location"; "lng_location_about" = "Display the location of your business on your account."; "lng_location_address" = "Enter Address"; +"lng_location_set_map" = "Set Location on Map"; "lng_location_fallback" = "You can set your location on the map from your mobile device."; "lng_hours_title" = "Business Hours"; @@ -2810,12 +2831,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_badge" = "x{amount}"; "lng_prizes_results_title" = "Winners Selected!"; +"lng_prizes_results_title_one" = "Winner Selected!"; "lng_prizes_results_about#one" = "**{count}** winner of the {link} was randomly selected by Telegram."; "lng_prizes_results_about#other" = "**{count}** winners of the {link} were randomly selected by Telegram."; "lng_prizes_results_link" = "Giveaway"; +"lng_prizes_results_winner" = "Winner"; "lng_prizes_results_winners" = "Winners"; "lng_prizes_results_more#one" = "and {count} more!"; "lng_prizes_results_more#other" = "and {count} more!"; +"lng_prizes_results_one" = "The winner received their gift link in a private message."; "lng_prizes_results_all" = "All winners received gift links in private messages."; "lng_prizes_results_some" = "Some winners couldn't be selected."; @@ -2845,6 +2869,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_link_pending_toast" = "Only the recipient can see the link."; "lng_gift_link_pending_footer" = "This link hasn't been activated yet."; +"lng_gift_stars_title#one" = "{count} Star"; +"lng_gift_stars_title#other" = "{count} Stars"; +"lng_gift_stars_outgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."; +"lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram."; + "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts."; "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; @@ -2920,6 +2949,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_masks_has_been_archived" = "Mask pack has been archived."; "lng_masks_installed" = "Mask pack has been installed."; "lng_emoji_nothing_found" = "No emoji found"; +"lng_stickers_context_reorder" = "Reorder"; +"lng_stickers_context_edit_name" = "Edit name"; +"lng_stickers_context_delete" = "Delete sticker"; +"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?"; +"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name"; +"lng_stickers_box_edit_name_about" = "Choose a name for your set."; +"lng_stickers_creator_badge" = "edit"; "lng_in_dlg_photo" = "Photo"; "lng_in_dlg_album" = "Album"; @@ -3151,6 +3187,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_close_warning_sure" = "Close anyway"; "lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time."; "lng_bot_add_to_side_menu_done" = "Bot added to the main menu."; +"lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps."; +"lng_bot_click_to_start" = "Click here to use this bot."; +"lng_bot_status_users#one" = "{count} user"; +"lng_bot_status_users#other" = "{count} users"; "lng_typing" = "typing"; "lng_user_typing" = "{user} is typing"; @@ -3186,6 +3226,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_unread_bar_some" = "Unread messages"; "lng_maps_point" = "Location"; +"lng_maps_select_on_map" = "Select on the Map"; +"lng_maps_point_send" = "Send This Location"; +"lng_maps_point_set" = "Set This Location"; +"lng_maps_or_choose" = "Or choose a venue"; +"lng_maps_places_in_area" = "Places in this area"; +"lng_maps_no_places" = "No places found"; +"lng_maps_choose_to_search" = "Choose location to see places nearby."; +"lng_maps_venues_source" = "Powered by Foursquare"; "lng_live_location" = "Live Location"; "lng_live_location_now" = "updated just now"; "lng_live_location_minutes#one" = "updated {count} minute ago"; @@ -3740,6 +3788,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_card_declined" = "Your card was declined."; "lng_payments_payment_failed" = "Payment failed. Your card has not been billed."; "lng_payments_precheckout_failed" = "The bot couldn't process your payment. Your card has not been billed."; +"lng_payments_precheckout_timeout" = "The bot didn't respond in time. Your card has not been billed."; +"lng_payments_precheckout_stars_failed" = "The bot couldn't process your payment."; +"lng_payments_precheckout_stars_timeout" = "The bot didn't respond in time."; "lng_payments_already_paid" = "You have already paid for this item."; "lng_payments_terms_title" = "Terms of Service"; @@ -5267,6 +5318,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_iv_join_channel" = "Join"; "lng_iv_window_title" = "Instant View"; "lng_iv_wrong_layout" = "Wrong layout?"; +"lng_iv_not_supported" = "This link appears to be invalid."; "lng_limit_download_title" = "Download speed limited"; "lng_limit_download_subscribe" = "Subscribe to {link} and increase download speed {increase}."; @@ -5295,12 +5347,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_none" = "Recent search results\nwill appear here."; "lng_recent_chats" = "Chats"; "lng_recent_channels" = "Channels"; +"lng_recent_apps" = "Apps"; "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_bot_apps_your" = "Apps you use"; +"lng_bot_apps_popular" = "Popular apps"; "lng_font_box_title" = "Choose font family"; "lng_font_default" = "Default"; diff --git a/Telegram/Resources/picker_html/picker.css b/Telegram/Resources/picker_html/picker.css new file mode 100644 index 000000000..ac3d5912b --- /dev/null +++ b/Telegram/Resources/picker_html/picker.css @@ -0,0 +1,120 @@ +:root { + --font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif; +} + +html { + width: 100%; + height: 100%; + padding: 0; + margin: 0; +} + +body { + font-family: var(--font-sans); + width: 100%; + height: 100%; + padding: 0; + margin: 0; + background-color: var(--td-window-bg); + color: var(--td-window-fg); +} + +html.custom_scroll ::-webkit-scrollbar { + border-radius: 5px !important; + border: 3px solid transparent !important; + background-color: var(--td-scroll-bg) !important; + background-clip: content-box !important; + width: 10px !important; +} +html.custom_scroll ::-webkit-scrollbar:hover { + background-color: var(--td-scroll-bg-over) !important; +} +html.custom_scroll ::-webkit-scrollbar-thumb { + border-radius: 5px !important; + border: 3px solid transparent !important; + background-color: var(--td-scroll-bar-bg) !important; + background-clip: content-box !important; +} +html.custom_scroll ::-webkit-scrollbar-thumb:hover { + background-color: var(--td-scroll-bar-bg-over) !important; +} + +#map { + position: relative; + width: 100%; + height: 100%; +} +#marker { + pointer-events: none; + display: none; + z-index: 2; + position: absolute; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; +} +#marker_drop { + margin-bottom: 0px; + transition: margin 160ms ease-in-out; +} +#marker_drop.moving { + margin-bottom: 24px; +} +#marker_shadow { + position: absolute; +} +#search_venues { + position: absolute; + left: 50%; + transform: translateX(-50%); + z-index: 2; + top: -30px; + transition: top 200ms ease-in-out; +} +#search_venues.shown { + top: 6px; +} +#search_venues_inner { + position: relative; + overflow: hidden; + font-size: 13px; + font-weight: 500; + background: var(--td-window-bg); + color: var(--td-window-active-text-fg); + cursor: pointer; + border-radius: 14px; + padding: 5px 12px 6px; + box-shadow: 0 0 3px 0px var(--td-history-to-down-shadow); +} +#search_venues_inner:hover { + background: var(--td-window-bg-over); +} +#search_venues_content { + position: relative; + z-index: 2; +} +#search_venues_content:before { + content: var(--td-lng-maps-places-in-area); +} +#search_venues_inner .ripple .inner { + position: absolute; + border-radius: 50%; + transform: scale(0); + opacity: 1; + animation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards; + background-color: var(--td-window-bg-ripple); +} +#search_venues_inner .ripple.hiding { + animation: fadeOut 200ms linear forwards; +} +@keyframes ripple { + to { + transform: scale(2); + } +} +@keyframes fadeOut { + to { + opacity: 0; + } +} diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js new file mode 100644 index 000000000..e44fd51a9 --- /dev/null +++ b/Telegram/Resources/picker_html/picker.js @@ -0,0 +1,199 @@ +var LocationPicker = { + startZoom: 14, + flySpeed: 2.4, + notify: function(message) { + if (window.external && window.external.invoke) { + window.external.invoke(JSON.stringify(message)); + } + }, + frameKeyDown: function (e) { + const keyW = (e.key === 'w') + || (e.code === 'KeyW') + || (e.keyCode === 87); + const keyQ = (e.key === 'q') + || (e.code === 'KeyQ') + || (e.keyCode === 81); + const keyM = (e.key === 'm') + || (e.code === 'KeyM') + || (e.keyCode === 77); + if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) { + e.preventDefault(); + LocationPicker.notify({ + event: 'keydown', + modifier: e.ctrlKey ? 'ctrl' : 'cmd', + key: keyW ? 'w' : keyQ ? 'q' : 'm', + }); + } else if (e.key === 'Escape' || e.keyCode === 27) { + e.preventDefault(); + LocationPicker.notify({ + event: 'keydown', + key: 'escape', + }); + } + }, + isNight: function() { + var html = document.getElementsByTagName('html')[0]; + return html.style.getPropertyValue('--td-night') == '1'; + }, + lightPreset: function() { + return LocationPicker.isNight() ? 'night' : 'day'; + }, + updateStyles: function (styles) { + if (LocationPicker.styles !== styles) { + LocationPicker.styles = styles; + document.getElementsByTagName('html')[0].style = styles; + + LocationPicker.map.setConfigProperty( + 'basemap', + 'lightPreset', + LocationPicker.lightPreset()); + } + }, + init: function (params) { + mapboxgl.accessToken = params.token; + if (params.protocol) { + mapboxgl.config.API_URL = params.protocol + '://domain/api.mapbox.com'; + } + + var options = { container: 'map', config: { + basemap: { lightPreset: LocationPicker.lightPreset() } + } }; + var center = params.center; + if (center) { + center = [center[1], center[0]]; + options.center = center; + options.zoom = LocationPicker.startZoom; + } else if (params.bounds) { + options.bounds = params.bounds; + center = new mapboxgl.LngLatBounds(params.bounds).getCenter(); + } else { + center = [0, 0]; + } + LocationPicker.map = new mapboxgl.Map(options); + LocationPicker.createMarker(center); + LocationPicker.trackMovement(); + LocationPicker.initSearchVenueRipple(); + }, + marker: function() { + return document.getElementById('marker_drop'); + }, + createMarker: function(center) { + document.getElementById('marker').style.display = 'flex'; + }, + clearMovingTimer: function() { + if (LocationPicker.clearMovingTimeoutId) { + clearTimeout(LocationPicker.clearMovingTimeoutId); + LocationPicker.clearMovingTimeoutId = 0; + } + }, + startMovingTimer: function(done) { + LocationPicker.clearMovingTimer(); + LocationPicker.clearMovingTimeoutId = setTimeout(done, 500); + }, + trackMovement: function() { + LocationPicker.map.on('movestart', function() { + LocationPicker.marker().classList.add('moving'); + LocationPicker.clearMovingTimer(); + LocationPicker.toggleSearchVenues(false); + LocationPicker.notify({ event: 'move_start' }); + }); + LocationPicker.map.on('moveend', function() { + LocationPicker.startMovingTimer(function() { + LocationPicker.marker().classList.remove('moving'); + LocationPicker.notify({ + event: 'move_end', + latitude: LocationPicker.map.getCenter().lat, + longitude: LocationPicker.map.getCenter().lng + }); + }); + }); + }, + narrowTo: function (point) { + LocationPicker.map.flyTo({ + center: [point[1], point[0]], + zoom: LocationPicker.startZoom, + speed: LocationPicker.flySpeed, + }); + }, + send: function () { + LocationPicker.notify({ + event: 'send', + latitude: LocationPicker.map.getCenter().lat, + longitude: LocationPicker.map.getCenter().lng + }); + }, + addRipple: function (button, x, y) { + const ripple = document.createElement('span'); + ripple.classList.add('ripple'); + + const inner = document.createElement('span'); + inner.classList.add('inner'); + + var rect = button.getBoundingClientRect(); + x -= rect.x; + y -= rect.y; + + const mx = button.clientWidth - x; + const my = button.clientHeight - y; + const sq1 = x * x + y * y; + const sq2 = mx * mx + y * y; + const sq3 = x * x + my * my; + const sq4 = mx * mx + my * my; + const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4)); + + inner.style.width = inner.style.height = `${2 * radius}px`; + inner.style.left = `${x - radius}px`; + inner.style.top = `${y - radius}px`; + inner.classList.add('inner'); + + ripple.addEventListener('animationend', function (e) { + if (e.animationName === 'fadeOut') { + ripple.remove(); + } + }); + + ripple.appendChild(inner); + button.appendChild(ripple); + }, + stopRipples: function (button) { + const id = button.id ? button.id : button; + button = document.getElementById(id); + const ripples = button.getElementsByClassName('ripple'); + for (var i = 0; i < ripples.length; ++i) { + const ripple = ripples[i]; + if (!ripple.classList.contains('hiding')) { + ripple.classList.add('hiding'); + } + } + }, + initSearchVenueRipple: function() { + var button = document.getElementById('search_venues_inner'); + button.addEventListener('mousedown', function (e) { + LocationPicker.addRipple(e.currentTarget, e.clientX, e.clientY); + LocationPicker.searchVenuesPressed = true; + }); + button.addEventListener('mouseup', function (e) { + const id = e.currentTarget.id; + setTimeout(function () { + LocationPicker.stopRipples(id); + }, 0); + if (LocationPicker.searchVenuesPressed) { + LocationPicker.searchVenuesPressed = false; + LocationPicker.toggleSearchVenues(false); + LocationPicker.notify({ + event: 'search_venues', + latitude: LocationPicker.map.getCenter().lat, + longitude: LocationPicker.map.getCenter().lng + }); + } + }); + button.addEventListener('mouseleave', function (e) { + LocationPicker.stopRipples(e.currentTarget); + LocationPicker.searchVenuesPressed = false; + }); + }, + toggleSearchVenues: function(shown) { + var button = document.getElementById('search_venues'); + button.classList.toggle('shown', shown); + }, +}; diff --git a/Telegram/Resources/qrc/telegram/picker.qrc b/Telegram/Resources/qrc/telegram/picker.qrc new file mode 100644 index 000000000..10a810aa9 --- /dev/null +++ b/Telegram/Resources/qrc/telegram/picker.qrc @@ -0,0 +1,6 @@ + + + ../../picker_html/picker.css + ../../picker_html/picker.js + + diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 53203464b..89f5f8761 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.3.2.0" /> Telegram Desktop Telegram Messenger LLP @@ -37,6 +37,9 @@ + + + fullId()); - BotGameUrlClickHandler(bot, scoreLink).onClick({ + BotGameUrlClickHandler(bot, link).onClick({ Qt::LeftButton, QVariant::fromValue(ClickHandlerContext{ .itemId = item->fullId(), @@ -492,20 +488,23 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { case ButtonType::WebView: { if (const auto bot = item->getMessageBot()) { - bot->session().attachWebView().request( - controller, - Api::SendAction(bot->owner().history(bot)), - bot, - { .text = button->text, .url = button->data }); + bot->session().attachWebView().open({ + .bot = bot, + .context = { .controller = controller }, + .button = { .text = button->text, .url = button->data }, + .source = InlineBots::WebViewSourceButton{ .simple = false }, + }); } } break; case ButtonType::SimpleWebView: { if (const auto bot = item->getMessageBot()) { - bot->session().attachWebView().requestSimple( - controller, - bot, - { .text = button->text, .url = button->data }); + bot->session().attachWebView().open({ + .bot = bot, + .context = { .controller = controller }, + .button = {.text = button->text, .url = button->data }, + .source = InlineBots::WebViewSourceButton{ .simple = true }, + }); } } break; } diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index cd8aa54e2..81f098d67 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -30,6 +30,10 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; + + friend inline bool operator==( + const SendOptions &, + const SendOptions &) = default; }; [[nodiscard]] SendOptions DefaultSendWhenOnlineOptions(); @@ -52,6 +56,10 @@ struct SendAction { MsgId replaceMediaOf = 0; [[nodiscard]] MTPInputReplyTo mtpReplyTo() const; + + friend inline bool operator==( + const SendAction &, + const SendAction &) = default; }; struct MessageToSend { diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 356e2cffa..727cee565 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -102,6 +102,7 @@ constexpr auto kTransactionsLimit = 100; : QDateTime(), .successLink = qs(tl.data().vtransaction_url().value_or_empty()), .in = (int64(tl.data().vstars().v) >= 0), + .gift = tl.data().is_gift(), }; } @@ -133,12 +134,12 @@ rpl::producer CreditsTopupOptions::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); - using TLOption = MTPStarsTopupOption; - _api.request(MTPpayments_GetStarsTopupOptions( - )).done([=](const MTPVector &result) { - _options = ranges::views::all( - result.v - ) | ranges::views::transform([](const TLOption &option) { + const auto giftBarePeerId = !_peer->isSelf() ? _peer->id.value : 0; + + const auto optionsFromTL = [giftBarePeerId](const auto &options) { + return ranges::views::all( + options + ) | ranges::views::transform([=](const auto &option) { return Data::CreditTopupOption{ .credits = option.data().vstars().v, .product = qs( @@ -146,12 +147,31 @@ rpl::producer CreditsTopupOptions::request() { .currency = qs(option.data().vcurrency()), .amount = option.data().vamount().v, .extended = option.data().is_extended(), + .giftBarePeerId = giftBarePeerId, }; }) | ranges::to_vector; - consumer.put_done(); - }).fail([=](const MTP::Error &error) { + }; + const auto fail = [=](const MTP::Error &error) { consumer.put_error_copy(error.type()); - }).send(); + }; + + if (_peer->isSelf()) { + using TLOption = MTPStarsTopupOption; + _api.request(MTPpayments_GetStarsTopupOptions( + )).done([=](const MTPVector &result) { + _options = optionsFromTL(result.v); + consumer.put_done(); + }).fail(fail).send(); + } else if (const auto user = _peer->asUser()) { + using TLOption = MTPStarsGiftOption; + _api.request(MTPpayments_GetStarsGiftOptions( + MTP_flags(MTPpayments_GetStarsGiftOptions::Flag::f_user_id), + user->inputUser + )).done([=](const MTPVector &result) { + _options = optionsFromTL(result.v); + consumer.put_done(); + }).fail(fail).send(); + } return lifetime; }; diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index f326275fd..662bfc980 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -128,7 +128,7 @@ mtpRequestId EditMessage( } if (updateRecentStickers) { - api->requestRecentStickersForce(true); + api->requestSpecialStickersForce(false, false, true); } }).fail([=](const MTP::Error &error, mtpRequestId requestId) { if constexpr (ErrorWithId) { @@ -153,9 +153,7 @@ mtpRequestId EditMessage( const auto &text = item->originalText(); const auto webpage = (!item->media() || !item->media()->webpage()) ? Data::WebPageDraft{ .removed = true } - : Data::WebPageDraft{ - .id = item->media()->webpage()->id, - }; + : Data::WebPageDraft::FromItem(item); return EditMessage( item, text, diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index a76cb4b67..46a5b7fa3 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -36,7 +36,8 @@ MTPVector ComposeSendingDocumentAttributes( MTP_double(document->duration() / 1000.), MTP_int(dimensions.width()), MTP_int(dimensions.height()), - MTPint())); // preload_prefix_size + MTPint(), // preload_prefix_size + MTPdouble())); // video_start_ts } else { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(dimensions.width()), diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index ade419248..c05a42e95 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -62,6 +62,79 @@ void InnerFillMessagePostFlags( } } +void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { + const auto history = action.history; + const auto peer = history->peer; + const auto session = &history->session(); + const auto api = &session->api(); + + action.clearDraft = false; + action.generateLocal = false; + api->sendAction(action); + + const auto randomId = base::RandomValue(); + + auto flags = NewMessageFlags(peer); + auto sendFlags = MTPmessages_SendMedia::Flags(0); + if (action.replyTo) { + flags |= MessageFlag::HasReplyInfo; + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; + } + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); + if (silentPost) { + sendFlags |= MTPmessages_SendMedia::Flag::f_silent; + } + const auto sendAs = action.options.sendAs; + if (sendAs) { + sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; + } + const auto messagePostAuthor = peer->isBroadcast() + ? session->user()->name() + : QString(); + + if (action.options.scheduled) { + flags |= MessageFlag::IsOrWasScheduled; + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } + if (action.options.invertCaption) { + flags |= MessageFlag::InvertMedia; + sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; + } + + auto &histories = history->owner().histories(); + histories.sendPreparedMessage( + history, + action.replyTo, + randomId, + Data::Histories::PrepareMessage( + MTP_flags(sendFlags), + peer->input, + Data::Histories::ReplyToPlaceholder(), + std::move(inputMedia), + MTPstring(), + MTP_long(randomId), + MTPReplyMarkup(), + MTPvector(), + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(session, action.options.shortcutId), + MTP_long(action.options.effectId) + ), [=](const MTPUpdates &result, const MTP::Response &response) { + }, [=](const MTP::Error &error, const MTP::Response &response) { + api->sendMessageFail(error, peer, randomId); + }); + + api->finishForwarding(action); +} + template void SendExistingMedia( MessageToSend &&message, @@ -362,6 +435,33 @@ bool SendDice(MessageToSend &message) { return true; } +void SendLocation(SendAction action, float64 lat, float64 lon) { + SendSimpleMedia( + action, + MTP_inputMediaGeoPoint( + MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(lat), + MTP_double(lon), + MTPint()))); // accuracy_radius +} + +void SendVenue(SendAction action, Data::InputVenue venue) { + SendSimpleMedia( + action, + MTP_inputMediaVenue( + MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(venue.lat), + MTP_double(venue.lon), + MTPint()), // accuracy_radius + MTP_string(venue.title), + MTP_string(venue.address), + MTP_string(venue.provider), + MTP_string(venue.id), + MTP_string(venue.venueType))); +} + void FillMessagePostFlags( const SendAction &action, not_null peer, diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h index 2fdbad843..c4bafc537 100644 --- a/Telegram/SourceFiles/api/api_sending.h +++ b/Telegram/SourceFiles/api/api_sending.h @@ -7,15 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Main { -class Session; -} // namespace Main - class History; class PhotoData; class DocumentData; struct FilePrepareResult; +namespace Data { +struct InputVenue; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + namespace Api { struct MessageToSend; @@ -33,6 +37,13 @@ void SendExistingPhoto( bool SendDice(MessageToSend &message); +// We can't create Data::LocationPoint() and use it +// for a local sending message, because we can't request +// map thumbnail in messages history without access hash. +void SendLocation(SendAction action, float64 lat, float64 lon); + +void SendVenue(SendAction action, Data::InputVenue venue); + void FillMessagePostFlags( const SendAction &action, not_null peer, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 9e465962b..4903d9892 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2629,7 +2629,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu void ApiWrap::updateStickers() { const auto now = crl::now(); requestStickers(now); - requestRecentStickers(now); + requestRecentStickers(now, false); requestFavedStickers(now); requestFeaturedStickers(now); } @@ -2651,8 +2651,15 @@ void ApiWrap::updateCustomEmoji() { requestFeaturedEmoji(now); } -void ApiWrap::requestRecentStickersForce(bool attached) { - requestRecentStickersWithHash(0, attached); +void ApiWrap::requestSpecialStickersForce( + bool faved, + bool recent, + bool attached) { + if (faved) { + requestFavedStickers(std::nullopt); + } else if (recent || attached) { + requestRecentStickers(std::nullopt, attached); + } } void ApiWrap::setGroupStickerSet( @@ -2805,18 +2812,17 @@ void ApiWrap::requestCustomEmoji(TimeId now) { }).send(); } -void ApiWrap::requestRecentStickers(TimeId now, bool attached) { - const auto needed = attached - ? _session->data().stickers().recentAttachedUpdateNeeded(now) - : _session->data().stickers().recentUpdateNeeded(now); +void ApiWrap::requestRecentStickers( + std::optional now, + bool attached) { + const auto needed = !now + ? true + : attached + ? _session->data().stickers().recentAttachedUpdateNeeded(*now) + : _session->data().stickers().recentUpdateNeeded(*now); if (!needed) { return; } - requestRecentStickersWithHash( - Api::CountRecentStickersHash(_session, attached), attached); -} - -void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) { const auto requestId = [=]() -> mtpRequestId & { return attached ? _recentAttachedStickersUpdateRequest @@ -2839,7 +2845,7 @@ void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) { : MTPmessages_getRecentStickers::Flags(0); requestId() = request(MTPmessages_GetRecentStickers( MTP_flags(flags), - MTP_long(hash) + MTP_long(now ? Api::CountRecentStickersHash(_session, attached) : 0) )).done([=](const MTPmessages_RecentStickers &result) { finish(); @@ -2866,13 +2872,15 @@ void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) { }).send(); } -void ApiWrap::requestFavedStickers(TimeId now) { - if (!_session->data().stickers().favedUpdateNeeded(now) - || _favedStickersUpdateRequest) { - return; +void ApiWrap::requestFavedStickers(std::optional now) { + if (now) { + if (!_session->data().stickers().favedUpdateNeeded(*now) + || _favedStickersUpdateRequest) { + return; + } } _favedStickersUpdateRequest = request(MTPmessages_GetFavedStickers( - MTP_long(Api::CountFavedStickersHash(_session)) + MTP_long(now ? Api::CountFavedStickersHash(_session) : 0) )).done([=](const MTPmessages_FavedStickers &result) { _session->data().stickers().setLastFavedUpdate(crl::now()); _favedStickersUpdateRequest = 0; @@ -4281,7 +4289,7 @@ void ApiWrap::sendMediaWithRandomId( ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { - requestRecentStickersForce(true); + requestRecentStickers(std::nullopt, true); } AyuWorker::markAsOnline(_session); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 15c0941c3..7259c410d 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -244,7 +244,10 @@ public: void updateSavedGifs(); void updateMasks(); void updateCustomEmoji(); - void requestRecentStickersForce(bool attached = false); + void requestSpecialStickersForce( + bool faved, + bool recent, + bool attached); void setGroupStickerSet( not_null megagroup, const StickerSetIdentifier &set); @@ -477,9 +480,10 @@ private: void requestStickers(TimeId now); void requestMasks(TimeId now); void requestCustomEmoji(TimeId now); - void requestRecentStickers(TimeId now, bool attached = false); - void requestRecentStickersWithHash(uint64 hash, bool attached = false); - void requestFavedStickers(TimeId now); + void requestRecentStickers( + std::optional now, + bool attached); + void requestFavedStickers(std::optional now); void requestFeaturedStickers(TimeId now); void requestFeaturedEmoji(TimeId now); void requestSavedGifs(TimeId now); diff --git a/Telegram/SourceFiles/boxes/about_box.cpp b/Telegram/SourceFiles/boxes/about_box.cpp index 94faa948c..6f22d75ad 100644 --- a/Telegram/SourceFiles/boxes/about_box.cpp +++ b/Telegram/SourceFiles/boxes/about_box.cpp @@ -100,6 +100,8 @@ void AboutBox::showVersionHistory() { url += u"win/%1.zip"_q; } else if (Platform::IsWindows64Bit()) { url += u"win64/%1.zip"_q; + } else if (Platform::IsWindowsARM64()) { + url += u"winarm/%1.zip"_q; } else if (Platform::IsMac()) { url += u"mac/%1.zip"_q; } else if (Platform::IsLinux()) { @@ -155,6 +157,8 @@ QString currentVersionText() { } if (Platform::IsWindows64Bit()) { result += " x64"; + } else if (Platform::IsWindowsARM64()) { + result += " arm64"; } return result; } diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 5af8577bf..4e99a01ba 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -39,6 +39,7 @@ Data::ChatFilter ChangedFilter( filter.id(), filter.title(), filter.iconEmoji(), + filter.colorIndex(), filter.flags(), std::move(always), filter.pinned(), @@ -58,6 +59,7 @@ Data::ChatFilter ChangedFilter( filter.id(), filter.title(), filter.iconEmoji(), + filter.colorIndex(), filter.flags(), std::move(always), filter.pinned(), diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index ce23d235f..df85e6254 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -83,6 +83,7 @@ not_null SetupChatsPreview( rules.id(), rules.title(), rules.iconEmoji(), + rules.colorIndex(), (rules.flags() & ~flag), rules.always(), rules.pinned(), @@ -104,6 +105,7 @@ not_null SetupChatsPreview( rules.id(), rules.title(), rules.iconEmoji(), + rules.colorIndex(), rules.flags(), std::move(always), std::move(pinned), @@ -170,6 +172,7 @@ void EditExceptions( rules.id(), rules.title(), rules.iconEmoji(), + rules.colorIndex(), ((rules.flags() & ~options) | rawController->chosenOptions()), include ? std::move(changed) : std::move(removeFrom), @@ -240,6 +243,7 @@ void CreateIconSelector( rules.id(), rules.title(), Ui::LookupFilterIcon(icon).emoji, + rules.colorIndex(), rules.flags(), rules.always(), rules.pinned(), diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp new file mode 100644 index 000000000..a2e36038a --- /dev/null +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -0,0 +1,180 @@ +/* +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/gift_credits_box.h" + +#include "api/api_credits.h" +#include "boxes/peer_list_controllers.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "settings/settings_credits_graphics.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_stars_colored.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/label_with_custom_emoji.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_channel_earn.h" +#include "styles/style_chat.h" +#include "styles/style_credits.h" +#include "styles/style_giveaway.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" + +namespace Ui { + +void GiftCreditsBox( + not_null box, + not_null peer, + Fn gifted) { + box->setStyle(st::creditsGiftBox); + box->setNoContentMargin(true); + box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); + + const auto content = box->setPinnedToTopContent( + object_ptr(box)); + + Ui::AddSkip(content); + Ui::AddSkip(content); + const auto &stUser = st::premiumGiftsUserpicButton; + const auto userpicWrap = content->add( + object_ptr>( + content, + object_ptr(content, peer, stUser))); + userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents); + Ui::AddSkip(content); + Ui::AddSkip(content); + + { + const auto widget = Ui::CreateChild(content); + using ColoredMiniStars = Ui::Premium::ColoredMiniStars; + const auto stars = widget->lifetime().make_state( + widget, + false, + Ui::Premium::MiniStars::Type::BiStars); + stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); + widget->resize( + st::boxWidth - stUser.photoSize, + stUser.photoSize * 2); + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + widget->moveToLeft(stUser.photoSize / 2, 0); + const auto starsRect = Rect(widget->size()); + stars->setPosition(starsRect.topLeft()); + stars->setSize(starsRect.size()); + widget->lower(); + }, widget->lifetime()); + widget->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(widget); + p.fillRect(r, Qt::transparent); + stars->paint(p); + }, widget->lifetime()); + } + { + Ui::AddSkip(content); + const auto arrow = Ui::Text::SingleCustomEmoji( + peer->owner().customEmojiManager().registerInternalEmoji( + st::topicButtonArrow, + st::channelEarnLearnArrowMargins, + false)); + auto link = tr::lng_credits_box_history_entry_gift_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_credits_box_history_entry_gift_about_url(tr::now)); + }); + content->add( + object_ptr>( + content, + Ui::CreateLabelWithCustomEmoji( + content, + tr::lng_credits_box_history_entry_gift_out_about( + lt_user, + rpl::single(TextWithEntities{ peer->shortName() }), + lt_link, + std::move(link), + Ui::Text::RichLangValue), + { .session = &peer->session() }, + st::creditsBoxAbout)), + st::boxRowPadding); + } + Ui::AddSkip(content); + Ui::AddSkip(box->verticalLayout()); + + Settings::FillCreditOptions( + Main::MakeSessionShow(box->uiShow(), &peer->session()), + box->verticalLayout(), + peer, + 0, + [=] { gifted(); box->uiShow()->hideLayer(); }); + + box->setPinnedToBottomContent( + object_ptr(box)); +} + +void ShowGiftCreditsBox( + not_null controller, + Fn gifted) { + + class Controller final : public ContactsBoxController { + public: + Controller( + not_null session, + Fn)> choose) + : ContactsBoxController(session) + , _choose(std::move(choose)) { + } + + protected: + std::unique_ptr createRow( + not_null user) override { + if (user->isSelf() + || user->isBot() + || user->isServiceUser() + || user->isInaccessible()) { + return nullptr; + } + return ContactsBoxController::createRow(user); + } + + void rowClicked(not_null row) override { + _choose(row->peer()); + } + + private: + const Fn)> _choose; + + }; + auto initBox = [=](not_null peersBox) { + peersBox->setTitle(tr::lng_credits_gift_title()); + peersBox->addButton(tr::lng_cancel(), [=] { peersBox->closeBox(); }); + }; + + const auto show = controller->uiShow(); + auto listController = std::make_unique( + &controller->session(), + [=](not_null peer) { + show->showBox(Box(GiftCreditsBox, peer, gifted)); + }); + show->showBox( + Box(std::move(listController), std::move(initBox)), + Ui::LayerOption::KeepOther); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.h b/Telegram/SourceFiles/boxes/gift_credits_box.h new file mode 100644 index 000000000..43b8556f3 --- /dev/null +++ b/Telegram/SourceFiles/boxes/gift_credits_box.h @@ -0,0 +1,20 @@ +/* +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 Window { +class SessionController; +} // namespace Window + +namespace Ui { + +void ShowGiftCreditsBox( + not_null controller, + Fn gifted); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 972a7a5f4..d478a7c6e 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -1014,6 +1014,7 @@ void GiftPremiumValidator::showChoosePeerBox(const QString &ref) { if (users.empty()) { show->showToast( tr::lng_settings_gift_premium_choose(tr::now)); + return; } const auto giftBox = show->show( Box(GiftsBox, _controller, users, api, ref)); @@ -1648,7 +1649,9 @@ void AddCreditsHistoryEntryTable( st::giveawayGiftCodeTableMargin); const auto peerId = PeerId(entry.barePeerId); if (peerId) { - auto text = tr::lng_credits_box_history_entry_peer(); + auto text = entry.in + ? tr::lng_credits_box_history_entry_peer_in() + : tr::lng_credits_box_history_entry_peer(); AddTableRow(table, std::move(text), controller, peerId); } if (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) { @@ -1692,14 +1695,24 @@ void AddCreditsHistoryEntryTable( } else if (entry.peerType == Type::Fragment) { AddTableRow( table, - tr::lng_credits_box_history_entry_via(), - tr::lng_credits_box_history_entry_fragment( - Ui::Text::RichLangValue)); + (entry.gift + ? tr::lng_credits_box_history_entry_peer_in + : tr::lng_credits_box_history_entry_via)(), + (entry.gift + ? tr::lng_credits_box_history_entry_anonymous + : tr::lng_credits_box_history_entry_fragment)( + Ui::Text::RichLangValue)); } else if (entry.peerType == Type::Ads) { AddTableRow( table, tr::lng_credits_box_history_entry_via(), tr::lng_credits_box_history_entry_ads(Ui::Text::RichLangValue)); + } else if (entry.peerType == Type::PremiumBot) { + AddTableRow( + table, + tr::lng_credits_box_history_entry_via(), + tr::lng_credits_box_history_entry_via_premium_bot( + Ui::Text::RichLangValue)); } if (!entry.id.isEmpty()) { constexpr auto kOneLineCount = 18; diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 70648d627..a3bc8b69b 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -459,6 +459,7 @@ Ui::BoostFeatures LookupBoostFeatures(not_null channel) { .customWallpaperLevel = group ? levelLimits.groupCustomWallpaperLevelMin() : levelLimits.channelCustomWallpaperLevelMin(), + .sponsoredLevel = levelLimits.channelRestrictSponsoredLevelMin(), }; } diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 451957c21..8febb5f42 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -418,7 +418,9 @@ void SimpleLimitBox( BoxShowFinishes(box), 0, descriptor.current, - descriptor.premiumLimit, + (descriptor.complexRatio + ? descriptor.premiumLimit + : 2 * descriptor.current), premiumPossible, descriptor.phrase, descriptor.icon); @@ -769,7 +771,7 @@ void FilterLinksLimitBox( premiumLimit, &st::premiumIconChats, std::nullopt, - true }); + /*true */}); // Don't use real ratio, "Free" doesn't fit. } @@ -856,7 +858,7 @@ void ShareableFiltersLimitBox( premiumLimit, &st::premiumIconFolders, std::nullopt, - true }); + /*true*/ }); // Don't use real ratio, "Free" doesn't fit. } void FilterPinsLimitBox( diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index 39d7983d7..a8771cc17 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" #include "settings/settings_credits_graphics.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg. @@ -257,6 +258,8 @@ void SendCreditsBox( if (state->confirmButtonBusy.current()) { return; } + const auto show = box->uiShow(); + const auto weak = MakeWeak(box.get()); state->confirmButtonBusy = true; session->api().request( MTPpayments_SendStarsForm( @@ -264,12 +267,31 @@ void SendCreditsBox( MTP_long(form->formId), form->inputInvoice) ).done([=](auto result) { - state->confirmButtonBusy = false; - box->closeBox(); + if (weak) { + state->confirmButtonBusy = false; + box->closeBox(); + } sent(); }).fail([=](const MTP::Error &error) { - state->confirmButtonBusy = false; - box->uiShow()->showToast(error.type()); + if (weak) { + state->confirmButtonBusy = false; + } + const auto id = error.type(); + if (id == u"BOT_PRECHECKOUT_FAILED"_q) { + auto error = ::Ui::MakeInformBox( + tr::lng_payments_precheckout_stars_failed(tr::now)); + error->boxClosing() | rpl::start_with_next([=] { + if (const auto paybox = weak.data()) { + paybox->closeBox(); + } + }, error->lifetime()); + show->showBox(std::move(error)); + } else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) { + show->showToast( + tr::lng_payments_precheckout_stars_timeout(tr::now)); + } else { + show->showToast(id); + } }).send(); }); { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index d86e24510..520ac1ac6 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1414,55 +1414,6 @@ std::vector> ShareBox::Inner::selected() const { return result; } -QString AppendShareGameScoreUrl( - not_null session, - const QString &url, - const FullMsgId &fullId) { - auto shareHashData = QByteArray(0x20, Qt::Uninitialized); - auto shareHashDataInts = reinterpret_cast(shareHashData.data()); - const auto peer = fullId.peer - ? session->data().peerLoaded(fullId.peer) - : static_cast(nullptr); - const auto channelAccessHash = uint64((peer && peer->isChannel()) - ? peer->asChannel()->access - : 0); - shareHashDataInts[0] = session->userId().bare; - shareHashDataInts[1] = fullId.peer.value; - shareHashDataInts[2] = uint64(fullId.msg.bare); - shareHashDataInts[3] = channelAccessHash; - - // Count SHA1() of data. - auto key128Size = 0x10; - auto shareHashEncrypted = QByteArray(key128Size + shareHashData.size(), Qt::Uninitialized); - hashSha1(shareHashData.constData(), shareHashData.size(), shareHashEncrypted.data()); - - //// Mix in channel access hash to the first 64 bits of SHA1 of data. - //*reinterpret_cast(shareHashEncrypted.data()) ^= channelAccessHash; - - // Encrypt data. - if (!session->local().encrypt(shareHashData.constData(), shareHashEncrypted.data() + key128Size, shareHashData.size(), shareHashEncrypted.constData())) { - return url; - } - - auto shareHash = shareHashEncrypted.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - auto shareUrl = u"tg://share_game_score?hash="_q + QString::fromLatin1(shareHash); - - auto shareComponent = u"tgShareScoreUrl="_q + qthelp::url_encode(shareUrl); - - auto hashPosition = url.indexOf('#'); - if (hashPosition < 0) { - return url + '#' + shareComponent; - } - auto hash = url.mid(hashPosition + 1); - if (hash.indexOf('=') >= 0 || hash.indexOf('?') >= 0) { - return url + '&' + shareComponent; - } - if (!hash.isEmpty()) { - return url + '?' + shareComponent; - } - return url + shareComponent; -} - ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs( const std::vector> &result, const MessageIdsList &msgIds) { @@ -1624,9 +1575,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( } void FastShareMessage( - not_null controller, + std::shared_ptr show, not_null item) { - const auto show = controller->uiShow(); const auto history = item->history(); const auto owner = &history->owner(); const auto session = &history->session(); @@ -1655,7 +1605,7 @@ void FastShareMessage( } if (item->hasDirectLink()) { using namespace HistoryView; - CopyPostLink(controller, item->fullId(), Context::History); + CopyPostLink(show, item->fullId(), Context::History); } else if (const auto bot = item->getMessageBot()) { if (const auto media = item->media()) { if (const auto game = media->game()) { @@ -1687,23 +1637,27 @@ void FastShareMessage( auto copyLinkCallback = canCopyLink ? Fn(std::move(copyCallback)) : Fn(); - controller->show( - Box(ShareBox::Descriptor{ - .session = session, - .copyCallback = std::move(copyLinkCallback), - .submitCallback = ShareBox::DefaultForwardCallback( - show, - history, - msgIds), - .filterCallback = std::move(filterCallback), - .forwardOptions = { - .sendersCount = ItemsForwardSendersCount(items), - .captionsCount = ItemsForwardCaptionsCount(items), - .show = !hasOnlyForcedForwardedInfo, - }, - .premiumRequiredError = SharePremiumRequiredError(), - }), - Ui::LayerOption::CloseOther); + show->show(Box(ShareBox::Descriptor{ + .session = session, + .copyCallback = std::move(copyLinkCallback), + .submitCallback = ShareBox::DefaultForwardCallback( + show, + history, + msgIds), + .filterCallback = std::move(filterCallback), + .forwardOptions = { + .sendersCount = ItemsForwardSendersCount(items), + .captionsCount = ItemsForwardCaptionsCount(items), + .show = !hasOnlyForcedForwardedInfo, + }, + .premiumRequiredError = SharePremiumRequiredError(), + }), Ui::LayerOption::CloseOther); +} + +void FastShareMessage( + not_null controller, + not_null item) { + FastShareMessage(controller->uiShow(), item); } void FastShareLink( @@ -1805,111 +1759,3 @@ auto SharePremiumRequiredError() -> Fn)> { return WritePremiumRequiredError; } - -void ShareGameScoreByHash( - not_null controller, - const QString &hash) { - auto &session = controller->session(); - auto key128Size = 0x10; - - auto hashEncrypted = QByteArray::fromBase64(hash.toLatin1(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - if (hashEncrypted.size() <= key128Size || (hashEncrypted.size() != key128Size + 0x20)) { - controller->show( - Ui::MakeInformBox(tr::lng_confirm_phone_link_invalid()), - Ui::LayerOption::CloseOther); - return; - } - - // Decrypt data. - auto hashData = QByteArray(hashEncrypted.size() - key128Size, Qt::Uninitialized); - if (!session.local().decrypt(hashEncrypted.constData() + key128Size, hashData.data(), hashEncrypted.size() - key128Size, hashEncrypted.constData())) { - return; - } - - // Count SHA1() of data. - char dataSha1[20] = { 0 }; - hashSha1(hashData.constData(), hashData.size(), dataSha1); - - //// Mix out channel access hash from the first 64 bits of SHA1 of data. - //auto channelAccessHash = *reinterpret_cast(hashEncrypted.data()) ^ *reinterpret_cast(dataSha1); - - //// Check next 64 bits of SHA1() of data. - //auto skipSha1Part = sizeof(channelAccessHash); - //if (memcmp(dataSha1 + skipSha1Part, hashEncrypted.constData() + skipSha1Part, key128Size - skipSha1Part) != 0) { - // Ui::show(Box(tr::lng_share_wrong_user(tr::now))); - // return; - //} - - // Check 128 bits of SHA1() of data. - if (memcmp(dataSha1, hashEncrypted.constData(), key128Size) != 0) { - controller->show( - Ui::MakeInformBox(tr::lng_share_wrong_user()), - Ui::LayerOption::CloseOther); - return; - } - - auto hashDataInts = reinterpret_cast(hashData.data()); - if (hashDataInts[0] != session.userId().bare) { - controller->show( - Ui::MakeInformBox(tr::lng_share_wrong_user()), - Ui::LayerOption::CloseOther); - return; - } - - const auto peerId = PeerId(hashDataInts[1]); - const auto channelAccessHash = hashDataInts[3]; - if (!peerIsChannel(peerId) && channelAccessHash) { - // If there is no channel id, there should be no channel access_hash. - controller->show( - Ui::MakeInformBox(tr::lng_share_wrong_user()), - Ui::LayerOption::CloseOther); - return; - } - - const auto msgId = MsgId(int64(hashDataInts[2])); - if (const auto item = session.data().message(peerId, msgId)) { - FastShareMessage(controller, item); - } else { - const auto weak = base::make_weak(controller); - const auto resolveMessageAndShareScore = crl::guard(weak, [=]( - PeerData *peer) { - auto done = crl::guard(weak, [=] { - const auto item = weak->session().data().message( - peerId, - msgId); - if (item) { - FastShareMessage(weak.get(), item); - } else { - weak->show( - Ui::MakeInformBox(tr::lng_edit_deleted()), - Ui::LayerOption::CloseOther); - } - }); - auto &api = weak->session().api(); - api.requestMessageData(peer, msgId, std::move(done)); - }); - - const auto peer = peerIsChannel(peerId) - ? controller->session().data().peerLoaded(peerId) - : nullptr; - if (peer || !peerIsChannel(peerId)) { - resolveMessageAndShareScore(peer); - } else { - const auto owner = &controller->session().data(); - controller->session().api().request(MTPchannels_GetChannels( - MTP_vector( - 1, - MTP_inputChannel( - MTP_long(peerToChannel(peerId).bare), - MTP_long(channelAccessHash))) - )).done([=](const MTPmessages_Chats &result) { - result.match([&](const auto &data) { - owner->processChats(data.vchats()); - }); - if (const auto peer = owner->peerLoaded(peerId)) { - resolveMessageAndShareScore(peer); - } - }).send(); - } - } -} diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 32e824b15..d0bf28ce9 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -59,13 +59,11 @@ class SlideWrap; class PopupMenu; } // namespace Ui -QString AppendShareGameScoreUrl( - not_null session, - const QString &url, - const FullMsgId &fullId); -void ShareGameScoreByHash( - not_null controller, - const QString &hash); +class ShareBox; + +void FastShareMessage( + std::shared_ptr show, + not_null item); void FastShareMessage( not_null controller, not_null item); diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 0a98761ff..8f8647cc7 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -7,50 +7,55 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/sticker_set_box.h" -#include "data/data_document.h" -#include "data/data_session.h" -#include "data/data_file_origin.h" -#include "data/data_document_media.h" -#include "data/data_peer_values.h" -#include "data/stickers/data_stickers.h" -#include "data/stickers/data_custom_emoji.h" -#include "menu/menu_send.h" -#include "lang/lang_keys.h" -#include "ui/boxes/confirm_box.h" +#include "api/api_common.h" +#include "api/api_toggling_media.h" +#include "apiwrap.h" +#include "base/unixtime.h" #include "boxes/premium_preview_box.h" +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/stickers_list_widget.h" +#include "chat_helpers/stickers_lottie.h" #include "core/application.h" -#include "mtproto/sender.h" -#include "storage/storage_account.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_peer_values.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" #include "dialogs/ui/dialogs_layout.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/scroll_area.h" -#include "ui/widgets/gradient_round_button.h" -#include "ui/image/image.h" -#include "ui/image/image_location_factory.h" -#include "ui/text/text_utilities.h" -#include "ui/text/custom_emoji_instance.h" +#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. +#include "lang/lang_keys.h" +#include "lottie/lottie_animation.h" +#include "lottie/lottie_multi_player.h" +#include "main/main_session.h" +#include "mainwindow.h" +#include "media/clip/media_clip_reader.h" +#include "menu/menu_send.h" +#include "mtproto/sender.h" +#include "settings/settings_premium.h" +#include "storage/storage_account.h" +#include "ui/boxes/confirm_box.h" +#include "ui/cached_round_corners.h" +#include "ui/effects/animation_value_f.h" #include "ui/effects/path_shift_gradient.h" #include "ui/emoji_config.h" +#include "ui/image/image.h" +#include "ui/image/image_location_factory.h" #include "ui/painter.h" #include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/text/custom_emoji_instance.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/fields/input_field.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" -#include "ui/cached_round_corners.h" -#include "lottie/lottie_multi_player.h" -#include "lottie/lottie_animation.h" -#include "chat_helpers/compose/compose_show.h" -#include "chat_helpers/stickers_lottie.h" -#include "chat_helpers/stickers_list_widget.h" -#include "media/clip/media_clip_reader.h" -#include "window/window_controller.h" -#include "settings/settings_premium.h" -#include "base/unixtime.h" -#include "main/main_session.h" -#include "apiwrap.h" -#include "api/api_toggling_media.h" -#include "api/api_common.h" -#include "mainwidget.h" -#include "mainwindow.h" +#include "ui/widgets/scroll_area.h" #include "styles/style_layers.h" #include "styles/style_chat_helpers.h" #include "styles/style_info.h" @@ -75,10 +80,12 @@ constexpr auto kEmojiPerRow = 8; constexpr auto kMinRepaintDelay = crl::time(33); constexpr auto kMinAfterScrollDelay = crl::time(33); constexpr auto kGrayLockOpacity = 0.3; +constexpr auto kStickerMoveDuration = crl::time(200); using Data::StickersSet; using Data::StickersPack; using SetFlag = Data::StickersSetFlag; +using TLStickerSet = MTPmessages_StickerSet; [[nodiscard]] std::optional ComputeImageColor( const style::icon &lockIcon, @@ -266,6 +273,20 @@ public: [[nodiscard]] rpl::producer setArchived() const; [[nodiscard]] rpl::producer<> updateControls() const; + void setReorderState(bool enabled) { + _dragging.enabled = enabled; + if (enabled) { + _shakeAnimation.init([=] { update(); }); + _shakeAnimation.start(); + } else { + _shakeAnimation.stop(); + update(); + } + } + [[nodiscard]] bool reorderState() const { + return _dragging.enabled; + } + [[nodiscard]] rpl::producer errors() const; void archiveStickers(); @@ -278,6 +299,12 @@ public: : Data::StickersType::Stickers; } + [[nodiscard]] bool amSetCreator() const { + return _amSetCreator; + } + + void applySet(const TLStickerSet &set); + ~Inner(); protected: @@ -313,6 +340,11 @@ private: QPoint position, bool paused, crl::time now) const; + void shakeTransform( + QPainter &p, + int index, + QPoint position, + crl::time now) const; void setupLottie(int index); void setupWebm(int index); void clipCallback( @@ -329,14 +361,19 @@ private: void startOverAnimation(int index, float64 from, float64 to); int stickerFromGlobalPos(const QPoint &p) const; - void gotSet(const MTPmessages_StickerSet &set); void installDone(const MTPmessages_StickerSetInstallResult &result); + void requestReorder(not_null document, int index); + void fillDeleteStickerBox(not_null box, int index); + void chosen( int index, not_null sticker, Api::SendOptions options); + [[nodiscard]] QPoint posFromIndex(int index) const; + [[nodiscard]] bool isDraggedAnimating() const; + not_null getLottiePlayer(); void showPreview(); @@ -373,6 +410,24 @@ private: TimeId _setInstallDate = TimeId(0); StickerType _setThumbnailType = StickerType::Webp; ImageWithLocation _setThumbnail; + bool _amSetCreator = false; + + struct { + bool enabled = false; + int index = -1; + int lastSelected = -1; + QPoint point; + } _dragging; + Ui::Animations::Basic _shakeAnimation; + std::deque> _reorderRequests; + std::optional _apiReorder; + + struct ShiftAnimation final { + Ui::Animations::Simple animation; + Ui::Animations::Simple yAnimation; + int shift = 0; + }; + base::flat_map _shiftAnimations; const std::unique_ptr _pathGradient; mutable StickerPremiumMark _premiumMark; @@ -545,9 +600,112 @@ void StickerSetBox::updateTitleAndButtons() { updateButtons(); } +void ChangeSetNameBox( + not_null box, + not_null data, + const StickerSetIdentifier &input, + Fn done) { + struct State final { + rpl::variable requestId = 0; + Ui::RpWidget* saveButton = nullptr; + }; + box->setTitle(tr::lng_stickers_box_edit_name_title()); + box->addRow( + object_ptr( + box, + tr::lng_stickers_box_edit_name_about(), + st::boxLabel)); + const auto state = box->lifetime().make_state(); + + const auto wasName = [&] { + const auto &sets = data->stickers().sets(); + const auto it = sets.find(input.id); + return (it == sets.end()) ? QString() : it->second->title; + }(); + const auto wrap = box->addRow(object_ptr( + box, + st::editStickerSetNameField.heightMin)); + auto owned = object_ptr( + wrap, + st::editStickerSetNameField, + tr::lng_stickers_context_edit_name(), + wasName); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->selectAll(); + constexpr auto kMaxSetNameLength = 50; + field->setMaxLength(kMaxSetNameLength); + Ui::AddLengthLimitLabel(field, kMaxSetNameLength, kMaxSetNameLength + 1); + box->setFocusCallback([=] { field->setFocusFast(); }); + const auto close = crl::guard(box, [=] { box->closeBox(); }); + const auto save = [=, show = box->uiShow()] { + if (state->requestId.current()) { + return; + } + const auto text = field->getLastText().trimmed(); + if ((Ui::ComputeRealUnicodeCharactersCount(text) > kMaxSetNameLength) + || text.isEmpty()) { + field->showError(); + return; + } + const auto buttonWidth = state->saveButton + ? state->saveButton->width() + : 0; + state->requestId = data->session().api().request( + MTPstickers_RenameStickerSet( + Data::InputStickerSet(input), + MTP_string(text)) + ).done([=](const TLStickerSet &result) { + result.match([&](const MTPDmessages_stickerSet &d) { + data->stickers().feedSetFull(d); + data->stickers().notifyUpdated(Data::StickersType::Stickers); + }, [](const auto &) { + }); + done(result); + close(); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + close(); + }).send(); + if (state->saveButton) { + state->saveButton->resizeToWidth(buttonWidth); + } + }; + + state->saveButton = box->addButton( + rpl::conditional( + state->requestId.value() | rpl::map(rpl::mappers::_1 > 0), + rpl::single(QString()), + tr::lng_box_done()), + save); + if (const auto saveButton = state->saveButton) { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + saveButton, + saveButton->height() / 2, + &st::editStickerSetNameLoading); + AddChildToWidgetCenter(saveButton, loadingAnimation); + loadingAnimation->showOn( + state->requestId.value() | rpl::map(rpl::mappers::_1 > 0)); + } + box->addButton(tr::lng_cancel(), [=] { + data->session().api().request(state->requestId.current()).cancel(); + close(); + }); +} + void StickerSetBox::updateButtons() { clearButtons(); - if (_inner->loaded()) { + if (_inner->reorderState()) { + addButton(tr::lng_box_done(), [=] { + _inner->setReorderState(false); + updateButtons(); + }); + } else if (_inner->loaded()) { const auto type = _inner->setType(); const auto share = [=] { copyStickersLink(); @@ -555,6 +713,34 @@ void StickerSetBox::updateButtons() { ? tr::lng_stickers_copied_emoji(tr::now) : tr::lng_stickers_copied(tr::now)); }; + const auto fillSetCreatorMenu = [&] { + using Filler = Fn)>; + if (!_inner->amSetCreator()) { + return Filler(nullptr); + } + const auto data = &_session->data(); + return Filler([=, show = _show, set = _set]( + not_null menu) { + const auto done = [inner = _inner](const TLStickerSet &set) { + if (const auto raw = inner.data()) { + raw->applySet(set); + } + }; + menu->addAction( + tr::lng_stickers_context_edit_name(tr::now), + [=] { + show->showBox(Box(ChangeSetNameBox, data, set, done)); + }, + &st::menuIconEdit); + menu->addAction( + tr::lng_stickers_context_reorder(tr::now), + [=] { + _inner->setReorderState(true); + updateButtons(); + }, + &st::menuIconManage); + }); + }(); const auto addPackOwner = [=](const std::shared_ptr> &menu) { if (type == Data::StickersType::Stickers || type == Data::StickersType::Emoji) { @@ -629,6 +815,9 @@ void StickerSetBox::updateButtons() { *menu = base::make_unique_q( top, st::popupMenuWithIcons); + if (fillSetCreatorMenu) { + fillSetCreatorMenu(*menu); + } (*menu)->addAction( ((type == Data::StickersType::Emoji) ? tr::lng_stickers_share_emoji @@ -680,6 +869,9 @@ void StickerSetBox::updateButtons() { remove, &st::menuIconRemove); } else { + if (fillSetCreatorMenu) { + fillSetCreatorMenu(*menu); + } (*menu)->addAction( (type == Data::StickersType::Masks ? tr::lng_masks_archive_pack(tr::now) @@ -732,8 +924,8 @@ StickerSetBox::Inner::Inner( _api.request(MTPmessages_GetStickerSet( Data::InputStickerSet(_input), MTP_int(0) // hash - )).done([=](const MTPmessages_StickerSet &result) { - gotSet(result); + )).done([=](const TLStickerSet &result) { + applySet(result); }).fail([=] { _loaded = true; _errors.fire(Error::NotFound); @@ -749,7 +941,7 @@ StickerSetBox::Inner::Inner( setMouseTracking(true); } -void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { +void StickerSetBox::Inner::applySet(const TLStickerSet &set) { _pack.clear(); _emoji.clear(); _elements.clear(); @@ -793,7 +985,9 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { } }); } - data.vset().match([&](const MTPDstickerSet &set) { + + { + const auto &set = data.vset().data(); _setTitle = _session->data().stickers().getSetTitle( set); _setShortName = qs(set.vshort_name()); @@ -804,6 +998,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { _setFlags = Data::ParseStickersSetFlags(set); _setInstallDate = set.vinstalled_date().value_or(0); _setThumbnailDocumentId = set.vthumb_document_id().value_or_empty(); + _amSetCreator = set.is_creator(); _setThumbnail = [&] { if (const auto thumbs = set.vthumbs()) { for (const auto &thumb : thumbs->v) { @@ -836,7 +1031,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { set->emoji = _emoji; set->setThumbnail(_setThumbnail, _setThumbnailType); } - }); + }; }, [&](const MTPDmessages_stickerSetNotModified &data) { LOG(("API Error: Unexpected messages.stickerSetNotModified.")); }); @@ -977,11 +1172,100 @@ void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) { if (index < 0 || index >= _pack.size()) { return; } + if (_dragging.enabled) { + _previewTimer.cancel(); + if (isDraggedAnimating()) { + return; + } + _dragging.index = index; + _dragging.point = mapFromGlobal(QCursor::pos()) - posFromIndex(index); + return; + } _previewTimer.callOnce(QApplication::startDragTime()); } void StickerSetBox::Inner::mouseMoveEvent(QMouseEvent *e) { updateSelected(); + const auto draggedAnimating = isDraggedAnimating(); + if (_selected >= 0 && !draggedAnimating) { + _dragging.lastSelected = _selected; + } + if (_dragging.index >= 0 + && _dragging.index < _pack.size() + && _dragging.lastSelected >= 0 + && !draggedAnimating) { + for (auto i = 0; i < _pack.size(); i++) { + if (i == _dragging.index) { + continue; + } + auto &entry = _shiftAnimations[i]; + const auto wasShift = entry.shift; + if ((i >= _dragging.index) && (i <= _dragging.lastSelected)) { + if (entry.shift == 0) { + entry.shift = -1; + } else if (entry.shift == 1) { + entry.shift = 0; + } + } else if ((i < _dragging.index) + && (i >= _dragging.lastSelected)) { + if (entry.shift == 0) { + entry.shift = 1; + } else if (entry.shift == -1) { + entry.shift = 0; + } + } + if ((i < std::min(_dragging.index, _dragging.lastSelected)) + || (i > std::max(_dragging.index, _dragging.lastSelected))) { + entry.shift = 0; + } + if (wasShift != entry.shift) { + const auto fromPoint = posFromIndex(i + wasShift); + const auto toPoint = posFromIndex(i + entry.shift); + const auto toX = float64(toPoint.x()); + const auto toY = float64(toPoint.y()); + const auto ratio = [&] { + const auto fromX = entry.animation.value(toX); + const auto ratioX = std::min(toX, fromX) + / std::max(toX, fromX); + const auto fromY = entry.yAnimation.value(toY); + const auto ratioY = std::min(toY, fromY) + / std::max(toY, fromY); + return (ratioX == 1.) + ? ratioY + : (ratioY == 1.) + ? ratioX + : std::max(ratioX, ratioY); + }(); + if (!entry.animation.animating()) { + entry.animation.stop(); + entry.animation.start( + [=] { update(); }, + fromPoint.x(), + toX, + kStickerMoveDuration); + } else { + entry.animation.change( + toX, + kStickerMoveDuration * (1. - ratio), + anim::linear); + } + if (!entry.yAnimation.animating()) { + entry.yAnimation.stop(); + entry.yAnimation.start( + [=] { update(); }, + fromPoint.y(), + toY, + kStickerMoveDuration); + } else { + entry.yAnimation.change( + toY, + kStickerMoveDuration * (1. - ratio), + anim::linear); + } + } + } + update(); + } if (_previewShown >= 0) { showPreviewAt(e->globalPos()); } @@ -1003,7 +1287,86 @@ void StickerSetBox::Inner::leaveEventHook(QEvent *e) { setSelected(-1); } +void StickerSetBox::Inner::requestReorder( + not_null document, + int index) { + if (!_apiReorder) { + _apiReorder.emplace(&_session->mtp()); + } + _reorderRequests.emplace_back([document, index, this] { + _apiReorder->request( + MTPstickers_ChangeStickerPosition( + document->mtpInput(), + MTP_int(index)) + ).done([this, document](const TLStickerSet &result) { + result.match([&](const MTPDmessages_stickerSet &d) { + document->owner().stickers().feedSetFull(d); + document->owner().stickers().notifyUpdated( + Data::StickersType::Stickers); + }, [](const auto &) { + }); + if (!_reorderRequests.empty()) { + _reorderRequests.pop_front(); + } + if (_reorderRequests.empty()) { + // applySet(result); // Causes stickers blink. + } else { + _reorderRequests.front()(); + } + }).fail([show = _show](const MTP::Error &error) { + show->showToast(error.type()); + }).send(); + }); + if (_reorderRequests.size() == 1) { + _reorderRequests.front()(); + } +} + void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) { + if (_dragging.index >= 0 && !isDraggedAnimating()) { + const auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point; + const auto toPos = posFromIndex(_dragging.lastSelected); + const auto document = _pack[_dragging.index]; + const auto wasPosition = _dragging.index; + const auto nowPosition = _dragging.lastSelected; + const auto finish = [=, this] { + requestReorder(document, nowPosition); + base::reorder(_pack, wasPosition, nowPosition); + base::reorder(_elements, wasPosition, nowPosition); + _dragging = {}; + _dragging.enabled = true; + _shiftAnimations.clear(); + }; + auto &entry = _shiftAnimations[_dragging.index]; + entry.animation.stop(); + entry.yAnimation.stop(); + entry.animation.start( + [finish, toPos, this](float64 value) { + const auto index = _dragging.index; + if (value >= toPos.x() + && index >= 0 + && !_shiftAnimations[index].yAnimation.animating()) { + finish(); + } + update(); + }, + fromPos.x(), + toPos.x(), + kStickerMoveDuration); + entry.yAnimation.start( + [finish, toPos, this](float64 value) { + const auto index = _dragging.index; + if (value >= toPos.y() + && index >= 0 + && !_shiftAnimations[index].animation.animating()) { + finish(); + } + update(); + }, + fromPos.y(), + toPos.y(), + kStickerMoveDuration); + } if (_previewShown >= 0) { _previewShown = -1; return; @@ -1106,6 +1469,20 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { (isFaved ? &st::menuIconUnfave : &st::menuIconFave)); + if (amSetCreator()) { + const auto addAction = Ui::Menu::CreateAddActionCallback( + _menu.get()); + addAction({ + .text = tr::lng_stickers_context_delete(tr::now), + .handler = [index, this, show = _show] { + show->showBox(Box([=](not_null box) { + fillDeleteStickerBox(box, index); + })); + }, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + } } if (_menu->empty()) { _menu = nullptr; @@ -1114,6 +1491,129 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { } } +void StickerSetBox::Inner::fillDeleteStickerBox( + not_null box, + int index) { + Expects(index >= 0 || index < _pack.size()); + const auto document = _pack[index]; + const auto weak = Ui::MakeWeak(this); + const auto show = _show; + + const auto container = box->verticalLayout(); + Ui::AddSkip(container); + Ui::AddSkip(container); + const auto line = container->add(object_ptr(container)); + line->resize(line->width(), _singleSize.height()); + + const auto sticker = Ui::CreateChild(line); + auto &lifetime = sticker->lifetime(); + struct State final { + rpl::variable requestId = 0; + Ui::RpWidget* saveButton = nullptr; + }; + const auto state = lifetime.make_state(); + sticker->resize(_singleSize); + { + const auto animation = lifetime.make_state(); + animation->init([=] { sticker->update(); }); + animation->start(); + } + sticker->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(sticker); + if (const auto strong = weak.data()) { + const auto paused = On(PowerSaving::kStickersPanel) + || show->paused(ChatHelpers::PauseReason::Layer); + paintSticker(p, index, QPoint(), paused, crl::now()); + if (_lottiePlayer && !paused) { + _lottiePlayer->markFrameShown(); + } + } + }, sticker->lifetime()); + const auto label = Ui::CreateChild( + line, + tr::lng_stickers_context_delete(), + box->getDelegate()->style().title); + line->widthValue( + ) | rpl::start_with_next([=](int width) { + sticker->moveToLeft(st::boxRowPadding.left(), 0); + const auto skip = st::defaultBoxCheckbox.textPosition.x(); + label->resizeToWidth(width + - rect::right(sticker) + - skip + - st::boxRowPadding.right()); + label->moveToLeft( + rect::right(sticker) + skip, + ((sticker->height() - label->height()) / 2)); + }, label->lifetime()); + + sticker->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + box->addRow( + object_ptr( + container, + tr::lng_stickers_context_delete_sure(), + st::boxLabel)); + const auto save = [=] { + if (state->requestId.current()) { + return; + } + const auto weakBox = Ui::MakeWeak(box); + const auto buttonWidth = state->saveButton + ? state->saveButton->width() + : 0; + state->requestId = document->owner().session().api().request( + MTPstickers_RemoveStickerFromSet(document->mtpInput() + )).done([=](const TLStickerSet &result) { + result.match([&](const MTPDmessages_stickerSet &d) { + document->owner().stickers().feedSetFull(d); + document->owner().stickers().notifyUpdated( + Data::StickersType::Stickers); + }, [](const auto &) { + }); + if (const auto strong = weak.data()) { + applySet(result); + } + if (const auto strongBox = weakBox.data()) { + strongBox->closeBox(); + } + }).fail([=](const MTP::Error &error) { + if (const auto strongBox = weakBox.data()) { + strongBox->uiShow()->showToast(error.type()); + } + }).send(); + if (state->saveButton) { + state->saveButton->resizeToWidth(buttonWidth); + } + }; + state->saveButton = box->addButton( + rpl::conditional( + state->requestId.value() | rpl::map(rpl::mappers::_1 > 0), + rpl::single(QString()), + tr::lng_selected_delete()), + save, + st::attentionBoxButton); + if (const auto saveButton = state->saveButton) { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + saveButton, + saveButton->height() / 2, + &st::editStickerSetNameLoading); + AddChildToWidgetCenter(saveButton, loadingAnimation); + loadingAnimation->showOn( + state->requestId.value() | rpl::map(rpl::mappers::_1 > 0)); + } + box->addButton(tr::lng_close(), [=] { + document->owner().session().api().request( + state->requestId.current()).cancel(); + box->closeBox(); + }); +} + void StickerSetBox::Inner::updateSelected() { auto selected = stickerFromGlobalPos(QCursor::pos()); setSelected(setType() == Data::StickersType::Masks ? -1 : selected); @@ -1124,7 +1624,11 @@ void StickerSetBox::Inner::setSelected(int selected) { startOverAnimation(_selected, 1., 0.); _selected = selected; startOverAnimation(_selected, 0., 1.); - setCursor(_selected >= 0 ? style::cur_pointer : style::cur_default); + setCursor((_selected < 0) + ? style::cur_default + : _dragging.enabled + ? style::cur_sizeall + : style::cur_pointer); } } @@ -1146,6 +1650,24 @@ void StickerSetBox::Inner::showPreview() { showPreviewAt(QCursor::pos()); } +QPoint StickerSetBox::Inner::posFromIndex(int index) const { + return { + _padding.left() + (index % _perRow) * _singleSize.width(), + _padding.top() + (index / _perRow) * _singleSize.height(), + }; +} + +bool StickerSetBox::Inner::isDraggedAnimating() const { + if (_dragging.index < 0) { + return false; + } + const auto it = _shiftAnimations.find(_dragging.index); + return (it == _shiftAnimations.end()) + ? false + : (it->second.animation.animating() + || it->second.yAnimation.animating()); +} + not_null StickerSetBox::Inner::getLottiePlayer() { if (!_lottiePlayer) { _lottiePlayer = std::make_unique( @@ -1185,12 +1707,36 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) { _pathGradient->startFrame(0, width(), width() / 2); + const auto indexUnderCursor = (_dragging.index >= 0 + && _dragging.index < _elements.size()) + ? stickerFromGlobalPos(QCursor::pos()) + : -2; + const auto lastIndex = indexUnderCursor >= 0 + ? indexUnderCursor + : _dragging.lastSelected; + const auto now = crl::now(); const auto paused = On(PowerSaving::kStickersPanel) || _show->paused(ChatHelpers::PauseReason::Layer); for (int32 i = from; i < to; ++i) { for (int32 j = 0; j < _perRow; ++j) { int32 index = i * _perRow + j; + + if (lastIndex >= 0) { + if (_dragging.index == index) { + continue; + } + const auto it = _shiftAnimations.find(index); + if (it != _shiftAnimations.end()) { + const auto &entry = it->second; + const auto toPos = posFromIndex(index + entry.shift); + const auto pos = QPoint( + entry.animation.value(toPos.x()), + entry.yAnimation.value(toPos.y())); + paintSticker(p, index, pos, paused, now); + continue; + } + } if (index >= _elements.size()) { break; } @@ -1200,6 +1746,14 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) { paintSticker(p, index, pos, paused, now); } } + if (_dragging.index >= 0 && _dragging.index < _elements.size()) { + const auto pos = isDraggedAnimating() + ? QPoint( + _shiftAnimations[_dragging.index].animation.value(0), + _shiftAnimations[_dragging.index].yAnimation.value(0)) + : (mapFromGlobal(QCursor::pos()) - _dragging.point); + paintSticker(p, _dragging.index, pos, paused, now); + } if (_lottiePlayer && !paused) { _lottiePlayer->markFrameShown(); @@ -1355,18 +1909,99 @@ void StickerSetBox::Inner::customEmojiRepaint() { update(); } +void StickerSetBox::Inner::shakeTransform( + QPainter &p, + int index, + QPoint position, + crl::time now) const { + constexpr auto kShakeADuration = crl::time(400); + constexpr auto kShakeXDuration = crl::time(kShakeADuration * 1.2); + constexpr auto kShakeYDuration = kShakeADuration; + const auto diff = ((index % 2) ? 0 : kShakeYDuration / 2) + + (now - _shakeAnimation.started()); + const auto pX = (diff % kShakeXDuration) + / float64(kShakeXDuration); + const auto pY = (diff % kShakeYDuration) + / float64(kShakeYDuration); + const auto pA = (diff % kShakeADuration) + / float64(kShakeADuration); + + constexpr auto kMaxA = 2.; + constexpr auto kMaxTranslation = .5; + constexpr auto kAStep = 1. / 5; + constexpr auto kXStep = 1. / 5; + constexpr auto kYStep = 1. / 4; + + // 0, -kMaxA, 0, kMaxA, 0. + const auto angle = (pA < kAStep) + ? anim::interpolateF(0., -kMaxA, pA / kAStep) + : (pA < kAStep * 2.) + ? anim::interpolateF(-kMaxA, 0, (pA - kAStep) / kAStep) + : (pA < kAStep * 3.) + ? anim::interpolateF(0, kMaxA, (pA - kAStep * 2.) / kAStep) + : (pA < kAStep * 4.) + ? anim::interpolateF(kMaxA, 0, (pA - kAStep * 3.) / kAStep) + : anim::interpolateF(0, 0., (pA - kAStep * 4.) / kAStep); + + // 0, kMaxTranslation, 0, -kMaxTranslation, 0. + const auto x = (pX < kXStep) + ? anim::interpolateF(0., kMaxTranslation, pX / kXStep) + : (pX < kXStep * 2.) + ? anim::interpolateF(kMaxTranslation, 0, (pX - kXStep) / kXStep) + : (pX < kXStep * 3.) + ? anim::interpolateF(0, -kMaxTranslation, (pX - kXStep * 2.) / kXStep) + : (pX < kXStep * 4.) + ? anim::interpolateF(-kMaxTranslation, 0, (pX - kXStep * 3.) / kXStep) + : anim::interpolateF(0, 0., (pX - kXStep * 4.) / kXStep); + + // 0, kMaxTranslation, -kMaxTranslation, 0. + const auto y = (pY < kYStep) + ? anim::interpolateF(0., kMaxTranslation, pY / kYStep) + : (pY < kYStep * 2.) + ? anim::interpolateF(kMaxTranslation, 0, (pY - kYStep) / kYStep) + : (pY < kYStep * 3.) + ? anim::interpolateF(0, -kMaxTranslation, (pY - kYStep * 2.) / kYStep) + : anim::interpolateF(-kMaxTranslation, 0, (pY - kYStep * 3) / kYStep); + + const auto center = position + QPoint( + _singleSize.width() / 2, + _singleSize.height() / 2); + + p.translate(center); + p.rotate(angle); + p.translate(-center); + p.translate(x, y); +} + void StickerSetBox::Inner::paintSticker( Painter &p, int index, QPoint position, bool paused, crl::time now) const { - if (const auto over = _elements[index].overAnimation.value((index == _selected) ? 1. : 0.)) { - p.setOpacity(over); - auto tl = position; - if (rtl()) tl.setX(width() - tl.x() - _singleSize.width()); - Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners); - p.setOpacity(1); + if (_dragging.index != index) { + const auto over = _elements[index].overAnimation.value( + (index == _selected) ? 1. : 0.); + if (over) { + p.setOpacity(over); + Ui::FillRoundRect( + p, + QRect( + rtl() + ? QPoint( + width() - position.x() - _singleSize.width(), + position.y()) + : position, + _singleSize), + st::emojiPanHover, + Ui::StickerHoverCorners); + p.setOpacity(1); + } + } + + const auto hasShake = _shakeAnimation.animating(); + if (hasShake) { + shakeTransform(p, index, position, now); } const auto &element = _elements[index]; @@ -1452,6 +2087,9 @@ void StickerSetBox::Inner::paintSticker( _singleSize, width()); } + if (hasShake) { + p.resetTransform(); + } } bool StickerSetBox::Inner::loaded() const { diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 83c4fe88e..c97dee88b 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -39,7 +39,6 @@ namespace tgcalls { class InstanceImpl; class InstanceV2Impl; class InstanceV2ReferenceImpl; -class InstanceV2_4_0_0Impl; class InstanceImplLegacy; void SetLegacyGlobalServerConfig(const std::string &serverConfig); } // namespace tgcalls @@ -56,7 +55,6 @@ const auto kDefaultVersion = "2.4.4"_q; const auto Register = tgcalls::Register(); const auto RegisterV2 = tgcalls::Register(); const auto RegV2Ref = tgcalls::Register(); -const auto RegisterV240 = tgcalls::Register(); const auto RegisterLegacy = tgcalls::Register(); [[nodiscard]] base::flat_set CollectEndpointIds( diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 4b92ee075..6a816af96 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -215,7 +215,7 @@ void Panel::initWindow() { } const auto shown = _layerBg->topShownLayer(); return (!shown || !shown->geometry().contains(widgetPoint)) - ? (Flag::Move | Flag::FullScreen) + ? (Flag::Move | Flag::Menu | Flag::FullScreen) : Flag::None; }); @@ -276,8 +276,8 @@ void Panel::initControls() { _layerBg->showBox(std::move(box)); } } else if (const auto source = env->uniqueDesktopCaptureSource()) { - if (_call->isSharingScreen()) { - _call->toggleScreenSharing(std::nullopt); + if (!chooseSourceActiveDeviceId().isEmpty()) { + chooseSourceStop(); } else { chooseSourceAccepted(*source, false); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index ce806928c..2853340a5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1195,24 +1195,7 @@ base::unique_qptr Members::Controller::createRowContextMenu( const auto addVolumeItem = (!muted || isMe(participantPeer)); const auto admin = IsGroupCallAdmin(_peer, participantPeer); const auto session = &_peer->session(); - const auto getCurrentWindow = [=]() -> Window::SessionController* { - if (const auto window = Core::App().windowFor(participantPeer)) { - if (const auto controller = window->sessionController()) { - if (&controller->session() == session) { - return controller; - } - } - } - return nullptr; - }; - const auto getWindow = [=] { - if (const auto current = getCurrentWindow()) { - return current; - } else if (&Core::App().domain().active() != &session->account()) { - Core::App().domain().activate(&session->account()); - } - return getCurrentWindow(); - }; + const auto account = &session->account(); auto result = base::make_unique_q( parent, @@ -1223,7 +1206,7 @@ base::unique_qptr Members::Controller::createRowContextMenu( : st::groupCallPopupMenu)); const auto weakMenu = Ui::MakeWeak(result.get()); const auto withActiveWindow = [=](auto callback) { - if (const auto window = getWindow()) { + if (const auto window = Core::App().activePrimaryWindow()) { if (const auto menu = weakMenu.data()) { menu->discardParentReActivate(); @@ -1232,8 +1215,13 @@ base::unique_qptr Members::Controller::createRowContextMenu( // PopupMenu::hide activates back the group call panel :( delete weakMenu; } - callback(window); - window->widget()->activate(); + window->invokeForSessionController( + account, + participantPeer, + [&](not_null newController) { + callback(newController); + newController->widget()->activate(); + }); } }; const auto showProfile = [=] { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 84c336131..927dba8f8 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -410,7 +410,7 @@ void Panel::initWindow() { } const auto shown = _layerBg->topShownLayer(); return (!shown || !shown->geometry().contains(widgetPoint)) - ? (Flag::Move | Flag::Maximize) + ? (Flag::Move | Flag::Menu | Flag::Maximize) : Flag::None; }); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 84fd1fdc8..3007f6a46 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -284,6 +284,10 @@ stickersTrendingSubheaderFont: normalFont; stickersTrendingSubheaderFg: windowSubTextFg; stickersTrendingSubheaderTop: 31px; +stickersHeaderBadgeFont: font(10px); +stickersHeaderBadgeFontTop: 12px; +stickersHeaderBadgeFontSkip: 12px; + emojiPanButtonRight: 7px; emojiPanButtonTop: 8px; emojiPanButton: RoundButton(defaultActiveButton) { @@ -1409,6 +1413,22 @@ editTagLimit: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } +editStickerSetNameField: InputField(defaultInputField) { + textMargins: margins(0px, 8px, 26px, 4px); + heightMin: 36px; + heightMax: 36px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; +} +editStickerSetNameLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { + color: lightButtonFg; + thickness: 2px; +} + paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }}; paidStarIconTop: 7px; paidAmountAbout: FlatLabel(defaultFlatLabel) { @@ -1423,3 +1443,61 @@ paidTagLabel: FlatLabel(defaultFlatLabel) { style: semiboldTextStyle; } paidTagPadding: margins(16px, 6px, 16px, 6px); + +pickLocationWindow: size(364px, 680px); +pickLocationMapHeight: 220px; +pickLocationCollapsedHeight: 92px; +pickLocationRowHeight: 52px; +pickLocationButton: FlatButton { + height: pickLocationRowHeight; + bgColor: contactsBg; + overBgColor: contactsBgOver; + ripple: defaultRippleAnimation; +} +pickLocationButtonText: FlatLabel(defaultFlatLabel) { + minWidth: 128px; + maxHeight: 20px; + style: semiboldTextStyle; + textFg: windowBoldFg; +} +pickLocationButtonStatus: FlatLabel(defaultFlatLabel) { + minWidth: 128px; + maxHeight: 20px; + textFg: windowSubTextFg; +} +pickLocationButtonSkip: 6px; +pickLocationSendIcon: icon{{ "chat/filled_location", windowFgActive }}; +pickLocationVenueItem: PeerListItem(defaultPeerListItem) { + height: pickLocationRowHeight; + photoSize: 42px; + photoPosition: point(18px, 5px); + namePosition: point(70px, 7px); + statusPosition: point(70px, 27px); + button: OutlineButton(defaultPeerListButton) { + textBg: contactsBg; + textBgOver: contactsBgOver; + font: normalFont; + padding: margins(11px, 5px, 11px, 5px); + ripple: defaultRippleAnimation; + } + statusFg: contactsStatusFg; + statusFgOver: contactsStatusFgOver; + statusFgActive: contactsStatusFgOnline; +} +pickLocationVenueList: PeerList(defaultPeerList) { + item: pickLocationVenueItem; + padding: margins(0px, 0px, 0px, 0px); +} +pickLocationIconSkip: 6px; +pickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { + size: size(56px, 56px); + color: windowSubTextFg; + thickness: 4px; +} +pickLocationPromoHeight: 32px; +pickLocationChooseOnMap: RoundButton(defaultActiveButton) { + height: 44px; + textTop: 11px; + width: -96px; + font: font(15px semibold); +} diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 4bab1bc3f..ca1857614 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "data/stickers/data_stickers.h" #include "menu/menu_send.h" // SendMenu::FillSendMenu +#include "mtproto/mtproto_config.h" #include "core/click_handler_types.h" #include "ui/controls/tabbed_search.h" #include "ui/widgets/buttons.h" @@ -55,7 +56,6 @@ namespace ChatHelpers { namespace { constexpr auto kSearchRequestDelay = 400; -constexpr auto kSearchBotUsername = "gif"_cs; constexpr auto kMinRepaintDelay = crl::time(33); constexpr auto kMinAfterScrollDelay = crl::time(33); @@ -893,13 +893,11 @@ void GifsListWidget::searchForGifs(const QString &query) { } if (!_searchBot && !_searchBotRequestId) { - auto username = kSearchBotUsername.utf16(); + const auto username = session().serverConfig().gifSearchUsername; _searchBotRequestId = _api.request(MTPcontacts_ResolveUsername( MTP_string(username) )).done([=](const MTPcontacts_ResolvedPeer &result) { - Expects(result.type() == mtpc_contacts_resolvedPeer); - - auto &data = result.c_contacts_resolvedPeer(); + auto &data = result.data(); session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); const auto peer = session().data().peerLoaded( diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 0326743b0..c17af896d 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -392,6 +392,9 @@ void InitMessageFieldHandlers( Fn customEmojiPaused, Fn)> allowPremiumEmoji, const style::InputField *fieldStyle) { + const auto paused = [customEmojiPaused] { + return customEmojiPaused && customEmojiPaused(); + }; field->setTagMimeProcessor( FieldTagMimeProcessor(session, allowPremiumEmoji)); field->setCustomTextContext([=](Fn repaint) { @@ -399,10 +402,10 @@ void InitMessageFieldHandlers( .session = session, .customEmojiRepaint = std::move(repaint), }); - }, [customEmojiPaused] { - return On(PowerSaving::kEmojiChat) || customEmojiPaused(); - }, [customEmojiPaused] { - return On(PowerSaving::kChatSpoiler) || customEmojiPaused(); + }, [paused] { + return On(PowerSaving::kEmojiChat) || paused(); + }, [paused] { + return On(PowerSaving::kChatSpoiler) || paused(); }); field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplacesEnabled( @@ -525,7 +528,7 @@ void InitMessageFieldGeometry(not_null field) { st::historySendSize.height() - 2 * st::historySendPadding); field->setMaxHeight(st::historyComposeFieldMaxHeight); - field->document()->setDocumentMargin(4.); + field->setDocumentMargin(4.); field->setAdditionalMargin(style::ConvertScale(4) - 4); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp index f54b1f1d3..f510540c2 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_document.h" +#include "data/data_file_origin.h" #include "data/data_session.h" #include "main/main_session.h" @@ -21,6 +22,16 @@ GiftBoxPack::GiftBoxPack(not_null session) GiftBoxPack::~GiftBoxPack() = default; +int GiftBoxPack::monthsForStars(int stars) const { + if (stars <= 1000) { + return 3; + } else if (stars < 2500) { + return 6; + } else { + return 12; + } +} + DocumentData *GiftBoxPack::lookup(int months) const { const auto it = ranges::lower_bound(_localMonths, months); const auto fallback = _documents.empty() ? nullptr : _documents[0]; @@ -38,6 +49,10 @@ DocumentData *GiftBoxPack::lookup(int months) const { return (index >= _documents.size()) ? fallback : _documents[index]; } +Data::FileOrigin GiftBoxPack::origin() const { + return Data::FileOriginStickerSet(_setId, _accessHash); +} + void GiftBoxPack::load() { if (_requestId || !_documents.empty()) { return; @@ -59,6 +74,7 @@ void GiftBoxPack::load() { void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) { _setId = data.vset().data().vid().v; + _accessHash = data.vset().data().vaccess_hash().v; auto documents = base::flat_map>(); for (const auto &sticker : data.vdocuments().v) { const auto document = _session->data().processDocument(sticker); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h index e722c1979..1e6c0e934 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class DocumentData; +namespace Data { +struct FileOrigin; +} // namespace Data + namespace Main { class Session; } // namespace Main @@ -21,7 +25,9 @@ public: ~GiftBoxPack(); void load(); + [[nodiscard]] int monthsForStars(int stars) const; [[nodiscard]] DocumentData *lookup(int months) const; + [[nodiscard]] Data::FileOrigin origin() const; private: using SetId = uint64; @@ -32,6 +38,7 @@ private: std::vector _documents; SetId _setId = 0; + uint64 _accessHash = 0; mtpRequestId _requestId = 0; }; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 1aca8af02..9a87760ca 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/stickers_list_widget.h" +#include "base/timer_rpl.h" #include "core/application.h" #include "data/data_document.h" #include "data/data_document_media.h" @@ -939,6 +940,9 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { if (sets.empty() && _section == Section::Search) { paintEmptySearchResults(p); } + const auto badgeText = tr::lng_stickers_creator_badge(tr::now); + const auto &badgeFont = st::stickersHeaderBadgeFont; + const auto badgeWidth = badgeFont->width(badgeText); enumerateSections([&](const SectionInfo &info) { if (clip.top() >= info.rowsBottom) { return true; @@ -1057,6 +1061,12 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { widthForTitle -= remove.width(); } + const auto amCreator = (set.flags & Data::StickersSetFlag::AmCreator); + if (amCreator) { + widthForTitle -= badgeWidth + + st::stickersFeaturedUnreadSkip + + st::stickersHeaderBadgeFontSkip; + } if (titleWidth > widthForTitle) { titleText = st::stickersTrendingHeaderFont->elided(titleText, widthForTitle); titleWidth = st::stickersTrendingHeaderFont->width(titleText); @@ -1064,6 +1074,39 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { p.setFont(st::emojiPanHeaderFont); p.setPen(st().headerFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st().headerTop, width(), titleText, titleWidth); + if (amCreator) { + const auto badgeLeft = st().headerLeft + - st().margin.left() + + titleWidth + + st::stickersFeaturedUnreadSkip; + { + auto color = st().headerFg->c; + color.setAlphaF(st().headerFg->c.alphaF() * 0.15); + p.setPen(Qt::NoPen); + p.setBrush(color); + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect( + style::rtlrect( + badgeLeft, + info.top + st::stickersHeaderBadgeFontTop, + badgeWidth + badgeFont->height, + badgeFont->height, + width()), + badgeFont->height / 2., + badgeFont->height / 2.); + } + p.setPen(st().headerFg); + p.setBrush(Qt::NoBrush); + p.setFont(badgeFont); + p.drawText( + QRect( + badgeLeft + badgeFont->height / 2, + info.top + st::stickersHeaderBadgeFontTop, + badgeWidth, + badgeFont->height), + badgeText, + style::al_center); + } } if (clip.top() + clip.height() <= info.rowsTop) { return true; @@ -1694,12 +1737,32 @@ QPoint StickersListWidget::buttonRippleTopLeft(int section) const { + st().removeSet.rippleAreaPosition; } -void StickersListWidget::showStickerSetBox(not_null document) { +void StickersListWidget::showStickerSetBox( + not_null document, + uint64 setId) { if (document->sticker() && document->sticker()->set) { checkHideWithBox(Box( _show, document->sticker()->set, document->sticker()->setType)); + } else if ((setId == Data::Stickers::FavedSetId) + || (setId == Data::Stickers::RecentSetId)) { + const auto lifetime = std::make_shared(); + constexpr auto kTimeout = 10000; + rpl::merge( + base::timer_once(kTimeout), + document->owner().stickers().updated( + Data::StickersType::Stickers) + ) | rpl::start_with_next([=, weak = Ui::MakeWeak(this)] { + if (weak.data()) { + showStickerSetBox(document, setId); + } + lifetime->destroy(); + }, *lifetime); + document->owner().session().api().requestSpecialStickersForce( + setId == Data::Stickers::FavedSetId, + setId == Data::Stickers::RecentSetId, + false); } } @@ -1756,8 +1819,8 @@ base::unique_qptr StickersListWidget::fillContextMenu( isFaved ? &icons->menuUnfave : &icons->menuFave); if (_features.openStickerSets) { - menu->addAction(tr::lng_context_pack_info(tr::now), [=] { - showStickerSetBox(document); + menu->addAction(tr::lng_context_pack_info(tr::now), [=, id = set.id] { + showStickerSetBox(document, id); }, &icons->menuStickerSet); } @@ -1827,7 +1890,7 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { const auto document = set.stickers[sticker->index].document; if (_features.openStickerSets && (e->modifiers() & Qt::ControlModifier)) { - showStickerSetBox(document); + showStickerSetBox(document, set.id); } else { auto settings = &AyuSettings::getInstance(); auto from = messageSentAnimationInfo( diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index f1a89b210..c436273fc 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -350,7 +350,9 @@ private: void refreshFooterIcons(); void refreshIcons(ValidateIconAnimations animations); - void showStickerSetBox(not_null document); + void showStickerSetBox( + not_null document, + uint64 setId); void cancelSetsSearch(); void showSearchResults(); diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 4540e704b..540271630 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -192,8 +192,11 @@ Application::Application() _platformIntegration->init(); passcodeLockChanges( - ) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=](bool locked) { _shouldLockAt = 0; + if (locked) { + closeAdditionalWindows(); + } }, _lifetime); passcodeLockChanges( @@ -215,6 +218,16 @@ Application::Application() }, _lifetime); } +void Application::closeAdditionalWindows() { + Payments::CheckoutProcess::ClearAll(); + for (const auto &[index, account] : _domain->accounts()) { + if (account->sessionExists()) { + account->session().attachWebView().closeAll(); + } + } + _iv->closeAll(); +} + Application::~Application() { if (_saveSettingsTimer && _saveSettingsTimer->isActive()) { Local::writeSettings(); @@ -234,9 +247,7 @@ Application::~Application() { // // For example Domain::removeRedundantAccounts() is called from // Domain::finish() and there is a violation on Ensures(started()). - Payments::CheckoutProcess::ClearAll(); - InlineBots::AttachWebView::ClearAll(); - _iv->closeAll(); + closeAdditionalWindows(); _domain->finish(); @@ -274,14 +285,9 @@ void Application::run() { refreshGlobalProxy(); // Depends on app settings being read. - if (const auto old = Local::oldSettingsVersion()) { - if (old < AppVersion) { - autoRegisterUrlScheme(); - Platform::NewVersionLaunched(old); - } - } else { - // Initial launch. + if (const auto old = Local::oldSettingsVersion(); old < AppVersion) { autoRegisterUrlScheme(); + Platform::NewVersionLaunched(old); } if (cAutoStart() && !Platform::AutostartSupported()) { @@ -692,7 +698,8 @@ bool Application::eventFilter(QObject *object, QEvent *e) { if (const auto file = event->file(); !file.isEmpty()) { _filesToOpen.append(file); _fileOpenTimer.callOnce(kFileOpenTimeoutMs); - } else if (event->url().scheme() == u"tg"_q) { + } else if (event->url().scheme() == u"tg"_q + || event->url().scheme() == u"tonsite"_q) { const auto url = QString::fromUtf8( event->url().toEncoded().trimmed()); cSetStartUrl(url.mid(0, 8192)); @@ -1093,13 +1100,18 @@ void Application::checkSendPaths() { } void Application::checkStartUrl() { - if (!cStartUrl().isEmpty() - && _lastActivePrimaryWindow - && !_lastActivePrimaryWindow->locked()) { + if (!cStartUrl().isEmpty()) { const auto url = cStartUrl(); - cSetStartUrl(QString()); - if (!openLocalUrl(url, {})) { - cSetStartUrl(url); + if (!Core::App().passcodeLocked()) { + if (url.startsWith("tonsite://", Qt::CaseInsensitive)) { + cSetStartUrl(QString()); + iv().showTonSite(url, {}); + } else if (_lastActivePrimaryWindow) { + cSetStartUrl(QString()); + if (!openLocalUrl(url, {})) { + cSetStartUrl(url); + } + } } } } @@ -1350,7 +1362,7 @@ Window::Controller *Application::ensureSeparateWindowFor( Window::Controller *Application::windowFor(Window::SeparateId id) const { if (const auto separate = separateWindowFor(id)) { return separate; - } else if (id && id.primary()) { + } else if (id && !id.primary()) { return windowFor(not_null(id.account)); } return activePrimaryWindow(); @@ -1807,11 +1819,13 @@ void Application::startShortcuts() { } void Application::RegisterUrlScheme() { + const auto arguments = Launcher::Instance().customWorkingDir() + ? u"-workdir \"%1\""_q.arg(cWorkingDir()) + : QString(); + base::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{ .executable = Platform::ExecutablePathForShortcuts(), - .arguments = Launcher::Instance().customWorkingDir() - ? u"-workdir \"%1\""_q.arg(cWorkingDir()) - : QString(), + .arguments = arguments, .protocol = u"tg"_q, .protocolName = u"Telegram Link"_q, .shortAppName = u"AyuGram"_q, @@ -1819,6 +1833,17 @@ void Application::RegisterUrlScheme() { .displayAppName = AppName.utf16(), .displayAppDescription = AppName.utf16(), }); + + base::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{ + .executable = Platform::ExecutablePathForShortcuts(), + .arguments = arguments, + .protocol = u"tonsite"_q, + .protocolName = u"TonSite Link"_q, + .shortAppName = u"tdesktop"_q, + .longAppName = QCoreApplication::applicationName(), + .displayAppName = AppName.utf16(), + .displayAppDescription = AppName.utf16(), + }); } bool IsAppLaunched() { diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index e73878ef2..a849eaf65 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -379,6 +379,7 @@ private: void showOpenGLCrashNotification(); void clearPasscodeLock(); + void closeAdditionalWindows(); bool openCustomUrl( const QString &protocol, diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 9b03e0b74..b1ba3d166 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_element.h" #include "history/history_item.h" +#include "inline_bots/bot_attach_web_view.h" +#include "data/data_game.h" #include "data/data_user.h" #include "data/data_session.h" #include "window/window_controller.h" @@ -120,7 +122,9 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { return result; }())); } else { - const auto parsedUrl = QUrl::fromUserInput(url); + const auto parsedUrl = url.startsWith(u"tonsite://"_q) + ? QUrl(url) + : QUrl::fromUserInput(url); if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { const auto my = context.value(); if (!my.show) { @@ -171,23 +175,42 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const { if (Core::InternalPassportLink(url)) { return; } - - const auto open = [=] { + const auto openLink = [=] { UrlClickHandler::Open(url, context.other); }; - if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { - open(); - } else if (!_bot - || _bot->isVerified() + const auto my = context.other.value(); + const auto weakController = my.sessionWindow; + const auto controller = weakController.get(); + const auto item = controller + ? controller->session().data().message(my.itemId) + : nullptr; + const auto media = item ? item->media() : nullptr; + const auto game = media ? media->game() : nullptr; + if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || !_bot || !game) { + openLink(); + } + const auto bot = _bot; + const auto title = game->title; + const auto itemId = my.itemId; + const auto openGame = [=] { + bot->session().attachWebView().open({ + .bot = bot, + .button = {.url = url.toUtf8() }, + .source = InlineBots::WebViewSourceGame{ + .messageId = itemId, + .title = title, + }, + }); + }; + if (_bot->isVerified() || _bot->session().local().isBotTrustedOpenGame(_bot->id)) { - open(); + openGame(); } else { - const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { const auto callback = [=, bot = _bot](Fn close) { close(); bot->session().local().markBotTrustedOpenGame(bot->id); - open(); + openGame(); }; controller->show(Ui::MakeConfirmBox({ .text = tr::lng_allow_bot_pass( diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index a63594195..b3aa0bae0 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -21,6 +21,10 @@ namespace Ui { class Show; } // namespace Ui +namespace InlineBots { +struct WebViewContext; +} // namespace InlineBots + namespace Main { class Session; } // namespace Main @@ -38,10 +42,10 @@ class SessionController; class PeerData; struct ClickHandlerContext { FullMsgId itemId; - QString attachBotWebviewUrl; // Is filled from sections. Fn elementDelegate; base::weak_ptr sessionWindow; + std::shared_ptr botWebviewContext; std::shared_ptr show; bool mayShowConfirmation = false; bool skipBotAutoLogin = false; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index a74468cb8..0da3ccbeb 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -224,7 +224,8 @@ QByteArray Settings::serialize() const { + Serialize::bytearraySize(ivPosition) + Serialize::stringSize(noWarningExtensions) + Serialize::stringSize(_customFontFamily) - + sizeof(qint32) * 2; + + sizeof(qint32) * 3 + + Serialize::bytearraySize(_tonsiteStorageToken); auto result = QByteArray(); result.reserve(size); @@ -376,7 +377,9 @@ QByteArray Settings::serialize() const { qRound(_dialogsNoChatWidthRatio.current() * 1000000), 0, 1000000)) - << qint32(_systemUnlockEnabled ? 1 : 0); + << qint32(_systemUnlockEnabled ? 1 : 0) + << qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2) + << _tonsiteStorageToken; } Ensures(result.size() == size); @@ -499,6 +502,8 @@ void Settings::addFromSerialized(const QByteArray &serialized) { QByteArray ivPosition; QString customFontFamily = _customFontFamily; qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0; + qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2; + QByteArray tonsiteStorageToken = _tonsiteStorageToken; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -799,6 +804,12 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> systemUnlockEnabled; } + if (!stream.atEnd()) { + stream >> weatherInCelsius; + } + if (!stream.atEnd()) { + stream >> tonsiteStorageToken; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1008,6 +1019,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) { } _customFontFamily = customFontFamily; _systemUnlockEnabled = (systemUnlockEnabled == 1); + _weatherInCelsius = !weatherInCelsius + ? std::optional() + : (weatherInCelsius == 1); + _tonsiteStorageToken = tonsiteStorageToken; } QString Settings::getSoundPath(const QString &key) const { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index f362bce7d..80b083906 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -897,6 +897,20 @@ public: _systemUnlockEnabled = enabled; } + [[nodiscard]] std::optional weatherInCelsius() const { + return _weatherInCelsius; + } + void setWeatherInCelsius(bool value) { + _weatherInCelsius = value; + } + + [[nodiscard]] QByteArray tonsiteStorageToken() const { + return _tonsiteStorageToken; + } + void setTonsiteStorageToken(const QByteArray &value) { + _tonsiteStorageToken = value; + } + [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); @@ -1028,6 +1042,8 @@ private: WindowPosition _ivPosition; QString _customFontFamily; bool _systemUnlockEnabled = false; + std::optional _weatherInCelsius; + QByteArray _tonsiteStorageToken; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/crash_reports.cpp b/Telegram/SourceFiles/core/crash_reports.cpp index 4c1a4f50e..60762c7d0 100644 --- a/Telegram/SourceFiles/core/crash_reports.cpp +++ b/Telegram/SourceFiles/core/crash_reports.cpp @@ -296,13 +296,17 @@ bool DumpCallback(const google_breakpad::MinidumpDescriptor &md, void *context, QString PlatformString() { if (Platform::IsWindowsStoreBuild()) { - return Platform::IsWindows64Bit() + return Platform::IsWindowsARM64() + ? u"WinStoreARM64"_q + : Platform::IsWindows64Bit() ? u"WinStore64Bit"_q : u"WinStore32Bit"_q; } else if (Platform::IsWindows32Bit()) { return u"Windows32Bit"_q; } else if (Platform::IsWindows64Bit()) { return u"Windows64Bit"_q; + } else if (Platform::IsWindowsARM64()) { + return u"WindowsARM64"_q; } else if (Platform::IsMacStoreBuild()) { return u"MacAppStore"_q; } else if (Platform::IsMac()) { diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp new file mode 100644 index 000000000..2ec4a9cfe --- /dev/null +++ b/Telegram/SourceFiles/core/current_geo_location.cpp @@ -0,0 +1,243 @@ +/* +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 "core/current_geo_location.h" + +#include "base/platform/base_platform_info.h" +#include "base/invoke_queued.h" +#include "base/timer.h" +#include "data/raw/raw_countries_bounds.h" +#include "platform/platform_current_geo_location.h" +#include "ui/ui_utility.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Core { +namespace { + +constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000); + +[[nodiscard]] QString ChooseLanguage(const QString &language) { + // https://docs.mapbox.com/api/search/geocoding#language-coverage + auto result = language.toLower().replace('-', '_'); + static const auto kGood = std::array{ + // Global coverage. + u"de"_q, u"en"_q, u"es"_q, u"fr"_q, u"it"_q, u"nl"_q, u"pl"_q, + + // Local coverage. + u"az"_q, u"bn"_q, u"ca"_q, u"cs"_q, u"da"_q, u"el"_q, u"fa"_q, + u"fi"_q, u"ga"_q, u"hu"_q, u"id"_q, u"is"_q, u"ja"_q, u"ka"_q, + u"km"_q, u"ko"_q, u"lt"_q, u"lv"_q, u"mn"_q, u"pt"_q, u"ro"_q, + u"sk"_q, u"sq"_q, u"sv"_q, u"th"_q, u"tl"_q, u"uk"_q, u"vi"_q, + u"zh"_q, u"zh_Hans"_q, u"zh_TW"_q, + + // Limited coverage. + u"ar"_q, u"bs"_q, u"gu"_q, u"he"_q, u"hi"_q, u"kk"_q, u"lo"_q, + u"my"_q, u"nb"_q, u"ru"_q, u"sr"_q, u"te"_q, u"tk"_q, u"tr"_q, + u"zh_Hant"_q, + }; + for (const auto &known : kGood) { + if (known.toLower() == result) { + return known; + } + } + if (const auto delimeter = result.indexOf('_'); delimeter > 0) { + result = result.mid(0, delimeter); + for (const auto &known : kGood) { + if (known == result) { + return known; + } + } + } + return u"en"_q; +} + +void ResolveLocationAddressGeneric( + const GeoLocation &location, + const QString &language, + const QString &token, + Fn callback) { + const auto partialUrl = u"https://api.mapbox.com/search/geocode/v6" + "/reverse?longitude=%1&latitude=%2&language=%3&access_token=%4"_q + .arg(location.point.y()) + .arg(location.point.x()) + .arg(ChooseLanguage(language)); + static auto Cache = base::flat_map(); + const auto i = Cache.find(partialUrl); + if (i != end(Cache)) { + callback(i->second); + return; + } + const auto finishWith = [=](GeoAddress result) { + Cache[partialUrl] = result; + callback(result); + }; + + struct State final : QObject { + explicit State(QObject *parent) + : QObject(parent) + , manager(this) + , destroyer([=] { if (sent.empty()) delete this; }) { + } + + QNetworkAccessManager manager; + std::vector> sent; + base::Timer destroyer; + }; + + static auto state = QPointer(); + if (!state) { + state = Ui::CreateChild(qApp); + } + const auto destroyReplyDelayed = [](QNetworkReply *reply) { + InvokeQueued(reply, [=] { + for (auto i = begin(state->sent); i != end(state->sent);) { + if (!*i || *i == reply) { + i = state->sent.erase(i); + } else { + ++i; + } + } + delete reply; + if (state->sent.empty()) { + state->destroyer.callOnce(kDestroyManagerTimeout); + } + }); + }; + + auto request = QNetworkRequest(partialUrl.arg(token)); + request.setRawHeader("Referer", "http://desktop-app-resource/"); + + const auto reply = state->manager.get(request); + QObject::connect(reply, &QNetworkReply::finished, [=] { + destroyReplyDelayed(reply); + + const auto json = QJsonDocument::fromJson(reply->readAll()); + if (!json.isObject()) { + finishWith({}); + return; + } + const auto features = json["features"].toArray(); + if (features.isEmpty()) { + finishWith({}); + return; + } + const auto feature = features.at(0).toObject(); + const auto properties = feature["properties"].toObject(); + const auto context = properties["context"].toObject(); + auto names = QStringList(); + auto add = [&](std::vector keys) { + for (const auto &key : keys) { + const auto value = context[key]; + if (value.isObject()) { + const auto name = value.toObject()["name"].toString(); + if (!name.isEmpty()) { + names.push_back(name); + break; + } + } + } + }; + add({ /*u"address"_q, u"street"_q, */u"neighborhood"_q }); + add({ u"place"_q, u"region"_q }); + add({ u"country"_q }); + finishWith({ .name = names.join(", ") }); + }); + QObject::connect(reply, &QNetworkReply::errorOccurred, [=] { + destroyReplyDelayed(reply); + + finishWith({}); + }); +} + +} // namespace + +GeoLocation ResolveCurrentCountryLocation() { + const auto iso2 = Platform::SystemCountry().toUpper(); + const auto &bounds = Raw::CountryBounds(); + const auto i = bounds.find(iso2); + if (i == end(bounds)) { + return { + .accuracy = GeoLocationAccuracy::Failed, + }; + } + return { + .point = { + (i->second.minLat + i->second.maxLat) / 2., + (i->second.minLon + i->second.maxLon) / 2., + }, + .bounds = { + i->second.minLat, + i->second.minLon, + i->second.maxLat - i->second.minLat, + i->second.maxLon - i->second.minLon, + }, + .accuracy = GeoLocationAccuracy::Country, + }; +} + +void ResolveCurrentGeoLocation(Fn callback) { + using namespace Platform; + return ResolveCurrentExactLocation([done = std::move(callback)]( + GeoLocation result) { + done(result.accuracy != GeoLocationAccuracy::Failed + ? result + : ResolveCurrentCountryLocation()); + }); +} + +void ResolveLocationAddress( + const GeoLocation &location, + const QString &language, + const QString &token, + Fn callback) { + auto done = [=, done = std::move(callback)](GeoAddress result) mutable { + if (!result && !token.isEmpty()) { + ResolveLocationAddressGeneric( + location, + language, + token, + std::move(done)); + } else { + done(result); + } + }; + Platform::ResolveLocationAddress(location, language, std::move(done)); +} + +bool AreTheSame(const GeoLocation &a, const GeoLocation &b) { + if (a.accuracy != GeoLocationAccuracy::Exact + || b.accuracy != GeoLocationAccuracy::Exact) { + return false; + } + const auto normalize = [](float64 value) { + value = std::fmod(value + 180., 360.); + return (value + (value < 0. ? 360. : 0.)) - 180.; + }; + constexpr auto kEpsilon = 0.0001; + const auto lon1 = normalize(a.point.y()); + const auto lon2 = normalize(b.point.y()); + const auto diffLat = std::abs(a.point.x() - b.point.x()); + if (std::abs(a.point.x()) >= (90. - kEpsilon) + || std::abs(b.point.x()) >= (90. - kEpsilon)) { + return diffLat <= kEpsilon; + } + auto diffLon = std::abs(lon1 - lon2); + if (diffLon > 180.) { + diffLon = 360. - diffLon; + } + + return diffLat <= kEpsilon && diffLon <= kEpsilon; +} + +} // namespace Core diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h new file mode 100644 index 000000000..3b495f115 --- /dev/null +++ b/Telegram/SourceFiles/core/current_geo_location.h @@ -0,0 +1,60 @@ +/* +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 Core { + +enum class GeoLocationAccuracy : uchar { + Exact, + Country, + Failed, +}; + +struct GeoLocation { + QPointF point; + QRectF bounds; + GeoLocationAccuracy accuracy = GeoLocationAccuracy::Failed; + + [[nodiscard]] bool exact() const { + return accuracy == GeoLocationAccuracy::Exact; + } + [[nodiscard]] bool country() const { + return accuracy == GeoLocationAccuracy::Country; + } + [[nodiscard]] bool failed() const { + return accuracy == GeoLocationAccuracy::Failed; + } + + explicit operator bool() const { + return !failed(); + } +}; + +[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b); + +struct GeoAddress { + QString name; + + [[nodiscard]] bool empty() const { + return name.isEmpty(); + } + explicit operator bool() const { + return !empty(); + } +}; + +[[nodiscard]] GeoLocation ResolveCurrentCountryLocation(); +void ResolveCurrentGeoLocation(Fn callback); + +void ResolveLocationAddress( + const GeoLocation &location, + const QString &language, + const QString &token, + Fn callback); + +} // namespace Core diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 815bb7d6d..7fae94334 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -331,21 +331,6 @@ bool ConfirmPhone( return true; } -bool ShareGameScore( - Window::SessionController *controller, - const Match &match, - const QVariant &context) { - if (!controller) { - return false; - } - const auto params = url_parse_params( - match->captured(1), - qthelp::UrlParamNameTransform::ToLower); - ShareGameScoreByHash(controller, params.value(u"hash"_q)); - controller->window().activate(); - return true; -} - bool ApplySocksProxy( Window::SessionController *controller, const Match &match, @@ -522,7 +507,9 @@ bool ResolveUsernameOrPhone( return false; } using ResolveType = Window::ResolveType; - auto resolveType = ResolveType::Default; + auto resolveType = params.contains(u"profile"_q) + ? ResolveType::Profile + : ResolveType::Default; auto startToken = params.value(u"start"_q); if (!startToken.isEmpty()) { resolveType = ResolveType::BotStart; @@ -592,8 +579,11 @@ bool ResolveUsernameOrPhone( : (appname.isEmpty() && params.contains(u"startapp"_q)) ? params.value(u"startapp"_q) : std::optional()), - .attachBotMenuOpen = (appname.isEmpty() + .attachBotMainOpen = (appname.isEmpty() && params.contains(u"startapp"_q)), + .attachBotMainCompact = (appname.isEmpty() + && params.contains(u"startapp"_q) + && (params.value(u"mode"_q) == u"compact"_q)), .attachBotChooseTypes = InlineBots::ParseChooseTypes( params.value(u"choose"_q)), .voicechatHash = (params.contains(u"livestream"_q) @@ -604,7 +594,7 @@ bool ResolveUsernameOrPhone( ? std::make_optional(params.value(u"voicechat"_q)) : std::nullopt), .clickFromMessageId = myContext.itemId, - .clickFromAttachBotWebviewUrl = myContext.attachBotWebviewUrl, + .clickFromBotWebviewContext = myContext.botWebviewContext, }); return true; } @@ -645,7 +635,7 @@ bool ResolvePrivatePost( } : Window::RepliesByLinkInfo{ v::null }, .clickFromMessageId = my.itemId, - .clickFromAttachBotWebviewUrl = my.attachBotWebviewUrl, + .clickFromBotWebviewContext = my.botWebviewContext, }); controller->window().activate(); return true; @@ -1197,7 +1187,7 @@ bool ResolveChatLink( controller->showPeerByLink(Window::PeerByLinkInfo{ .chatLinkSlug = match->captured(1), .clickFromMessageId = myContext.itemId, - .clickFromAttachBotWebviewUrl = myContext.attachBotWebviewUrl, + .clickFromBotWebviewContext = myContext.botWebviewContext, }); return true; } @@ -1234,10 +1224,6 @@ const std::vector &LocalUrlHandlers() { u"^confirmphone/?\\?(.+)(#|$)"_q, ConfirmPhone }, - { - u"^share_game_score/?\\?(.+)(#|$)"_q, - ShareGameScore - }, { u"^socks/?\\?(.+)(#|$)"_q, ApplySocksProxy @@ -1291,7 +1277,7 @@ const std::vector &LocalUrlHandlers() { ResolveBoost, }, { - u"^message/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, + u"^message/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, ResolveChatLink }, { @@ -1363,6 +1349,13 @@ QString TryConvertUrlToLocal(QString url) { using namespace qthelp; auto matchOptions = RegExOption::CaseInsensitive; + auto tonsiteMatch = (url.indexOf(u".ton") >= 0) + ? regex_match(u"^(https?://)?[^/@:]+\\.ton($|/)"_q, url, matchOptions) + : RegularExpressionMatch(QRegularExpressionMatch()); + if (tonsiteMatch) { + const auto protocol = tonsiteMatch->captured(1); + return u"tonsite://"_q + url.mid(protocol.size()); + } auto subdomainMatch = regex_match(u"^(https?://)?([a-zA-Z0-9\\_]+)\\.t\\.me(/\\d+)?/?(\\?.+)?"_q, url, matchOptions); if (subdomainMatch) { const auto name = subdomainMatch->captured(2); diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 1e09dd3df..67b757449 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -242,6 +242,9 @@ bool UiIntegration::handleUrlClick( } else if (local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { Core::App().openLocalUrl(local, context); return true; + } else if (local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) { + Core::App().iv().showTonSite(local, context); + return true; } else if (local.startsWith(u"internal:"_q, Qt::CaseInsensitive)) { Core::App().openInternalUrl(local, context); return true; diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 8c530334c..0768aad93 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -245,6 +245,7 @@ QString FindUpdateFile() { "^(" "tupdate|" "tx64upd|" + "tarm64upd|" "tmacupd|" "tarmacupd|" "tlinuxupd|" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 5db1682b1..187ecb7a0 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5002002; -constexpr auto AppVersionStr = "5.2.2"; +constexpr auto AppVersion = 5003002; +constexpr auto AppVersionStr = "5.3.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index e158c7a3a..d9bb6ec7a 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -130,6 +130,42 @@ void BusinessInfo::saveChatIntro(ChatIntro data, Fn fail) { session->user()->setBusinessDetails(std::move(details)); } +void BusinessInfo::saveLocation( + BusinessLocation data, + Fn fail) { + const auto session = &_owner->session(); + auto details = session->user()->businessDetails(); + const auto &was = details.location; + if (was == data) { + return; + } else { + const auto session = &_owner->session(); + using Flag = MTPaccount_UpdateBusinessLocation::Flag; + session->api().request(MTPaccount_UpdateBusinessLocation( + MTP_flags((data.point ? Flag::f_geo_point : Flag()) + | (data.address.isEmpty() ? Flag() : Flag::f_address)), + (data.point + ? MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(data.point->lat()), + MTP_double(data.point->lon()), + MTPint()) // accuracy_radius + : MTP_inputGeoPointEmpty()), + MTP_string(data.address) + )).fail([=](const MTP::Error &error) { + auto details = session->user()->businessDetails(); + details.location = was; + session->user()->setBusinessDetails(std::move(details)); + if (fail) { + fail(error.type()); + } + }).send(); + } + + details.location = std::move(data); + session->user()->setBusinessDetails(std::move(details)); +} + void BusinessInfo::applyAwaySettings(AwaySettings data) { if (_awaySettings == data) { return; diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index 7b99e4c8f..01993f223 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -22,6 +22,7 @@ public: void saveWorkingHours(WorkingHours data, Fn fail); void saveChatIntro(ChatIntro data, Fn fail); + void saveLocation(BusinessLocation data, Fn fail); void saveAwaySettings(AwaySettings data, Fn fail); void applyAwaySettings(AwaySettings data); diff --git a/Telegram/SourceFiles/data/components/location_pickers.cpp b/Telegram/SourceFiles/data/components/location_pickers.cpp new file mode 100644 index 000000000..4402e4ccf --- /dev/null +++ b/Telegram/SourceFiles/data/components/location_pickers.cpp @@ -0,0 +1,44 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/components/location_pickers.h" + +#include "api/api_common.h" +#include "ui/controls/location_picker.h" + +namespace Data { + +struct LocationPickers::Entry { + Api::SendAction action; + base::weak_ptr picker; +}; + +LocationPickers::LocationPickers() = default; + +LocationPickers::~LocationPickers() = default; + +Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) { + for (auto i = begin(_pickers); i != end(_pickers);) { + if (const auto strong = i->picker.get()) { + if (i->action == action) { + return i->picker.get(); + } + ++i; + } else { + i = _pickers.erase(i); + } + } + return nullptr; +} + +void LocationPickers::emplace( + const Api::SendAction &action, + not_null picker) { + _pickers.push_back({ action, picker }); +} + +} // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/components/location_pickers.h b/Telegram/SourceFiles/data/components/location_pickers.h new file mode 100644 index 000000000..ad1046095 --- /dev/null +++ b/Telegram/SourceFiles/data/components/location_pickers.h @@ -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 + +#include "base/weak_ptr.h" + +namespace Api { +struct SendAction; +} // namespace Api + +namespace Ui { +class LocationPicker; +} // namespace Ui + +namespace Data { + +class LocationPickers final { +public: + LocationPickers(); + ~LocationPickers(); + + Ui::LocationPicker *lookup(const Api::SendAction &action); + void emplace( + const Api::SendAction &action, + not_null picker); + +private: + struct Entry; + + std::vector _pickers; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp index f476869b6..57974febf 100644 --- a/Telegram/SourceFiles/data/components/top_peers.cpp +++ b/Telegram/SourceFiles/data/components/top_peers.cpp @@ -41,12 +41,36 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000); ) / 1'000'000.; } +[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) { + switch (type) { + case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents(); + case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp(); + } + Unexpected("Type in TypeToCategory."); +} + +[[nodiscard]] auto TypeToGetFlags(TopPeerType type) { + using Flag = MTPcontacts_GetTopPeers::Flag; + switch (type) { + case TopPeerType::Chat: return Flag::f_correspondents; + case TopPeerType::BotApp: return Flag::f_bots_app; + } + Unexpected("Type in TypeToGetFlags."); +} + } // namespace -TopPeers::TopPeers(not_null session) -: _session(session) { +TopPeers::TopPeers(not_null session, TopPeerType type) +: _session(session) +, _type(type) { + if (_type == TopPeerType::Chat) { + loadAfterChats(); + } +} + +void TopPeers::loadAfterChats() { using namespace rpl::mappers; - crl::on_main(session, [=] { + crl::on_main(_session, [=] { _session->data().chatsListLoadedEvents( ) | rpl::filter(_1 == nullptr) | rpl::start_with_next([=] { crl::on_main(_session, [=] { @@ -84,7 +108,7 @@ void TopPeers::remove(not_null peer) { } _requestId = _session->api().request(MTPcontacts_ResetTopPeerRating( - MTP_topPeerCategoryCorrespondents(), + TypeToCategory(_type), peer->input )).send(); } @@ -160,11 +184,13 @@ void TopPeers::request() { } _requestId = _session->api().request(MTPcontacts_GetTopPeers( - MTP_flags(MTPcontacts_GetTopPeers::Flag::f_correspondents), + MTP_flags(TypeToGetFlags(_type)), MTP_int(0), MTP_int(kLimit), MTP_long(countHash()) - )).done([=](const MTPcontacts_TopPeers &result, const MTP::Response &response) { + )).done([=]( + const MTPcontacts_TopPeers &result, + const MTP::Response &response) { _lastReceivedDate = TimeId(response.outerMsgId >> 32); _lastReceived = crl::now(); _requestId = 0; @@ -176,19 +202,22 @@ void TopPeers::request() { 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 &) { + const auto cons = (_type == TopPeerType::Chat) + ? mtpc_topPeerCategoryCorrespondents + : mtpc_topPeerCategoryBotsApp; + if (data.vcategory().type() != cons) { LOG(("API Error: Unexpected top peer category.")); - }); + continue; + } + _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; } updated(); }, [&](const MTPDcontacts_topPeersDisabled &) { diff --git a/Telegram/SourceFiles/data/components/top_peers.h b/Telegram/SourceFiles/data/components/top_peers.h index 5f1250b53..7f834ad84 100644 --- a/Telegram/SourceFiles/data/components/top_peers.h +++ b/Telegram/SourceFiles/data/components/top_peers.h @@ -13,9 +13,14 @@ class Session; namespace Data { +enum class TopPeerType { + Chat, + BotApp, +}; + class TopPeers final { public: - explicit TopPeers(not_null session); + TopPeers(not_null session, TopPeerType type); ~TopPeers(); [[nodiscard]] std::vector> list() const; @@ -36,11 +41,13 @@ private: float64 rating = 0.; }; + void loadAfterChats(); void request(); [[nodiscard]] uint64 countHash() const; void updated(); const not_null _session; + const TopPeerType _type = {}; std::vector _list; rpl::event_stream<> _updates; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 2f7694395..6162024fd 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1089,7 +1089,8 @@ void ApplyChannelUpdate( | Flag::CanGetStatistics | Flag::ViewAsMessages | Flag::CanViewRevenue - | Flag::PaidMediaAllowed; + | Flag::PaidMediaAllowed + | Flag::CanViewCreditsRevenue; channel->setFlags((channel->flags() & ~mask) | (update.is_can_set_username() ? Flag::CanSetUsername : Flag()) | (update.is_can_view_participants() @@ -1107,7 +1108,10 @@ void ApplyChannelUpdate( ? Flag::ViewAsMessages : Flag()) | (update.is_paid_media_allowed() ? Flag::PaidMediaAllowed : Flag()) - | (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag())); + | (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag()) + | (update.is_can_view_stars_revenue() + ? Flag::CanViewCreditsRevenue + : Flag())); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { channel->addFlags(Flag::Megagroup); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index d6f880bda..81ad7778d 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -67,6 +67,7 @@ enum class ChannelDataFlag : uint64 { SimilarExpanded = (1ULL << 31), CanViewRevenue = (1ULL << 32), PaidMediaAllowed = (1ULL << 33), + CanViewCreditsRevenue = (1ULL << 34), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 3e8006259..bf463b5ea 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -47,6 +47,7 @@ ChatFilter::ChatFilter( FilterId id, const QString &title, const QString &iconEmoji, + std::optional colorIndex, Flags flags, base::flat_set> always, std::vector> pinned, @@ -54,6 +55,7 @@ ChatFilter::ChatFilter( : _id(id) , _title(title) , _iconEmoji(iconEmoji) +, _colorIndex(colorIndex) , _always(std::move(always)) , _pinned(std::move(pinned)) , _never(std::move(never)) @@ -99,6 +101,9 @@ ChatFilter ChatFilter::FromTL( data.vid().v, qs(data.vtitle()), qs(data.vemoticon().value_or_empty()), + data.vcolor() + ? std::make_optional(data.vcolor()->v) + : std::nullopt, flags, std::move(list), std::move(pinned), @@ -144,6 +149,9 @@ ChatFilter ChatFilter::FromTL( data.vid().v, qs(data.vtitle()), qs(data.vemoticon().value_or_empty()), + data.vcolor() + ? std::make_optional(data.vcolor()->v) + : std::nullopt, (Flag::Chatlist | (data.is_has_my_invites() ? Flag::HasMyLinks : Flag())), std::move(list), @@ -193,18 +201,20 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { } if (_flags & Flag::Chatlist) { using TLFlag = MTPDdialogFilterChatlist::Flag; - const auto flags = TLFlag::f_emoticon; + const auto flags = TLFlag::f_emoticon + | (_colorIndex ? TLFlag::f_color : TLFlag(0)); return MTP_dialogFilterChatlist( MTP_flags(flags), MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), - MTPint(), // color + MTP_int(_colorIndex.value_or(0)), MTP_vector(pinned), MTP_vector(include)); } using TLFlag = MTPDdialogFilter::Flag; const auto flags = TLFlag::f_emoticon + | (_colorIndex ? TLFlag::f_color : TLFlag(0)) | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) @@ -225,7 +235,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), - MTPint(), // color + MTP_int(_colorIndex.value_or(0)), MTP_vector(pinned), MTP_vector(include), MTP_vector(never)); @@ -243,6 +253,10 @@ QString ChatFilter::iconEmoji() const { return _iconEmoji; } +std::optional ChatFilter::colorIndex() const { + return _colorIndex; +} + ChatFilter::Flags ChatFilter::flags() const { return _flags; } @@ -572,7 +586,7 @@ void ChatFilters::applyInsert(ChatFilter filter, int position) { _list.insert( begin(_list) + position, - ChatFilter(filter.id(), {}, {}, {}, {}, {}, {})); + ChatFilter(filter.id(), {}, {}, {}, {}, {}, {}, {})); applyChange(*(begin(_list) + position), std::move(filter)); } @@ -599,7 +613,7 @@ void ChatFilters::applyRemove(int position) { Expects(position >= 0 && position < _list.size()); const auto i = begin(_list) + position; - applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {})); + applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {}, {})); _list.erase(i); } @@ -728,6 +742,7 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( id, i->title(), i->iconEmoji(), + i->colorIndex(), i->flags(), std::move(always), std::move(pinned), diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 7b5a96476..e123ab2c1 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -52,6 +52,7 @@ public: FilterId id, const QString &title, const QString &iconEmoji, + std::optional colorIndex, Flags flags, base::flat_set> always, std::vector> pinned, @@ -71,6 +72,7 @@ public: [[nodiscard]] FilterId id() const; [[nodiscard]] QString title() const; [[nodiscard]] QString iconEmoji() const; + [[nodiscard]] std::optional colorIndex() const; [[nodiscard]] Flags flags() const; [[nodiscard]] bool chatlist() const; [[nodiscard]] bool hasMyLinks() const; @@ -84,6 +86,7 @@ private: FilterId _id = 0; QString _title; QString _iconEmoji; + std::optional _colorIndex; base::flat_set> _always; std::vector> _pinned; base::flat_set> _never; @@ -94,6 +97,7 @@ private: inline bool operator==(const ChatFilter &a, const ChatFilter &b) { return (a.title() == b.title()) && (a.iconEmoji() == b.iconEmoji()) + && (a.colorIndex() == b.colorIndex()) && (a.flags() == b.flags()) && (a.always() == b.always()) && (a.never() == b.never()); diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 75da4db5b..ee8d948a7 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -15,6 +15,7 @@ struct CreditTopupOption final { QString currency; uint64 amount = 0; bool extended = false; + uint64 giftBarePeerId = 0; }; using CreditTopupOptions = std::vector; @@ -57,7 +58,7 @@ struct CreditsHistoryEntry final { QDateTime successDate; QString successLink; bool in = false; - + bool gift = false; }; struct CreditsStatusSlice final { diff --git a/Telegram/SourceFiles/data/data_location.cpp b/Telegram/SourceFiles/data/data_location.cpp index 80727b3ba..457bb6411 100644 --- a/Telegram/SourceFiles/data/data_location.cpp +++ b/Telegram/SourceFiles/data/data_location.cpp @@ -26,6 +26,11 @@ LocationPoint::LocationPoint(const MTPDgeoPoint &point) , _access(point.vaccess_hash().v) { } +LocationPoint::LocationPoint(float64 lat, float64 lon, IgnoreAccessHash) +: _lat(lat) +, _lon(lon) { +} + QString LocationPoint::latAsString() const { return AsString(_lat); } diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h index a5e0090db..114d21c08 100644 --- a/Telegram/SourceFiles/data/data_location.h +++ b/Telegram/SourceFiles/data/data_location.h @@ -16,6 +16,11 @@ public: LocationPoint() = default; explicit LocationPoint(const MTPDgeoPoint &point); + enum IgnoreAccessHash { + NoAccessHash, + }; + LocationPoint(float64 lat, float64 lon, IgnoreAccessHash); + [[nodiscard]] QString latAsString() const; [[nodiscard]] QString lonAsString() const; [[nodiscard]] MTPGeoPoint toMTP() const; @@ -45,6 +50,24 @@ private: }; +struct InputVenue { + float64 lat = 0.; + float64 lon = 0.; + QString title; + QString address; + QString provider; + QString id; + QString venueType; + + [[nodiscard]] bool justLocation() const { + return id.isEmpty(); + } + + friend inline bool operator==( + const InputVenue &, + const InputVenue &) = default; +}; + [[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 15dfedecd..70d29f28a 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -121,8 +121,8 @@ struct AlbumCounts { ImageRoundRadius radius, bool spoiler) { const auto original = image->original(); - if (original.width() * 10 < original.height() - || original.height() * 10 < original.width()) { + if (original.width() * 20 < original.height() + || original.height() * 20 < original.width()) { return QImage(); } const auto factor = style::DevicePixelRatio(); @@ -2303,8 +2303,9 @@ ClickHandlerPtr MediaDice::MakeHandler( MediaGiftBox::MediaGiftBox( not_null parent, not_null from, - int months) -: MediaGiftBox(parent, from, GiftCode{ .months = months }) { + GiftType type, + int count) +: MediaGiftBox(parent, from, GiftCode{ .count = count, .type = type }) { } MediaGiftBox::MediaGiftBox( @@ -2631,7 +2632,11 @@ const GiveawayResults *MediaGiveawayResults::giveawayResults() const { } TextWithEntities MediaGiveawayResults::notificationText() const { - return Ui::Text::Colorized({ tr::lng_prizes_results_title(tr::now) }); + return Ui::Text::Colorized({ + ((_data.winnersCount == 1) + ? tr::lng_prizes_results_title_one + : tr::lng_prizes_results_title)(tr::now) + }); } QString MediaGiveawayResults::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 424e2c448..b8c943edc 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -125,10 +125,16 @@ struct GiveawayResults { bool all = false; }; +enum class GiftType : uchar { + Premium, // count - months + Stars, // count - stars +}; + struct GiftCode { QString slug; ChannelData *channel = nullptr; - int months = 0; + int count = 0; + GiftType type = GiftType::Premium; bool viaGiveaway = false; bool unclaimed = false; }; @@ -591,7 +597,8 @@ public: MediaGiftBox( not_null parent, not_null from, - int months); + GiftType type, + int count); MediaGiftBox( not_null parent, not_null from, diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index f7cc4d453..1bb374ce7 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -57,6 +57,12 @@ std::optional OnlineTextSpecial(not_null user) { } else if (user->isSupport()) { return tr::lng_status_support(tr::now); } else if (user->isBot()) { + if (const auto count = user->botInfo->activeUsers) { + return tr::lng_bot_status_users( + tr::now, + lt_count_decimal, + count); + } return tr::lng_status_bot(tr::now); } else if (user->isServiceUser()) { return tr::lng_status_support(tr::now); @@ -69,12 +75,14 @@ std::optional OnlineTextCommon(LastseenStatus status, TimeId now) { return tr::lng_status_online(tr::now); } else if (status.isLongAgo()) { return tr::lng_status_offline(tr::now); - } else if (status.isRecently() || status.isHidden()) { + } else if (status.isRecently()) { return tr::lng_status_recently(tr::now); } else if (status.isWithinWeek()) { return tr::lng_status_last_week(tr::now); } else if (status.isWithinMonth()) { return tr::lng_status_last_month(tr::now); + } else if (status.isHidden()) { + return tr::lng_status_recently(tr::now); } return std::nullopt; } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index bae83a711..4727d5d5c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -731,6 +731,8 @@ not_null Session::processUser(const MTPUser &data) { result->botInfo->supportsAttachMenu = data.is_bot_attach_menu(); result->botInfo->supportsBusiness = data.is_bot_business(); result->botInfo->canEditInformation = data.is_bot_can_edit(); + result->botInfo->activeUsers = data.vbot_active_users().value_or_empty(); + result->botInfo->hasMainApp = data.is_bot_has_main_app(); } else { result->setBotInfoVersion(-1); } @@ -3390,6 +3392,22 @@ void Session::documentApplyFields( } } +not_null Session::venueIconDocument(const QString &icon) { + const auto i = _venueIcons.find(icon); + if (i != end(_venueIcons)) { + return i->second; + } + const auto result = documentFromWeb(MTP_webDocumentNoProxy( + MTP_string(u"https://ss3.4sqi.net/img/categories_v2/"_q + + icon + + u"_64.png"_q), + MTP_int(0), + MTP_string("image/png"), + MTP_vector()), {}, {}); + _venueIcons.emplace(icon, result); + return result; +} + not_null Session::webpage(WebPageId id) { auto i = _webpages.find(id); if (i == _webpages.cend()) { @@ -4582,7 +4600,8 @@ void Session::serviceNotification( MTPVector(), MTPint(), // stories_max_id MTPPeerColor(), // color - MTPPeerColor())); // profile_color + MTPPeerColor(), // profile_color + MTPint())); // bot_active_users } const auto history = this->history(PeerData::kServiceNotificationsId); const auto insert = [=] { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 12c4cba3a..06b4b267d 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -559,6 +559,8 @@ public: const MTPWebDocument &data, const ImageLocation &thumbnailLocation, const ImageLocation &videoThumbnailLocation); + [[nodiscard]] not_null venueIconDocument( + const QString &icon); [[nodiscard]] not_null webpage(WebPageId id); not_null processWebpage(const MTPWebPage &data); @@ -1002,6 +1004,7 @@ private: FullStoryId, base::flat_set>> _storyItems; base::flat_map> _highlightings; + base::flat_map> _venueIcons; base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 38ea28aae..d9c37de4e 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/download_manager_mtproto.h" #include "storage/file_download.h" // kMaxFileInMemory #include "ui/text/text_utilities.h" +#include "ui/color_int_conversion.h" namespace Data { namespace { @@ -40,6 +41,7 @@ using UpdateFlag = StoryUpdate::Flag; return { .geometry = { corner / 100., size / 100. }, .rotation = data.vrotation().v, + .radius = data.vradius().value_or_empty(), }; } @@ -83,6 +85,7 @@ using UpdateFlag = StoryUpdate::Flag; }, [&](const MTPDmediaAreaSuggestedReaction &data) { }, [&](const MTPDmediaAreaChannelPost &data) { }, [&](const MTPDmediaAreaUrl &data) { + }, [&](const MTPDmediaAreaWeather &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -105,6 +108,7 @@ using UpdateFlag = StoryUpdate::Flag; }); }, [&](const MTPDmediaAreaChannelPost &data) { }, [&](const MTPDmediaAreaUrl &data) { + }, [&](const MTPDmediaAreaWeather &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -127,6 +131,7 @@ using UpdateFlag = StoryUpdate::Flag; data.vmsg_id().v), }); }, [&](const MTPDmediaAreaUrl &data) { + }, [&](const MTPDmediaAreaWeather &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -147,6 +152,33 @@ using UpdateFlag = StoryUpdate::Flag; .area = ParseArea(data.vcoordinates()), .url = qs(data.vurl()), }); + }, [&](const MTPDmediaAreaWeather &data) { + }, [&](const MTPDinputMediaAreaChannelPost &data) { + LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); + }, [&](const MTPDinputMediaAreaVenue &data) { + LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); + }); + return result; +} + +[[nodiscard]] auto ParseWeatherArea(const MTPMediaArea &area) +-> std::optional { + auto result = std::optional(); + area.match([&](const MTPDmediaAreaVenue &data) { + }, [&](const MTPDmediaAreaGeoPoint &data) { + }, [&](const MTPDmediaAreaSuggestedReaction &data) { + }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDmediaAreaUrl &data) { + }, [&](const MTPDmediaAreaWeather &data) { + result.emplace(WeatherArea{ + .area = ParseArea(data.vcoordinates()), + .emoji = qs(data.vemoji()), + .color = Ui::Color32FromSerialized(data.vcolor().v), + .millicelsius = int(1000. * std::clamp( + data.vtemperature_c().v, + -274., + 1'000'000.)), + }); }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -689,6 +721,10 @@ const std::vector &Story::urlAreas() const { return _urlAreas; } +const std::vector &Story::weatherAreas() const { + return _weatherAreas; +} + void Story::applyChanges( StoryMedia media, const MTPDstoryItem &data, @@ -793,6 +829,7 @@ void Story::applyFields( auto suggestedReactions = std::vector(); auto channelPosts = std::vector(); auto urlAreas = std::vector(); + auto weatherAreas = std::vector(); if (const auto areas = data.vmedia_areas()) { for (const auto &area : areas->v) { if (const auto location = ParseLocation(area)) { @@ -808,6 +845,8 @@ void Story::applyFields( channelPosts.push_back(*post); } else if (auto url = ParseUrlArea(area)) { urlAreas.push_back(*url); + } else if (auto weather = ParseWeatherArea(area)) { + weatherAreas.push_back(*weather); } } } @@ -821,6 +860,7 @@ void Story::applyFields( = (_suggestedReactions != suggestedReactions); const auto channelPostsChanged = (_channelPosts != channelPosts); const auto urlAreasChanged = (_urlAreas != urlAreas); + const auto weatherAreasChanged = (_weatherAreas != weatherAreas); const auto reactionChanged = (_sentReactionId != reaction); _out = out; @@ -849,6 +889,9 @@ void Story::applyFields( if (urlAreasChanged) { _urlAreas = std::move(urlAreas); } + if (weatherAreasChanged) { + _weatherAreas = std::move(weatherAreas); + } if (reactionChanged) { _sentReactionId = reaction; } @@ -859,7 +902,8 @@ void Story::applyFields( || mediaChanged || locationsChanged || channelPostsChanged - || urlAreasChanged; + || urlAreasChanged + || weatherAreasChanged; const auto reactionsChanged = reactionChanged || suggestedReactionsChanged; if (!initial && (changed || reactionsChanged)) { diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index b318ce9fe..bd508591c 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -81,6 +81,7 @@ struct StoryViews { struct StoryArea { QRectF geometry; float64 rotation = 0; + float64 radius = 0; friend inline bool operator==( const StoryArea &, @@ -131,6 +132,17 @@ struct UrlArea { const UrlArea &) = default; }; +struct WeatherArea { + StoryArea area; + QString emoji; + QColor color; + int millicelsius = 0; + + friend inline bool operator==( + const WeatherArea &, + const WeatherArea &) = default; +}; + class Story final { public: Story( @@ -208,6 +220,8 @@ public: -> const std::vector &; [[nodiscard]] auto urlAreas() const -> const std::vector &; + [[nodiscard]] auto weatherAreas() const + -> const std::vector &; void applyChanges( StoryMedia media, @@ -259,6 +273,7 @@ private: std::vector _suggestedReactions; std::vector _channelPosts; std::vector _urlAreas; + std::vector _weatherAreas; StoryViews _views; StoryViews _channelReactions; const TimeId _date = 0; diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 67da9e3f5..8f84555d3 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -40,12 +40,14 @@ struct BotInfo { int version = 0; int descriptionVersion = 0; + int activeUsers = 0; bool inited : 1 = false; bool readsAllHistory : 1 = false; bool cantJoinGroups : 1 = false; bool supportsAttachMenu : 1 = false; bool canEditInformation : 1 = false; bool supportsBusiness : 1 = false; + bool hasMainApp : 1 = false; }; enum class UserDataFlag : uint32 { diff --git a/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp b/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp new file mode 100644 index 000000000..d4e383121 --- /dev/null +++ b/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp @@ -0,0 +1,193 @@ +/* +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/raw/raw_countries_bounds.h" + +// Source: https://github.com/sandstrom/country-bounding-boxes + +namespace Raw { + +const base::flat_map &CountryBounds() { + static const auto result = base::flat_map{ + { u"AF"_q, GeoBounds{ 60.53, 29.32, 75.16, 38.49 } }, + { u"AO"_q, GeoBounds{ 11.64, -17.93, 24.08, -4.44 } }, + { u"AL"_q, GeoBounds{ 19.3, 39.62, 21.02, 42.69 } }, + { u"AE"_q, GeoBounds{ 51.58, 22.5, 56.4, 26.06 } }, + { u"AR"_q, GeoBounds{ -73.42, -55.25, -53.63, -21.83 } }, + { u"AM"_q, GeoBounds{ 43.58, 38.74, 46.51, 41.25 } }, + { u"AQ"_q, GeoBounds{ -180.0, -90.0, 180.0, -63.27 } }, + { u"TF"_q, GeoBounds{ 68.72, -49.78, 70.56, -48.63 } }, + { u"AU"_q, GeoBounds{ 113.34, -43.63, 153.57, -10.67 } }, + { u"AT"_q, GeoBounds{ 9.48, 46.43, 16.98, 49.04 } }, + { u"AZ"_q, GeoBounds{ 44.79, 38.27, 50.39, 41.86 } }, + { u"BI"_q, GeoBounds{ 29.02, -4.5, 30.75, -2.35 } }, + { u"BE"_q, GeoBounds{ 2.51, 49.53, 6.16, 51.48 } }, + { u"BJ"_q, GeoBounds{ 0.77, 6.14, 3.8, 12.24 } }, + { u"BF"_q, GeoBounds{ -5.47, 9.61, 2.18, 15.12 } }, + { u"BD"_q, GeoBounds{ 88.08, 20.67, 92.67, 26.45 } }, + { u"BG"_q, GeoBounds{ 22.38, 41.23, 28.56, 44.23 } }, + { u"BS"_q, GeoBounds{ -78.98, 23.71, -77.0, 27.04 } }, + { u"BA"_q, GeoBounds{ 15.75, 42.65, 19.6, 45.23 } }, + { u"BY"_q, GeoBounds{ 23.2, 51.32, 32.69, 56.17 } }, + { u"BZ"_q, GeoBounds{ -89.23, 15.89, -88.11, 18.5 } }, + { u"BO"_q, GeoBounds{ -69.59, -22.87, -57.5, -9.76 } }, + { u"BR"_q, GeoBounds{ -73.99, -33.77, -34.73, 5.24 } }, + { u"BN"_q, GeoBounds{ 114.2, 4.01, 115.45, 5.45 } }, + { u"BT"_q, GeoBounds{ 88.81, 26.72, 92.1, 28.3 } }, + { u"BW"_q, GeoBounds{ 19.9, -26.83, 29.43, -17.66 } }, + { u"CF"_q, GeoBounds{ 14.46, 2.27, 27.37, 11.14 } }, + { u"CA"_q, GeoBounds{ -141.0, 41.68, -52.65, 73.23 } }, + { u"CH"_q, GeoBounds{ 6.02, 45.78, 10.44, 47.83 } }, + { u"CL"_q, GeoBounds{ -75.64, -55.61, -66.96, -17.58 } }, + { u"CN"_q, GeoBounds{ 73.68, 18.2, 135.03, 53.46 } }, + { u"CI"_q, GeoBounds{ -8.6, 4.34, -2.56, 10.52 } }, + { u"CM"_q, GeoBounds{ 8.49, 1.73, 16.01, 12.86 } }, + { u"CD"_q, GeoBounds{ 12.18, -13.26, 31.17, 5.26 } }, + { u"CG"_q, GeoBounds{ 11.09, -5.04, 18.45, 3.73 } }, + { u"CO"_q, GeoBounds{ -78.99, -4.3, -66.88, 12.44 } }, + { u"CR"_q, GeoBounds{ -85.94, 8.23, -82.55, 11.22 } }, + { u"CU"_q, GeoBounds{ -84.97, 19.86, -74.18, 23.19 } }, + { u"CY"_q, GeoBounds{ 32.26, 34.57, 34.0, 35.17 } }, + { u"CZ"_q, GeoBounds{ 12.24, 48.56, 18.85, 51.12 } }, + { u"DE"_q, GeoBounds{ 5.99, 47.3, 15.02, 54.98 } }, + { u"DJ"_q, GeoBounds{ 41.66, 10.93, 43.32, 12.7 } }, + { u"DK"_q, GeoBounds{ 8.09, 54.8, 12.69, 57.73 } }, + { u"DO"_q, GeoBounds{ -71.95, 17.6, -68.32, 19.88 } }, + { u"DZ"_q, GeoBounds{ -8.68, 19.06, 12.0, 37.12 } }, + { u"EC"_q, GeoBounds{ -80.97, -4.96, -75.23, 1.38 } }, + { u"EG"_q, GeoBounds{ 24.7, 22.0, 36.87, 31.59 } }, + { u"ER"_q, GeoBounds{ 36.32, 12.46, 43.08, 18.0 } }, + { u"ES"_q, GeoBounds{ -9.39, 35.95, 3.04, 43.75 } }, + { u"EE"_q, GeoBounds{ 23.34, 57.47, 28.13, 59.61 } }, + { u"ET"_q, GeoBounds{ 32.95, 3.42, 47.79, 14.96 } }, + { u"FI"_q, GeoBounds{ 20.65, 59.85, 31.52, 70.16 } }, + { u"FJ"_q, GeoBounds{ -180.0, -18.29, 180.0, -16.02 } }, + { u"FK"_q, GeoBounds{ -61.2, -52.3, -57.75, -51.1 } }, + { u"FR"_q, GeoBounds{ -5.0, 42.5, 9.56, 51.15 } }, + { u"GA"_q, GeoBounds{ 8.8, -3.98, 14.43, 2.33 } }, + { u"GB"_q, GeoBounds{ -7.57, 49.96, 1.68, 58.64 } }, + { u"GE"_q, GeoBounds{ 39.96, 41.06, 46.64, 43.55 } }, + { u"GH"_q, GeoBounds{ -3.24, 4.71, 1.06, 11.1 } }, + { u"GN"_q, GeoBounds{ -15.13, 7.31, -7.83, 12.59 } }, + { u"GM"_q, GeoBounds{ -16.84, 13.13, -13.84, 13.88 } }, + { u"GW"_q, GeoBounds{ -16.68, 11.04, -13.7, 12.63 } }, + { u"GQ"_q, GeoBounds{ 9.31, 1.01, 11.29, 2.28 } }, + { u"GR"_q, GeoBounds{ 20.15, 34.92, 26.6, 41.83 } }, + { u"GL"_q, GeoBounds{ -73.3, 60.04, -12.21, 83.65 } }, + { u"GT"_q, GeoBounds{ -92.23, 13.74, -88.23, 17.82 } }, + { u"GY"_q, GeoBounds{ -61.41, 1.27, -56.54, 8.37 } }, + { u"HN"_q, GeoBounds{ -89.35, 12.98, -83.15, 16.01 } }, + { u"HR"_q, GeoBounds{ 13.66, 42.48, 19.39, 46.5 } }, + { u"HT"_q, GeoBounds{ -74.46, 18.03, -71.62, 19.92 } }, + { u"HU"_q, GeoBounds{ 16.2, 45.76, 22.71, 48.62 } }, + { u"ID"_q, GeoBounds{ 95.29, -10.36, 141.03, 5.48 } }, + { u"IN"_q, GeoBounds{ 68.18, 7.97, 97.4, 35.49 } }, + { u"IE"_q, GeoBounds{ -9.98, 51.67, -6.03, 55.13 } }, + { u"IR"_q, GeoBounds{ 44.11, 25.08, 63.32, 39.71 } }, + { u"IQ"_q, GeoBounds{ 38.79, 29.1, 48.57, 37.39 } }, + { u"IS"_q, GeoBounds{ -24.33, 63.5, -13.61, 66.53 } }, + { u"IL"_q, GeoBounds{ 34.27, 29.5, 35.84, 33.28 } }, + { u"IT"_q, GeoBounds{ 6.75, 36.62, 18.48, 47.12 } }, + { u"JM"_q, GeoBounds{ -78.34, 17.7, -76.2, 18.52 } }, + { u"JO"_q, GeoBounds{ 34.92, 29.2, 39.2, 33.38 } }, + { u"JP"_q, GeoBounds{ 129.41, 31.03, 145.54, 45.55 } }, + { u"KZ"_q, GeoBounds{ 46.47, 40.66, 87.36, 55.39 } }, + { u"KE"_q, GeoBounds{ 33.89, -4.68, 41.86, 5.51 } }, + { u"KG"_q, GeoBounds{ 69.46, 39.28, 80.26, 43.3 } }, + { u"KH"_q, GeoBounds{ 102.35, 10.49, 107.61, 14.57 } }, + { u"KR"_q, GeoBounds{ 126.12, 34.39, 129.47, 38.61 } }, + { u"KW"_q, GeoBounds{ 46.57, 28.53, 48.42, 30.06 } }, + { u"LA"_q, GeoBounds{ 100.12, 13.88, 107.56, 22.46 } }, + { u"LB"_q, GeoBounds{ 35.13, 33.09, 36.61, 34.64 } }, + { u"LR"_q, GeoBounds{ -11.44, 4.36, -7.54, 8.54 } }, + { u"LY"_q, GeoBounds{ 9.32, 19.58, 25.16, 33.14 } }, + { u"LK"_q, GeoBounds{ 79.7, 5.97, 81.79, 9.82 } }, + { u"LS"_q, GeoBounds{ 27.0, -30.65, 29.33, -28.65 } }, + { u"LT"_q, GeoBounds{ 21.06, 53.91, 26.59, 56.37 } }, + { u"LU"_q, GeoBounds{ 5.67, 49.44, 6.24, 50.13 } }, + { u"LV"_q, GeoBounds{ 21.06, 55.62, 28.18, 57.97 } }, + { u"MA"_q, GeoBounds{ -17.02, 21.42, -1.12, 35.76 } }, + { u"MD"_q, GeoBounds{ 26.62, 45.49, 30.02, 48.47 } }, + { u"MG"_q, GeoBounds{ 43.25, -25.6, 50.48, -12.04 } }, + { u"MX"_q, GeoBounds{ -117.13, 14.54, -86.81, 32.72 } }, + { u"MK"_q, GeoBounds{ 20.46, 40.84, 22.95, 42.32 } }, + { u"ML"_q, GeoBounds{ -12.17, 10.1, 4.27, 24.97 } }, + { u"MM"_q, GeoBounds{ 92.3, 9.93, 101.18, 28.34 } }, + { u"ME"_q, GeoBounds{ 18.45, 41.88, 20.34, 43.52 } }, + { u"MN"_q, GeoBounds{ 87.75, 41.6, 119.77, 52.05 } }, + { u"MZ"_q, GeoBounds{ 30.18, -26.74, 40.78, -10.32 } }, + { u"MR"_q, GeoBounds{ -17.06, 14.62, -4.92, 27.4 } }, + { u"MW"_q, GeoBounds{ 32.69, -16.8, 35.77, -9.23 } }, + { u"MY"_q, GeoBounds{ 100.09, 0.77, 119.18, 6.93 } }, + { u"NA"_q, GeoBounds{ 11.73, -29.05, 25.08, -16.94 } }, + { u"NC"_q, GeoBounds{ 164.03, -22.4, 167.12, -20.11 } }, + { u"NE"_q, GeoBounds{ 0.3, 11.66, 15.9, 23.47 } }, + { u"NG"_q, GeoBounds{ 2.69, 4.24, 14.58, 13.87 } }, + { u"NI"_q, GeoBounds{ -87.67, 10.73, -83.15, 15.02 } }, + { u"NL"_q, GeoBounds{ 3.31, 50.8, 7.09, 53.51 } }, + { u"NO"_q, GeoBounds{ 4.99, 58.08, 31.29, 70.92 } }, + { u"NP"_q, GeoBounds{ 80.09, 26.4, 88.17, 30.42 } }, + { u"NZ"_q, GeoBounds{ 166.51, -46.64, 178.52, -34.45 } }, + { u"OM"_q, GeoBounds{ 52.0, 16.65, 59.81, 26.4 } }, + { u"PK"_q, GeoBounds{ 60.87, 23.69, 77.84, 37.13 } }, + { u"PA"_q, GeoBounds{ -82.97, 7.22, -77.24, 9.61 } }, + { u"PE"_q, GeoBounds{ -81.41, -18.35, -68.67, -0.06 } }, + { u"PH"_q, GeoBounds{ 117.17, 5.58, 126.54, 18.51 } }, + { u"PG"_q, GeoBounds{ 141.0, -10.65, 156.02, -2.5 } }, + { u"PL"_q, GeoBounds{ 14.07, 49.03, 24.03, 54.85 } }, + { u"PR"_q, GeoBounds{ -67.24, 17.95, -65.59, 18.52 } }, + { u"KP"_q, GeoBounds{ 124.27, 37.67, 130.78, 42.99 } }, + { u"PT"_q, GeoBounds{ -9.53, 36.84, -6.39, 42.28 } }, + { u"PY"_q, GeoBounds{ -62.69, -27.55, -54.29, -19.34 } }, + { u"QA"_q, GeoBounds{ 50.74, 24.56, 51.61, 26.11 } }, + { u"RO"_q, GeoBounds{ 20.22, 43.69, 29.63, 48.22 } }, + { u"RU"_q, GeoBounds{ -180.0, 41.15, 180.0, 81.25 } }, + { u"RW"_q, GeoBounds{ 29.02, -2.92, 30.82, -1.13 } }, + { u"SA"_q, GeoBounds{ 34.63, 16.35, 55.67, 32.16 } }, + { u"SD"_q, GeoBounds{ 21.94, 8.62, 38.41, 22.0 } }, + { u"SS"_q, GeoBounds{ 23.89, 3.51, 35.3, 12.25 } }, + { u"SN"_q, GeoBounds{ -17.63, 12.33, -11.47, 16.6 } }, + { u"SB"_q, GeoBounds{ 156.49, -10.83, 162.4, -6.6 } }, + { u"SL"_q, GeoBounds{ -13.25, 6.79, -10.23, 10.05 } }, + { u"SV"_q, GeoBounds{ -90.1, 13.15, -87.72, 14.42 } }, + { u"SO"_q, GeoBounds{ 40.98, -1.68, 51.13, 12.02 } }, + { u"RS"_q, GeoBounds{ 18.83, 42.25, 22.99, 46.17 } }, + { u"SR"_q, GeoBounds{ -58.04, 1.82, -53.96, 6.03 } }, + { u"SK"_q, GeoBounds{ 16.88, 47.76, 22.56, 49.57 } }, + { u"SI"_q, GeoBounds{ 13.7, 45.45, 16.56, 46.85 } }, + { u"SE"_q, GeoBounds{ 11.03, 55.36, 23.9, 69.11 } }, + { u"SZ"_q, GeoBounds{ 30.68, -27.29, 32.07, -25.66 } }, + { u"SY"_q, GeoBounds{ 35.7, 32.31, 42.35, 37.23 } }, + { u"TD"_q, GeoBounds{ 13.54, 7.42, 23.89, 23.41 } }, + { u"TG"_q, GeoBounds{ -0.05, 5.93, 1.87, 11.02 } }, + { u"TH"_q, GeoBounds{ 97.38, 5.69, 105.59, 20.42 } }, + { u"TJ"_q, GeoBounds{ 67.44, 36.74, 74.98, 40.96 } }, + { u"TM"_q, GeoBounds{ 52.5, 35.27, 66.55, 42.75 } }, + { u"TL"_q, GeoBounds{ 124.97, -9.39, 127.34, -8.27 } }, + { u"TT"_q, GeoBounds{ -61.95, 10.0, -60.9, 10.89 } }, + { u"TN"_q, GeoBounds{ 7.52, 30.31, 11.49, 37.35 } }, + { u"TR"_q, GeoBounds{ 26.04, 35.82, 44.79, 42.14 } }, + { u"TW"_q, GeoBounds{ 120.11, 21.97, 121.95, 25.3 } }, + { u"TZ"_q, GeoBounds{ 29.34, -11.72, 40.32, -0.95 } }, + { u"UG"_q, GeoBounds{ 29.58, -1.44, 35.04, 4.25 } }, + { u"UA"_q, GeoBounds{ 22.09, 44.36, 40.08, 52.34 } }, + { u"UY"_q, GeoBounds{ -58.43, -34.95, -53.21, -30.11 } }, + { u"US"_q, GeoBounds{ -125.0, 25.0, -66.96, 49.5 } }, + { u"UZ"_q, GeoBounds{ 55.93, 37.14, 73.06, 45.59 } }, + { u"VE"_q, GeoBounds{ -73.3, 0.72, -59.76, 12.16 } }, + { u"VN"_q, GeoBounds{ 102.17, 8.6, 109.34, 23.35 } }, + { u"VU"_q, GeoBounds{ 166.63, -16.6, 167.84, -14.63 } }, + { u"PS"_q, GeoBounds{ 34.93, 31.35, 35.55, 32.53 } }, + { u"YE"_q, GeoBounds{ 42.6, 12.59, 53.11, 19.0 } }, + { u"ZA"_q, GeoBounds{ 16.34, -34.82, 32.83, -22.09 } }, + { u"ZM"_q, GeoBounds{ 21.89, -17.96, 33.49, -8.24 } }, + { u"ZW"_q, GeoBounds{ 25.26, -22.27, 32.85, -15.51 } } + }; + return result; +} + +} // namespace Raw diff --git a/Telegram/SourceFiles/data/raw/raw_countries_bounds.h b/Telegram/SourceFiles/data/raw/raw_countries_bounds.h new file mode 100644 index 000000000..4f0944c81 --- /dev/null +++ b/Telegram/SourceFiles/data/raw/raw_countries_bounds.h @@ -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 + +#include + +namespace Raw { + +struct GeoBounds { + double minLat = 0.; + double minLon = 0.; + double maxLat = 0.; + double maxLon = 0.; +}; + +[[nodiscard]] const base::flat_map &CountryBounds(); + +} // namespace Raw diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 04bb9c925..1721a53ca 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -228,7 +228,7 @@ void Stickers::incrementSticker(not_null document) { auto index = set->stickers.indexOf(document); if (index > 0) { if (set->dates.empty()) { - session().api().requestRecentStickersForce(); + session().api().requestSpecialStickersForce(false, true, false); } else { Assert(set->dates.size() == set->stickers.size()); set->dates.erase(set->dates.begin() + index); @@ -260,7 +260,7 @@ void Stickers::incrementSticker(not_null document) { set->emoji[emoji].push_front(document); } } else { - session().api().requestRecentStickersForce(); + session().api().requestSpecialStickersForce(false, true, false); } writeRecentStickers = true; diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp index 324377242..75d98404e 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp @@ -55,7 +55,8 @@ StickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) { | (data.vinstalled_date() ? Flag::Installed : Flag()) //| (data.is_videos() ? Flag::Webm : Flag()) | (data.is_text_color() ? Flag::TextColor : Flag()) - | (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag()); + | (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag()) + | (data.is_creator() ? Flag::AmCreator : Flag()); } StickersSet::StickersSet( diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.h b/Telegram/SourceFiles/data/stickers/data_stickers_set.h index e77ee46b5..c218ce2fa 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.h @@ -59,6 +59,7 @@ enum class StickersSetFlag : ushort { Emoji = (1 << 9), TextColor = (1 << 10), ChannelStatus = (1 << 11), + AmCreator = (1 << 12), }; inline constexpr bool is_flag_type(StickersSetFlag) { return true; }; using StickersSetFlags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index cb33e8a9a..6e60f072d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -3814,7 +3814,9 @@ ChosenRow InnerWidget::computeChosenRow() const { bool InnerWidget::isUserpicPress() const { return (_lastRowLocalMouseX >= 0) - && (_lastRowLocalMouseX < _st->nameLeft); + && (_lastRowLocalMouseX < _st->nameLeft) + && (_collapsedSelected < 0 + || _collapsedSelected >= _collapsedRows.size()); } bool InnerWidget::isUserpicPressOnWide() const { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 333282c92..41a9ddd8d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -78,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" #include "info/info_memento.h" +#include "inline_bots/bot_attach_web_view.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" @@ -1282,6 +1283,27 @@ void Widget::updateSuggestions(anim::type animated) { } }, _suggestions->lifetime()); + _suggestions->recentAppChosen( + ) | rpl::start_with_next([=](not_null peer) { + if (const auto user = peer->asUser()) { + if (const auto info = user->botInfo.get()) { + if (info->hasMainApp) { + openBotMainApp(user); + return; + } + } + } + chosenRow({ + .key = peer->owner().history(peer), + .newWindow = base::IsCtrlPressed(), + }); + }, _suggestions->lifetime()); + + _suggestions->popularAppChosen( + ) | rpl::start_with_next([=](not_null peer) { + controller()->showPeerInfo(peer); + }, _suggestions->lifetime()); + updateControlsGeometry(); _suggestions->show(animated, [=] { @@ -1293,6 +1315,17 @@ void Widget::updateSuggestions(anim::type animated) { } } +void Widget::openBotMainApp(not_null bot) { + session().attachWebView().open({ + .bot = bot, + .context = { + .controller = controller(), + .maySkipConfirmation = true, + }, + .source = InlineBots::WebViewSourceBotProfile(), + }); +} + void Widget::changeOpenedSubsection( FnMut change, bool fromRight, diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 347683d0c..a1ee3950d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -214,6 +214,7 @@ private: void refreshTopBars(); void showSearchInTopBar(anim::type animated); void checkUpdateStatus(); + void openBotMainApp(not_null bot); void changeOpenedSubsection( FnMut change, bool fromRight, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index a19200d8a..96529c241 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "dialogs/ui/chat_search_empty.h" #include "history/history.h" +#include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" @@ -56,6 +57,8 @@ namespace { constexpr auto kCollapsedChannelsCount = 5; constexpr auto kProbablyMaxChannels = 1000; constexpr auto kProbablyMaxRecommendations = 100; +constexpr auto kCollapsedAppsCount = 5; +constexpr auto kProbablyMaxApps = 100; class RecentRow final : public PeerListRow { public: @@ -87,66 +90,6 @@ private: }; -class ControllerWithPreviews - : public PeerListController - , public base::has_weak_ptr { -public: - explicit ControllerWithPreviews( - not_null window); - - [[nodiscard]] not_null window() const { - return _window; - } - - bool rowTrackPress(not_null row) override; - void rowTrackPressCancel() override; - bool rowTrackPressSkipMouseSelection() override; - - bool processTouchEvent(not_null e); - void setupTouchChatPreview(not_null scroll); - -private: - const not_null _window; - - std::optional _chatPreviewTouchGlobal; - rpl::event_stream<> _touchCancelRequests; - -}; - -class RecentsController final : public ControllerWithPreviews { -public: - RecentsController( - not_null window, - RecentPeersList list); - - [[nodiscard]] rpl::producer count() const { - return _count.value(); - } - [[nodiscard]] rpl::producer> chosen() const { - return _chosen.events(); - } - - void prepare() override; - void rowClicked(not_null row) override; - base::unique_qptr rowContextMenu( - QWidget *parent, - not_null row) override; - Main::Session &session() const override; - - QString savedMessagesChatStatus() const override; - -private: - void setupDivider(); - void subscribeToEvents(); - [[nodiscard]] Fn removeAllCallback(); - - RecentPeersList _recent; - rpl::variable _count; - rpl::event_stream> _chosen; - rpl::lifetime _lifetime; - -}; - class ChannelRow final : public PeerListRow { public: using PeerListRow::PeerListRow; @@ -161,73 +104,6 @@ private: }; -class MyChannelsController final : public ControllerWithPreviews { -public: - explicit MyChannelsController( - not_null window); - - [[nodiscard]] rpl::producer count() const { - return _count.value(); - } - [[nodiscard]] rpl::producer> chosen() const { - return _chosen.events(); - } - - void prepare() override; - void rowClicked(not_null row) override; - base::unique_qptr rowContextMenu( - QWidget *parent, - not_null row) override; - Main::Session &session() const override; - -private: - void setupDivider(); - void appendRow(not_null channel); - void fill(bool force = false); - - std::vector> _channels; - rpl::variable _toggleExpanded = nullptr; - rpl::variable _count = 0; - rpl::variable _expanded = false; - rpl::event_stream> _chosen; - rpl::lifetime _lifetime; - -}; - -class RecommendationsController final : public ControllerWithPreviews { -public: - explicit RecommendationsController( - not_null window); - - [[nodiscard]] rpl::producer count() const { - return _count.value(); - } - [[nodiscard]] rpl::producer> chosen() const { - return _chosen.events(); - } - - void prepare() override; - void rowClicked(not_null row) override; - base::unique_qptr rowContextMenu( - QWidget *parent, - not_null row) override; - Main::Session &session() const override; - - void load(); - -private: - void fill(); - void setupDivider(); - void appendRow(not_null channel); - - rpl::variable _count; - History *_activeHistory = nullptr; - bool _requested = false; - rpl::event_stream> _chosen; - rpl::lifetime _lifetime; - -}; - struct EntryMenuDescriptor { not_null controller; not_null peer; @@ -290,16 +166,17 @@ void FillEntryMenu( .icon = &st::menuIconDeleteAttention, .isAttention = true, }); - - add({ - .text = descriptor.removeAllText, - .handler = RemoveAllConfirm( - descriptor.controller, - descriptor.removeAllConfirm, - descriptor.removeAll), - .icon = &st::menuIconCancelAttention, - .isAttention = true, - }); + if (!descriptor.removeAllText.isEmpty()) { + add({ + .text = descriptor.removeAllText, + .handler = RemoveAllConfirm( + descriptor.controller, + descriptor.removeAllConfirm, + descriptor.removeAll), + .icon = &st::menuIconCancelAttention, + .isAttention = true, + }); + } } RecentRow::RecentRow(not_null peer) @@ -422,12 +299,180 @@ const style::PeerListItem &ChannelRow::computeSt( return _active ? st::recentPeersItemActive : st::recentPeersItem; } -ControllerWithPreviews::ControllerWithPreviews( +} // namespace + + +class Suggestions::ObjectListController + : public PeerListController + , public base::has_weak_ptr { +public: + explicit ObjectListController( + not_null window); + + [[nodiscard]] not_null window() const { + return _window; + } + [[nodiscard]] rpl::producer count() const { + return _count.value(); + } + [[nodiscard]] rpl::producer> chosen() const { + return _chosen.events(); + } + + Main::Session &session() const override { + return _window->session(); + } + + void rowClicked(not_null row) override; + bool rowTrackPress(not_null row) override; + void rowTrackPressCancel() override; + bool rowTrackPressSkipMouseSelection() override; + + bool processTouchEvent(not_null e); + void setupTouchChatPreview(not_null scroll); + +protected: + [[nodiscard]] int countCurrent() const; + void setCount(int count); + + [[nodiscard]] bool expandedCurrent() const; + [[nodiscard]] rpl::producer expanded() const; + + void setupPlainDivider(rpl::producer title); + void setupExpandDivider(rpl::producer title); + +private: + const not_null _window; + + std::optional _chatPreviewTouchGlobal; + rpl::event_stream<> _touchCancelRequests; + rpl::event_stream> _chosen; + rpl::variable _count; + rpl::variable _toggleExpanded = nullptr; + rpl::variable _expanded = false; + +}; + +class RecentsController final : public Suggestions::ObjectListController { +public: + RecentsController( + not_null window, + RecentPeersList list); + + void prepare() override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + QString savedMessagesChatStatus() const override; + +private: + void setupDivider(); + void subscribeToEvents(); + [[nodiscard]] Fn removeAllCallback(); + + RecentPeersList _recent; + rpl::lifetime _lifetime; + +}; + +class MyChannelsController final + : public Suggestions::ObjectListController { +public: + explicit MyChannelsController( + not_null window); + + void prepare() override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + +private: + void appendRow(not_null channel); + void fill(bool force = false); + + std::vector> _channels; + rpl::lifetime _lifetime; + +}; + +class RecommendationsController final + : public Suggestions::ObjectListController { +public: + explicit RecommendationsController( + not_null window); + + void prepare() override; + + void load(); + +private: + void fill(); + void appendRow(not_null channel); + + History *_activeHistory = nullptr; + bool _requested = false; + rpl::lifetime _lifetime; + +}; + +class RecentAppsController final + : public Suggestions::ObjectListController { +public: + explicit RecentAppsController( + not_null window); + + void prepare() override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + void load(); + + [[nodiscard]] rpl::producer<> refreshed() const; + [[nodiscard]] bool shown(not_null peer) const; + +private: + void appendRow(not_null bot); + void fill(); + + std::vector> _bots; + rpl::event_stream<> _refreshed; + rpl::lifetime _lifetime; + +}; + +class PopularAppsController final + : public Suggestions::ObjectListController { +public: + explicit PopularAppsController( + not_null window, + Fn)> filterOut, + rpl::producer<> filterOutRefreshes); + + void prepare() override; + + void load(); + +private: + void fill(); + void appendRow(not_null bot); + + Fn)> _filterOut; + rpl::producer<> _filterOutRefreshes; + History *_activeHistory = nullptr; + bool _requested = false; + rpl::lifetime _lifetime; + +}; + +Suggestions::ObjectListController::ObjectListController( not_null window) : _window(window) { } -bool ControllerWithPreviews::rowTrackPress(not_null row) { +bool Suggestions::ObjectListController::rowTrackPress( + not_null row) { const auto peer = row->peer(); const auto history = peer->owner().history(peer); const auto callback = crl::guard(this, [=](bool shown) { @@ -454,16 +499,17 @@ bool ControllerWithPreviews::rowTrackPress(not_null row) { return false; } -void ControllerWithPreviews::rowTrackPressCancel() { +void Suggestions::ObjectListController::rowTrackPressCancel() { _chatPreviewTouchGlobal = {}; _window->cancelScheduledPreview(); } -bool ControllerWithPreviews::rowTrackPressSkipMouseSelection() { +bool Suggestions::ObjectListController::rowTrackPressSkipMouseSelection() { return _chatPreviewTouchGlobal.has_value(); } -bool ControllerWithPreviews::processTouchEvent(not_null e) { +bool Suggestions::ObjectListController::processTouchEvent( + not_null e) { const auto point = e->touchPoints().empty() ? std::optional() : e->touchPoints().front().screenPos().toPoint(); @@ -500,7 +546,7 @@ bool ControllerWithPreviews::processTouchEvent(not_null e) { return false; } -void ControllerWithPreviews::setupTouchChatPreview( +void Suggestions::ObjectListController::setupTouchChatPreview( not_null scroll) { _touchCancelRequests.events() | rpl::start_with_next([=] { QTouchEvent ev(QEvent::TouchCancel); @@ -509,10 +555,122 @@ void ControllerWithPreviews::setupTouchChatPreview( }, lifetime()); } +int Suggestions::ObjectListController::countCurrent() const { + return _count.current(); +} + +void Suggestions::ObjectListController::setCount(int count) { + _count = count; +} + +bool Suggestions::ObjectListController::expandedCurrent() const { + return _expanded.current(); +} + +rpl::producer Suggestions::ObjectListController::expanded() const { + return _expanded.value(); +} + +void Suggestions::ObjectListController::rowClicked( + not_null row) { + _chosen.fire(row->peer()); +} + +void Suggestions::ObjectListController::setupPlainDivider( + rpl::producer title) { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + std::move(title), + st::searchedBarLabel); + raw->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); +} + +void Suggestions::ObjectListController::setupExpandDivider( + rpl::producer title) { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + std::move(title), + st::searchedBarLabel); + count( + ) | rpl::map( + rpl::mappers::_1 > kCollapsedChannelsCount + ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) { + _expanded = false; + if (!more) { + const auto toggle = _toggleExpanded.current(); + _toggleExpanded = nullptr; + delete toggle; + return; + } else if (_toggleExpanded.current()) { + return; + } + const auto toggle = Ui::CreateChild( + raw, + tr::lng_channels_your_more(tr::now), + st::searchedBarLink); + toggle->show(); + toggle->setClickedCallback([=] { + const auto expand = !_expanded.current(); + toggle->setText(expand + ? tr::lng_channels_your_less(tr::now) + : tr::lng_channels_your_more(tr::now)); + _expanded = expand; + }); + rpl::combine( + raw->sizeValue(), + toggle->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + toggle->moveToRight(0, 0, size.width()); + label->resizeToWidth(size.width() - x - width); + label->moveToLeft(x, y, size.width()); + }, toggle->lifetime()); + _toggleExpanded = toggle; + }, raw->lifetime()); + + rpl::combine( + raw->sizeValue(), + _toggleExpanded.value() + ) | rpl::filter( + rpl::mappers::_2 == nullptr + ) | rpl::start_with_next([=](QSize size, const auto) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); +} + RecentsController::RecentsController( not_null window, RecentPeersList list) -: ControllerWithPreviews(window) +: ObjectListController(window) , _recent(std::move(list)) { } @@ -523,21 +681,17 @@ void RecentsController::prepare() { delegate()->peerListAppendRow(std::make_unique(peer)); } delegate()->peerListRefreshRows(); - _count = _recent.list.size(); + setCount(_recent.list.size()); subscribeToEvents(); } -void RecentsController::rowClicked(not_null row) { - _chosen.fire(row->peer()); -} - Fn RecentsController::removeAllCallback() { const auto weak = base::make_weak(this); const auto session = &this->session(); return crl::guard(session, [=] { if (weak) { - _count = 0; + setCount(0); while (delegate()->peerListFullRowsCount() > 0) { delegate()->peerListRemoveRow(delegate()->peerListRowAt(0)); } @@ -560,7 +714,7 @@ base::unique_qptr RecentsController::rowContextMenu( if (weak) { const auto rowId = peer->id.value; if (const auto row = delegate()->peerListFindRow(rowId)) { - _count = std::max(0, _count.current() - 1); + setCount(std::max(0, countCurrent() - 1)); delegate()->peerListRemoveRow(row); delegate()->peerListRefreshRows(); } @@ -579,10 +733,6 @@ base::unique_qptr RecentsController::rowContextMenu( return result; } -Main::Session &RecentsController::session() const { - return window()->session(); -} - QString RecentsController::savedMessagesChatStatus() const { return tr::lng_saved_forward_here(tr::now); } @@ -649,7 +799,7 @@ void RecentsController::subscribeToEvents() { session().data().unreadBadgeChanges( ) | rpl::start_with_next([=] { - for (auto i = 0; i != _count.current(); ++i) { + for (auto i = 0; i != countCurrent(); ++i) { const auto row = delegate()->peerListRowAt(i); if (static_cast(row.get())->refreshBadge()) { delegate()->peerListUpdateRow(row); @@ -660,11 +810,11 @@ void RecentsController::subscribeToEvents() { MyChannelsController::MyChannelsController( not_null window) -: ControllerWithPreviews(window) { +: ObjectListController(window) { } void MyChannelsController::prepare() { - setupDivider(); + setupExpandDivider(tr::lng_channels_your_title()); session().changes().peerUpdates( Data::PeerUpdate::Flag::ChannelAmIn @@ -683,7 +833,7 @@ void MyChannelsController::prepare() { if (row) { delegate()->peerListRemoveRow(row); } - _count = int(_channels.size()); + setCount(_channels.size()); fill(true); }, _lifetime); @@ -704,9 +854,9 @@ void MyChannelsController::prepare() { } ranges::sort(_channels, ranges::greater(), &History::chatListTimeId); - _count = int(_channels.size()); + setCount(_channels.size()); - _expanded.value() | rpl::start_with_next([=] { + expanded() | rpl::start_with_next([=] { fill(); }, _lifetime); @@ -728,18 +878,18 @@ void MyChannelsController::prepare() { } } } - const auto was = _count.current(); + const auto was = countCurrent(); const auto now = int(_channels.size()); if (was != now) { - _count = now; + setCount(now); fill(); } }, _lifetime); } void MyChannelsController::fill(bool force) { - const auto count = _count.current(); - const auto limit = _expanded.current() + const auto count = countCurrent(); + const auto limit = expandedCurrent() ? count : std::min(count, kCollapsedChannelsCount); const auto already = delegate()->peerListFullRowsCount(); @@ -771,10 +921,6 @@ void MyChannelsController::appendRow(not_null channel) { delegate()->peerListAppendRow(std::move(row)); } -void MyChannelsController::rowClicked(not_null row) { - _chosen.fire(row->peer()); -} - base::unique_qptr MyChannelsController::rowContextMenu( QWidget *parent, not_null row) { @@ -793,88 +939,18 @@ base::unique_qptr MyChannelsController::rowContextMenu( return result; } -Main::Session &MyChannelsController::session() const { - return window()->session(); -} - -void MyChannelsController::setupDivider() { - auto result = object_ptr( - (QWidget*)nullptr, - st::searchedBarHeight); - const auto raw = result.data(); - const auto label = Ui::CreateChild( - raw, - tr::lng_channels_your_title(), - st::searchedBarLabel); - _count.value( - ) | rpl::map( - rpl::mappers::_1 > kCollapsedChannelsCount - ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) { - _expanded = false; - if (!more) { - const auto toggle = _toggleExpanded.current(); - _toggleExpanded = nullptr; - delete toggle; - return; - } else if (_toggleExpanded.current()) { - return; - } - const auto toggle = Ui::CreateChild( - raw, - tr::lng_channels_your_more(tr::now), - st::searchedBarLink); - toggle->show(); - toggle->setClickedCallback([=] { - const auto expand = !_expanded.current(); - toggle->setText(expand - ? tr::lng_channels_your_less(tr::now) - : tr::lng_channels_your_more(tr::now)); - _expanded = expand; - }); - rpl::combine( - raw->sizeValue(), - toggle->widthValue() - ) | rpl::start_with_next([=](QSize size, int width) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - toggle->moveToRight(0, 0, size.width()); - label->resizeToWidth(size.width() - x - width); - label->moveToLeft(x, y, size.width()); - }, toggle->lifetime()); - _toggleExpanded = toggle; - }, raw->lifetime()); - - rpl::combine( - raw->sizeValue(), - _toggleExpanded.value() - ) | rpl::filter( - rpl::mappers::_2 == nullptr - ) | rpl::start_with_next([=](QSize size, const auto) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - label->resizeToWidth(size.width() - x * 2); - label->moveToLeft(x, y, size.width()); - }, raw->lifetime()); - - raw->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(raw).fillRect(clip, st::searchedBarBg); - }, raw->lifetime()); - - delegate()->peerListSetAboveWidget(std::move(result)); -} - RecommendationsController::RecommendationsController( not_null window) -: ControllerWithPreviews(window) { +: ObjectListController(window) { } void RecommendationsController::prepare() { - setupDivider(); + setupPlainDivider(tr::lng_channels_recommended()); fill(); } void RecommendationsController::load() { - if (_requested || _count.current()) { + if (_requested || countCurrent()) { return; } _requested = true; @@ -898,7 +974,7 @@ void RecommendationsController::fill() { } } delegate()->peerListRefreshRows(); - _count = delegate()->peerListFullRowsCount(); + setCount(delegate()->peerListFullRowsCount()); window()->activeChatValue() | rpl::start_with_next([=](const Key &key) { const auto history = key.history(); @@ -935,44 +1011,163 @@ void RecommendationsController::appendRow(not_null channel) { delegate()->peerListAppendRow(std::move(row)); } -void RecommendationsController::rowClicked(not_null row) { - _chosen.fire(row->peer()); +RecentAppsController::RecentAppsController( + not_null window) +: ObjectListController(window) { } -base::unique_qptr RecommendationsController::rowContextMenu( +void RecentAppsController::prepare() { + setupExpandDivider(tr::lng_bot_apps_your()); + + _bots.reserve(kProbablyMaxApps); + rpl::single() | rpl::then( + session().topBotApps().updates() + ) | rpl::start_with_next([=] { + _bots.clear(); + for (const auto &peer : session().topBotApps().list()) { + if (const auto bot = peer->asUser()) { + if (bot->isBot() && !bot->isInaccessible()) { + _bots.push_back(bot); + } + } + } + setCount(_bots.size()); + while (delegate()->peerListFullRowsCount()) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(0)); + } + fill(); + }, _lifetime); + + expanded() | rpl::skip(1) | rpl::start_with_next([=] { + fill(); + }, _lifetime); +} + +base::unique_qptr RecentAppsController::rowContextMenu( QWidget *parent, not_null row) { - return nullptr; + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + const auto peer = row->peer(); + const auto weak = base::make_weak(this); + const auto session = &this->session(); + const auto removeOne = crl::guard(session, [=] { + if (weak) { + const auto rowId = peer->id.value; + if (const auto row = delegate()->peerListFindRow(rowId)) { + setCount(std::max(0, countCurrent() - 1)); + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + } + session->topBotApps().remove(peer); + }); + FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), { + .controller = window(), + .peer = peer, + .removeOneText = tr::lng_recent_remove(tr::now), + .removeOne = removeOne, + }); + return result; } -Main::Session &RecommendationsController::session() const { - return window()->session(); +void RecentAppsController::load() { + session().topBotApps().reload(); } -void RecommendationsController::setupDivider() { - auto result = object_ptr( - (QWidget*)nullptr, - st::searchedBarHeight); - const auto raw = result.data(); - const auto label = Ui::CreateChild( - raw, - tr::lng_channels_recommended(), - st::searchedBarLabel); - raw->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - label->resizeToWidth(size.width() - x * 2); - label->moveToLeft(x, y, size.width()); - }, raw->lifetime()); - raw->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(raw).fillRect(clip, st::searchedBarBg); - }, raw->lifetime()); - - delegate()->peerListSetAboveWidget(std::move(result)); +rpl::producer<> RecentAppsController::refreshed() const { + return _refreshed.events(); } -} // namespace +bool RecentAppsController::shown(not_null peer) const { + return delegate()->peerListFindRow(peer->id.value) != nullptr; +} + +void RecentAppsController::fill() { + const auto count = countCurrent(); + const auto limit = expandedCurrent() + ? count + : std::min(count, kCollapsedAppsCount); + const auto already = delegate()->peerListFullRowsCount(); + const auto delta = limit - already; + if (!delta) { + return; + } else if (delta > 0) { + for (auto i = already; i != limit; ++i) { + appendRow(_bots[i]); + } + } else if (delta < 0) { + for (auto i = already; i != limit;) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i)); + } + } + delegate()->peerListRefreshRows(); + + _refreshed.fire({}); +} + +void RecentAppsController::appendRow(not_null bot) { + auto row = std::make_unique(bot); + if (const auto count = bot->botInfo->activeUsers) { + row->setCustomStatus( + tr::lng_bot_status_users(tr::now, lt_count_decimal, count)); + } + delegate()->peerListAppendRow(std::move(row)); +} + +PopularAppsController::PopularAppsController( + not_null window, + Fn)> filterOut, + rpl::producer<> filterOutRefreshes) +: ObjectListController(window) +, _filterOut(std::move(filterOut)) +, _filterOutRefreshes(std::move(filterOutRefreshes)) { +} + +void PopularAppsController::prepare() { + setupPlainDivider(tr::lng_bot_apps_popular()); + rpl::single() | rpl::then( + std::move(_filterOutRefreshes) + ) | rpl::start_with_next([=] { + fill(); + }, _lifetime); +} + +void PopularAppsController::load() { + if (_requested || countCurrent()) { + return; + } + _requested = true; + const auto attachWebView = &session().attachWebView(); + attachWebView->loadPopularAppBots(); + attachWebView->popularAppBotsLoaded( + ) | rpl::take(1) | rpl::start_with_next([=] { + fill(); + }, _lifetime); +} + +void PopularAppsController::fill() { + while (delegate()->peerListFullRowsCount()) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(0)); + } + for (const auto &bot : session().attachWebView().popularAppBots()) { + if (!_filterOut || !_filterOut(bot)) { + appendRow(bot); + } + } + delegate()->peerListRefreshRows(); + setCount(delegate()->peerListFullRowsCount()); +} + +void PopularAppsController::appendRow(not_null bot) { + auto row = std::make_unique(bot); + //if (const auto count = bot->botInfo->activeUsers) { + // row->setCustomStatus( + // tr::lng_bot_status_users(tr::now, lt_count_decimal, count)); + //} + delegate()->peerListAppendRow(std::move(row)); +} Suggestions::Suggestions( not_null parent, @@ -990,18 +1185,24 @@ Suggestions::Suggestions( this, object_ptr(this, std::move(topPeers))))) , _topPeers(_topPeersWrap->entity()) -, _recentPeers(_chatsContent->add(setupRecentPeers(std::move(recentPeers)))) +, _recent(setupRecentPeers(std::move(recentPeers))) , _emptyRecent(_chatsContent->add(setupEmptyRecent())) , _channelsScroll(std::make_unique(this)) , _channelsContent( _channelsScroll->setOwnedWidget(object_ptr(this))) -, _myChannels(_channelsContent->add(setupMyChannels())) -, _recommendations(_channelsContent->add(setupRecommendations())) -, _emptyChannels(_channelsContent->add(setupEmptyChannels())) { +, _myChannels(setupMyChannels()) +, _recommendations(setupRecommendations()) +, _emptyChannels(_channelsContent->add(setupEmptyChannels())) +, _appsScroll(std::make_unique(this)) +, _appsContent( + _appsScroll->setOwnedWidget(object_ptr(this))) +, _recentApps(setupRecentApps()) +, _popularApps(setupPopularApps()) { setupTabs(); setupChats(); setupChannels(); + setupApps(); } Suggestions::~Suggestions() = default; @@ -1024,18 +1225,23 @@ void Suggestions::setupTabs() { _tabs->setSections({ tr::lng_recent_chats(tr::now), tr::lng_recent_channels(tr::now), + tr::lng_recent_apps(tr::now), }); _tabs->sectionActivated( ) | rpl::start_with_next([=](int section) { - switchTab(section ? Tab::Channels : Tab::Chats); + switchTab(section == 2 + ? Tab::Apps + : section + ? Tab::Channels + : Tab::Chats); }, _tabs->lifetime()); } void Suggestions::setupChats() { - _recentCount.value() | rpl::start_with_next([=](int count) { - _recentPeers->toggle(count > 0, anim::type::instant); + _recent->count.value() | rpl::start_with_next([=](int count) { + _recent->wrap->toggle(count > 0, anim::type::instant); _emptyRecent->toggle(count == 0, anim::type::instant); - }, _recentPeers->lifetime()); + }, _recent->wrap->lifetime()); _topPeers->emptyValue() | rpl::start_with_next([=](bool empty) { _topPeersWrap->toggle(!empty, anim::type::instant); @@ -1097,7 +1303,7 @@ void Suggestions::setupChats() { }, _topPeers->lifetime()); _chatsScroll->setVisible(_tab.current() == Tab::Chats); - _chatsScroll->setCustomTouchProcess(_recentProcessTouch); + _chatsScroll->setCustomTouchProcess(_recent->processTouch); } void Suggestions::handlePressForChatPreview( @@ -1115,59 +1321,78 @@ void Suggestions::handlePressForChatPreview( } void Suggestions::setupChannels() { - _myChannelsCount.value() | rpl::start_with_next([=](int count) { - _myChannels->toggle(count > 0, anim::type::instant); - }, _myChannels->lifetime()); + _myChannels->count.value() | rpl::start_with_next([=](int count) { + _myChannels->wrap->toggle(count > 0, anim::type::instant); + }, _myChannels->wrap->lifetime()); - _recommendationsCount.value() | rpl::start_with_next([=](int count) { - _recommendations->toggle(count > 0, anim::type::instant); - }, _recommendations->lifetime()); + _recommendations->count.value() | rpl::start_with_next([=](int count) { + _recommendations->wrap->toggle(count > 0, anim::type::instant); + }, _recommendations->wrap->lifetime()); _emptyChannels->toggleOn( rpl::combine( - _myChannelsCount.value(), - _recommendationsCount.value(), + _myChannels->count.value(), + _recommendations->count.value(), rpl::mappers::_1 + rpl::mappers::_2 == 0), anim::type::instant); _channelsScroll->setVisible(_tab.current() == Tab::Channels); _channelsScroll->setCustomTouchProcess([=](not_null e) { - const auto myChannels = _myChannelsProcessTouch(e); - const auto recommendations = _recommendationsProcessTouch(e); + const auto myChannels = _myChannels->processTouch(e); + const auto recommendations = _recommendations->processTouch(e); return myChannels || recommendations; }); } +void Suggestions::setupApps() { + _recentApps->count.value() | rpl::start_with_next([=](int count) { + _recentApps->wrap->toggle(count > 0, anim::type::instant); + }, _recentApps->wrap->lifetime()); + + _popularApps->count.value() | rpl::start_with_next([=](int count) { + _popularApps->wrap->toggle(count > 0, anim::type::instant); + }, _popularApps->wrap->lifetime()); + + _appsScroll->setVisible(_tab.current() == Tab::Apps); + _appsScroll->setCustomTouchProcess([=](not_null e) { + const auto recentApps = _recentApps->processTouch(e); + const auto popularApps = _popularApps->processTouch(e); + return recentApps || popularApps; + }); +} + void Suggestions::selectJump(Qt::Key direction, int pageSize) { - if (_tab.current() == Tab::Chats) { - selectJumpChats(direction, pageSize); - } else { - selectJumpChannels(direction, pageSize); + switch (_tab.current()) { + case Tab::Chats: selectJumpChats(direction, pageSize); return; + case Tab::Channels: selectJumpChannels(direction, pageSize); return; + case Tab::Apps: selectJumpApps(direction, pageSize); return; } + Unexpected("Tab in Suggestions::selectJump."); } void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) { const auto recentHasSelection = [=] { - return _recentSelectJump({}, 0) == JumpResult::Applied; + return _recent->selectJump({}, 0) == JumpResult::Applied; }; if (pageSize) { if (direction == Qt::Key_Down || direction == Qt::Key_Up) { _topPeers->deselectByKeyboard(); if (!recentHasSelection()) { if (direction == Qt::Key_Down) { - _recentSelectJump(direction, 0); + _recent->selectJump(direction, 0); } else { return; } } - if (_recentSelectJump(direction, pageSize) == JumpResult::AppliedAndOut) { + if (_recent->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { if (direction == Qt::Key_Up) { _chatsScroll->scrollTo(0); } } } } else if (direction == Qt::Key_Up) { - if (_recentSelectJump(direction, pageSize) + if (_recent->selectJump(direction, pageSize) == JumpResult::AppliedAndOut) { _topPeers->selectByKeyboard(direction); } else if (_topPeers->selectedByKeyboard()) { @@ -1175,12 +1400,12 @@ void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) { } } else if (direction == Qt::Key_Down) { if (!_topPeersWrap->toggled() || recentHasSelection()) { - _recentSelectJump(direction, pageSize); + _recent->selectJump(direction, pageSize); } else if (_topPeers->selectedByKeyboard()) { if (!_topPeers->selectByKeyboard(direction) - && _recentCount.current() > 0) { + && _recent->count.current() > 0) { _topPeers->deselectByKeyboard(); - _recentSelectJump(direction, pageSize); + _recent->selectJump(direction, pageSize); } } else { _topPeers->selectByKeyboard({}); @@ -1195,70 +1420,147 @@ void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) { void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) { const auto myChannelsHasSelection = [=] { - return _myChannelsSelectJump({}, 0) == JumpResult::Applied; + return _myChannels->selectJump({}, 0) == JumpResult::Applied; }; const auto recommendationsHasSelection = [=] { - return _recommendationsSelectJump({}, 0) == JumpResult::Applied; + return _recommendations->selectJump({}, 0) == JumpResult::Applied; }; if (pageSize) { if (direction == Qt::Key_Down) { if (recommendationsHasSelection()) { - _recommendationsSelectJump(direction, pageSize); + _recommendations->selectJump(direction, pageSize); } else if (myChannelsHasSelection()) { - if (_myChannelsSelectJump(direction, pageSize) + if (_myChannels->selectJump(direction, pageSize) == JumpResult::AppliedAndOut) { - _recommendationsSelectJump(direction, 0); + _recommendations->selectJump(direction, 0); } - } else if (_myChannelsCount.current()) { - _myChannelsSelectJump(direction, 0); - _myChannelsSelectJump(direction, pageSize); - } else if (_recommendationsCount.current()) { - _recommendationsSelectJump(direction, 0); - _recommendationsSelectJump(direction, pageSize); + } else if (_myChannels->count.current()) { + _myChannels->selectJump(direction, 0); + _myChannels->selectJump(direction, pageSize); + } else if (_recommendations->count.current()) { + _recommendations->selectJump(direction, 0); + _recommendations->selectJump(direction, pageSize); } } else if (direction == Qt::Key_Up) { if (myChannelsHasSelection()) { - if (_myChannelsSelectJump(direction, pageSize) + if (_myChannels->selectJump(direction, pageSize) == JumpResult::AppliedAndOut) { _channelsScroll->scrollTo(0); } } else if (recommendationsHasSelection()) { - if (_recommendationsSelectJump(direction, pageSize) + if (_recommendations->selectJump(direction, pageSize) == JumpResult::AppliedAndOut) { - _myChannelsSelectJump(direction, -1); + _myChannels->selectJump(direction, -1); } } } } else if (direction == Qt::Key_Up) { if (myChannelsHasSelection()) { - _myChannelsSelectJump(direction, 0); - } else if (_recommendationsSelectJump(direction, 0) + _myChannels->selectJump(direction, 0); + } else if (_recommendations->selectJump(direction, 0) == JumpResult::AppliedAndOut) { - _myChannelsSelectJump(direction, -1); + _myChannels->selectJump(direction, -1); } else if (!recommendationsHasSelection()) { - if (_myChannelsSelectJump(direction, 0) + if (_myChannels->selectJump(direction, 0) == JumpResult::AppliedAndOut) { _channelsScroll->scrollTo(0); } } } else if (direction == Qt::Key_Down) { if (recommendationsHasSelection()) { - _recommendationsSelectJump(direction, 0); - } else if (_myChannelsSelectJump(direction, 0) + _recommendations->selectJump(direction, 0); + } else if (_myChannels->selectJump(direction, 0) == JumpResult::AppliedAndOut) { - _recommendationsSelectJump(direction, 0); + _recommendations->selectJump(direction, 0); } else if (!myChannelsHasSelection()) { - if (_recommendationsSelectJump(direction, 0) + if (_recommendations->selectJump(direction, 0) == JumpResult::AppliedAndOut) { - _myChannelsSelectJump(direction, 0); + _myChannels->selectJump(direction, 0); + } + } + } +} + +void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) { + const auto recentAppsHasSelection = [=] { + return _recentApps->selectJump({}, 0) == JumpResult::Applied; + }; + const auto popularAppsHasSelection = [=] { + return _popularApps->selectJump({}, 0) == JumpResult::Applied; + }; + if (pageSize) { + if (direction == Qt::Key_Down) { + if (popularAppsHasSelection()) { + _popularApps->selectJump(direction, pageSize); + } else if (recentAppsHasSelection()) { + if (_recentApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _popularApps->selectJump(direction, 0); + } + } else if (_recentApps->count.current()) { + _recentApps->selectJump(direction, 0); + _recentApps->selectJump(direction, pageSize); + } else if (_popularApps->count.current()) { + _popularApps->selectJump(direction, 0); + _popularApps->selectJump(direction, pageSize); + } + } else if (direction == Qt::Key_Up) { + if (recentAppsHasSelection()) { + if (_recentApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _channelsScroll->scrollTo(0); + } + } else if (popularAppsHasSelection()) { + if (_popularApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, -1); + } + } + } + } else if (direction == Qt::Key_Up) { + if (recentAppsHasSelection()) { + _recentApps->selectJump(direction, 0); + } else if (_popularApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, -1); + } else if (!popularAppsHasSelection()) { + if (_recentApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _channelsScroll->scrollTo(0); + } + } + } else if (direction == Qt::Key_Down) { + if (popularAppsHasSelection()) { + _popularApps->selectJump(direction, 0); + } else if (_recentApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _popularApps->selectJump(direction, 0); + } else if (!recentAppsHasSelection()) { + if (_popularApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, 0); } } } } void Suggestions::chooseRow() { - if (!_topPeers->chooseRow()) { - _recentPeersChoose(); + switch (_tab.current()) { + case Tab::Chats: + if (!_topPeers->chooseRow()) { + _recent->choose(); + } + break; + case Tab::Channels: + if (!_myChannels->choose()) { + _recommendations->choose(); + } + break; + case Tab::Apps: + if (!_recentApps->choose()) { + _popularApps->choose(); + } + break; } } @@ -1272,14 +1574,21 @@ Data::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) { if (const auto top = _topPeers->updateFromParentDrag(globalPosition)) { return _controller->session().data().history(PeerId(top)); } - return fromListId(_recentUpdateFromParentDrag(globalPosition)); + return fromListId(_recent->updateFromParentDrag(globalPosition)); } Data::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) { - if (const auto id = _myChannelsUpdateFromParentDrag(globalPosition)) { + if (const auto id = _myChannels->updateFromParentDrag(globalPosition)) { return fromListId(id); } - return fromListId(_recommendationsUpdateFromParentDrag(globalPosition)); + return fromListId(_recommendations->updateFromParentDrag(globalPosition)); +} + +Data::Thread *Suggestions::updateFromAppsDrag(QPoint globalPosition) { + if (const auto id = _recentApps->updateFromParentDrag(globalPosition)) { + return fromListId(id); + } + return fromListId(_popularApps->updateFromParentDrag(globalPosition)); } Data::Thread *Suggestions::fromListId(uint64 peerListRowId) { @@ -1290,9 +1599,11 @@ Data::Thread *Suggestions::fromListId(uint64 peerListRowId) { void Suggestions::dragLeft() { _topPeers->dragLeft(); - _recentDragLeft(); - _myChannelsDragLeft(); - _recommendationsDragLeft(); + _recent->dragLeft(); + _myChannels->dragLeft(); + _recommendations->dragLeft(); + _recentApps->dragLeft(); + _popularApps->dragLeft(); } void Suggestions::show(anim::type animated, Fn finish) { @@ -1318,7 +1629,8 @@ void Suggestions::hide(anim::type animated, Fn finish) { } void Suggestions::switchTab(Tab tab) { - if (_tab.current() == tab) { + const auto was = _tab.current(); + if (was == tab) { return; } _tab = tab; @@ -1326,19 +1638,29 @@ void Suggestions::switchTab(Tab tab) { if (_tabs->isHidden()) { return; } - startSlideAnimation(); + startSlideAnimation(was, tab); } -void Suggestions::startSlideAnimation() { +void Suggestions::startSlideAnimation(Tab was, Tab now) { if (!_slideAnimation.animating()) { - _slideLeft = Ui::GrabWidget(_chatsScroll.get()); - _slideRight = Ui::GrabWidget(_channelsScroll.get()); + _slideLeft = (was == Tab::Chats || now == Tab::Chats) + ? Ui::GrabWidget(_chatsScroll.get()) + : Ui::GrabWidget(_channelsScroll.get()); + _slideLeftTop = (was == Tab::Chats || now == Tab::Chats) + ? _chatsScroll->y() + : _channelsScroll->y(); + _slideRight = (was == Tab::Apps || now == Tab::Apps) + ? Ui::GrabWidget(_appsScroll.get()) + : Ui::GrabWidget(_channelsScroll.get()); + _slideRightTop = (was == Tab::Apps || now == Tab::Apps) + ? _appsScroll->y() + : _channelsScroll->y(); _chatsScroll->hide(); _channelsScroll->hide(); + _appsScroll->hide(); } - const auto channels = (_tab.current() == Tab::Channels); - const auto from = channels ? 0. : 1.; - const auto to = channels ? 1. : 0.; + const auto from = (now > was) ? 0. : 1.; + const auto to = (now > was) ? 1. : 0.; _slideAnimation.start([=] { update(); if (!_slideAnimation.animating() && !_shownAnimation.animating()) { @@ -1372,20 +1694,23 @@ void Suggestions::startShownAnimation(bool shown, Fn finish) { _tabs->hide(); _chatsScroll->hide(); _channelsScroll->hide(); + _appsScroll->hide(); _slideAnimation.stop(); } void Suggestions::finishShow() { _slideAnimation.stop(); _slideLeft = _slideRight = QPixmap(); + _slideLeftTop = _slideRightTop = 0; _shownAnimation.stop(); _cache = QPixmap(); _tabs->show(); - const auto channels = (_tab.current() == Tab::Channels); - _chatsScroll->setVisible(!channels); - _channelsScroll->setVisible(channels); + const auto tab = _tab.current(); + _chatsScroll->setVisible(tab == Tab::Chats); + _channelsScroll->setVisible(tab == Tab::Channels); + _appsScroll->setVisible(tab == Tab::Apps); } float64 Suggestions::shownOpacity() const { @@ -1410,12 +1735,12 @@ void Suggestions::paintEvent(QPaintEvent *e) { p.setOpacity(1. - progress); p.drawPixmap( anim::interpolate(0, -slide, progress), - _chatsScroll->y(), + _slideLeftTop, _slideLeft); p.setOpacity(progress); p.drawPixmap( anim::interpolate(slide, 0, progress), - _channelsScroll->y(), + _slideRightTop, _slideRight); } } @@ -1430,38 +1755,30 @@ void Suggestions::resizeEvent(QResizeEvent *e) { _channelsScroll->setGeometry(0, tabs, w, height() - tabs); _channelsContent->resizeToWidth(w); + + _appsScroll->setGeometry(0, tabs, w, height() - tabs); + _appsContent->resizeToWidth(w); } -object_ptr> Suggestions::setupRecentPeers( - RecentPeersList recentPeers) { - auto &lifetime = _chatsContent->lifetime(); - const auto delegate = lifetime.make_state< - PeerListContentDelegateSimple - >(); - const auto controller = lifetime.make_state( +auto Suggestions::setupRecentPeers(RecentPeersList recentPeers) +-> std::unique_ptr { + const auto controller = lifetime().make_state( _controller, std::move(recentPeers)); - controller->setStyleOverrides(&st::recentPeersList); - _recentCount = controller->count(); - _recentProcessTouch = [=](not_null e) { - return controller->processTouchEvent(e); + const auto addToScroll = [=] { + return _topPeersWrap->toggled() ? _topPeers->height() : 0; }; + auto result = setupObjectList( + _chatsScroll.get(), + _chatsContent, + controller, + addToScroll); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); - controller->chosen( - ) | rpl::start_with_next([=](not_null peer) { - _controller->session().recentPeers().bump(peer); - _recentPeerChosen.fire_copy(peer); - }, lifetime); - - auto content = object_ptr(_chatsContent, controller); - - const auto raw = content.data(); - _recentPeersChoose = [=] { - return raw->submitted(); - }; - _recentSelectJump = [raw](Qt::Key direction, int pageSize) { - const auto had = raw->hasSelection(); + raw->selectJump = [list](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); if (direction == Qt::Key()) { return had ? JumpResult::Applied : JumpResult::NotApplied; } else if (direction == Qt::Key_Up && !had) { @@ -1469,11 +1786,11 @@ object_ptr> Suggestions::setupRecentPeers( } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { const auto delta = (direction == Qt::Key_Down) ? 1 : -1; if (pageSize > 0) { - raw->selectSkipPage(pageSize, delta); + list->selectSkipPage(pageSize, delta); } else { - raw->selectSkip(delta); + list->selectSkip(delta); } - return raw->hasSelection() + return list->hasSelection() ? JumpResult::Applied : had ? JumpResult::AppliedAndOut @@ -1481,23 +1798,13 @@ object_ptr> Suggestions::setupRecentPeers( } return JumpResult::NotApplied; }; - _recentUpdateFromParentDrag = [=](QPoint globalPosition) { - return raw->updateFromParentDrag(globalPosition); - }; - _recentDragLeft = [=] { - raw->dragLeft(); - }; - raw->scrollToRequests( - ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { - const auto add = _topPeersWrap->toggled() ? _topPeers->height() : 0; - _chatsScroll->scrollToY(request.ymin + add, request.ymax + add); - }, lifetime); - delegate->setContent(raw); - controller->setDelegate(delegate); - controller->setupTouchChatPreview(_chatsScroll.get()); + raw->chosen.events( + ) | rpl::start_with_next([=](not_null peer) { + _controller->session().recentPeers().bump(peer); + }, list->lifetime()); - return object_ptr>(this, std::move(content)); + return result; } object_ptr> Suggestions::setupEmptyRecent() { @@ -1505,6 +1812,283 @@ object_ptr> Suggestions::setupEmptyRecent() { return setupEmpty(_chatsContent, icon, tr::lng_recent_none()); } +auto Suggestions::setupMyChannels() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller); + + auto result = setupObjectList( + _channelsScroll.get(), + _channelsContent, + controller); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [=](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + if (pageSize < 0) { + list->selectLast(); + return list->hasSelection() + ? JumpResult::Applied + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto was = list->selectedIndex(); + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + if (had + && delta > 0 + && raw->count.current() + && list->selectedIndex() == was) { + list->clearSelection(); + return JumpResult::AppliedAndOut; + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = false; + }, list->lifetime()); + + return result; +} + +auto Suggestions::setupRecommendations() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller); + + const auto addToScroll = [=] { + const auto wrap = _myChannels->wrap; + return wrap->toggled() ? wrap->height() : 0; + }; + auto result = setupObjectList( + _channelsScroll.get(), + _channelsContent, + controller, + addToScroll); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [list](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = true; + }, list->lifetime()); + + _tab.value() | rpl::filter( + rpl::mappers::_1 == Tab::Channels + ) | rpl::start_with_next([=] { + controller->load(); + }, list->lifetime()); + + return result; +} + +auto Suggestions::setupRecentApps() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller); + _recentAppsShows = [=](not_null peer) { + return controller->shown(peer); + }; + _recentAppsRefreshed = controller->refreshed(); + + auto result = setupObjectList( + _appsScroll.get(), + _appsContent, + controller); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [=](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + if (pageSize < 0) { + list->selectLast(); + return list->hasSelection() + ? JumpResult::Applied + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto was = list->selectedIndex(); + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + if (had + && delta > 0 + && raw->count.current() + && list->selectedIndex() == was) { + list->clearSelection(); + return JumpResult::AppliedAndOut; + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = false; + }, list->lifetime()); + + controller->load(); + + return result; +} + +auto Suggestions::setupPopularApps() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller, + _recentAppsShows, + rpl::duplicate(_recentAppsRefreshed)); + + const auto addToScroll = [=] { + const auto wrap = _recentApps->wrap; + return wrap->toggled() ? wrap->height() : 0; + }; + auto result = setupObjectList( + _appsScroll.get(), + _appsContent, + controller, + addToScroll); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [list](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = true; + }, list->lifetime()); + + _tab.value() | rpl::filter( + rpl::mappers::_1 == Tab::Apps + ) | rpl::start_with_next([=] { + controller->load(); + }, list->lifetime()); + + return result; +} + +auto Suggestions::setupObjectList( + not_null scroll, + not_null parent, + not_null controller, + Fn addToScroll) +-> std::unique_ptr { + auto &lifetime = parent->lifetime(); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + controller->setStyleOverrides(&st::recentPeersList); + + auto content = object_ptr(parent, controller); + const auto list = content.data(); + + auto result = std::make_unique(ObjectList{ + .wrap = parent->add(object_ptr>( + parent, + std::move(content))), + }); + const auto raw = result.get(); + + raw->count = controller->count(); + raw->processTouch = [=](not_null e) { + return controller->processTouchEvent(e); + }; + + controller->chosen( + ) | rpl::start_with_next([=](not_null peer) { + raw->chosen.fire_copy(peer); + }, lifetime); + + raw->choose = [=] { + return list->submitted(); + }; + raw->updateFromParentDrag = [=](QPoint globalPosition) { + return list->updateFromParentDrag(globalPosition); + }; + raw->dragLeft = [=] { + list->dragLeft(); + }; + + list->scrollToRequests( + ) | rpl::start_with_next([=](Ui::ScrollToRequest request) { + const auto add = addToScroll ? addToScroll() : 0; + scroll->scrollToY(request.ymin + add, request.ymax + add); + }, list->lifetime()); + + delegate->setContent(list); + controller->setDelegate(delegate); + controller->setupTouchChatPreview(scroll); + + return result; +} + object_ptr> Suggestions::setupEmptyChannels() { const auto icon = SearchEmptyIcon::NoResults; return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about()); @@ -1541,157 +2125,6 @@ object_ptr> Suggestions::setupEmpty( return result; } -object_ptr> Suggestions::setupMyChannels() { - auto &lifetime = _channelsContent->lifetime(); - const auto delegate = lifetime.make_state< - PeerListContentDelegateSimple - >(); - const auto controller = lifetime.make_state( - _controller); - controller->setStyleOverrides(&st::recentPeersList); - - _myChannelsCount = controller->count(); - _myChannelsProcessTouch = [=](not_null e) { - return controller->processTouchEvent(e); - }; - - controller->chosen( - ) | rpl::start_with_next([=](not_null peer) { - _persist = false; - _myChannelChosen.fire_copy(peer); - }, lifetime); - - auto content = object_ptr(_channelsContent, controller); - - const auto raw = content.data(); - _myChannelsChoose = [=] { - return raw->submitted(); - }; - _myChannelsSelectJump = [=](Qt::Key direction, int pageSize) { - const auto had = raw->hasSelection(); - if (direction == Qt::Key()) { - return had ? JumpResult::Applied : JumpResult::NotApplied; - } else if (direction == Qt::Key_Up && !had) { - if (pageSize < 0) { - raw->selectLast(); - return raw->hasSelection() - ? JumpResult::Applied - : JumpResult::NotApplied; - } - return JumpResult::NotApplied; - } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { - const auto was = raw->selectedIndex(); - const auto delta = (direction == Qt::Key_Down) ? 1 : -1; - if (pageSize > 0) { - raw->selectSkipPage(pageSize, delta); - } else { - raw->selectSkip(delta); - } - if (had - && delta > 0 - && _recommendationsCount.current() - && raw->selectedIndex() == was) { - raw->clearSelection(); - return JumpResult::AppliedAndOut; - } - return raw->hasSelection() - ? JumpResult::Applied - : had - ? JumpResult::AppliedAndOut - : JumpResult::NotApplied; - } - return JumpResult::NotApplied; - }; - _myChannelsUpdateFromParentDrag = [=](QPoint globalPosition) { - return raw->updateFromParentDrag(globalPosition); - }; - _myChannelsDragLeft = [=] { - raw->dragLeft(); - }; - raw->scrollToRequests( - ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { - _channelsScroll->scrollToY(request.ymin, request.ymax); - }, lifetime); - - delegate->setContent(raw); - controller->setDelegate(delegate); - controller->setupTouchChatPreview(_channelsScroll.get()); - - return object_ptr>(this, std::move(content)); -} - -object_ptr> Suggestions::setupRecommendations() { - auto &lifetime = _channelsContent->lifetime(); - const auto delegate = lifetime.make_state< - PeerListContentDelegateSimple - >(); - const auto controller = lifetime.make_state( - _controller); - controller->setStyleOverrides(&st::recentPeersList); - - _recommendationsCount = controller->count(); - _recommendationsProcessTouch = [=](not_null e) { - return controller->processTouchEvent(e); - }; - - _tab.value() | rpl::filter( - rpl::mappers::_1 == Tab::Channels - ) | rpl::start_with_next([=] { - controller->load(); - }, lifetime); - - controller->chosen( - ) | rpl::start_with_next([=](not_null peer) { - _persist = true; - _recommendationChosen.fire_copy(peer); - }, lifetime); - - auto content = object_ptr(_channelsContent, controller); - - const auto raw = content.data(); - _recommendationsChoose = [=] { - return raw->submitted(); - }; - _recommendationsSelectJump = [raw](Qt::Key direction, int pageSize) { - const auto had = raw->hasSelection(); - if (direction == Qt::Key()) { - return had ? JumpResult::Applied : JumpResult::NotApplied; - } else if (direction == Qt::Key_Up && !had) { - return JumpResult::NotApplied; - } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { - const auto delta = (direction == Qt::Key_Down) ? 1 : -1; - if (pageSize > 0) { - raw->selectSkipPage(pageSize, delta); - } else { - raw->selectSkip(delta); - } - return raw->hasSelection() - ? JumpResult::Applied - : had - ? JumpResult::AppliedAndOut - : JumpResult::NotApplied; - } - return JumpResult::NotApplied; - }; - _recommendationsUpdateFromParentDrag = [=](QPoint globalPosition) { - return raw->updateFromParentDrag(globalPosition); - }; - _recommendationsDragLeft = [=] { - raw->dragLeft(); - }; - raw->scrollToRequests( - ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { - const auto add = _myChannels->toggled() ? _myChannels->height() : 0; - _channelsScroll->scrollToY(request.ymin + add, request.ymax + add); - }, lifetime); - - delegate->setContent(raw); - controller->setDelegate(delegate); - controller->setupTouchChatPreview(_channelsScroll.get()); - - return object_ptr>(this, std::move(content)); -} - bool Suggestions::persist() const { return _persist; } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 8b33718e6..cf8b21780 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/rp_widget.h" +class PeerListContent; + namespace Data { class Thread; } // namespace Data @@ -67,21 +69,32 @@ public: } [[nodiscard]] auto recentPeerChosen() const -> rpl::producer> { - return _recentPeerChosen.events(); + return _recent->chosen.events(); } [[nodiscard]] auto myChannelChosen() const -> rpl::producer> { - return _myChannelChosen.events(); + return _myChannels->chosen.events(); } [[nodiscard]] auto recommendationChosen() const -> rpl::producer> { - return _recommendationChosen.events(); + return _recommendations->chosen.events(); } + [[nodiscard]] auto recentAppChosen() const + -> rpl::producer> { + return _recentApps->chosen.events(); + } + [[nodiscard]] auto popularAppChosen() const + -> rpl::producer> { + return _popularApps->chosen.events(); + } + + class ObjectListController; private: enum class Tab : uchar { Chats, Channels, + Apps, }; enum class JumpResult : uchar { NotApplied, @@ -89,29 +102,54 @@ private: AppliedAndOut, }; + struct ObjectList { + not_null*> wrap; + rpl::variable count; + Fn choose; + Fn selectJump; + Fn updateFromParentDrag; + Fn dragLeft; + Fn)> processTouch; + rpl::event_stream> chosen; + }; + void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; void setupTabs(); void setupChats(); void setupChannels(); + void setupApps(); void selectJumpChats(Qt::Key direction, int pageSize); void selectJumpChannels(Qt::Key direction, int pageSize); + void selectJumpApps(Qt::Key direction, int pageSize); [[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition); [[nodiscard]] Data::Thread *updateFromChannelsDrag( QPoint globalPosition); + [[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition); [[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId); - [[nodiscard]] object_ptr> setupRecentPeers( + [[nodiscard]] std::unique_ptr setupRecentPeers( RecentPeersList recentPeers); - [[nodiscard]] object_ptr> setupEmptyRecent(); - [[nodiscard]] object_ptr> setupMyChannels(); - [[nodiscard]] auto setupRecommendations() + [[nodiscard]] auto setupEmptyRecent() -> object_ptr>; + + [[nodiscard]] std::unique_ptr setupMyChannels(); + [[nodiscard]] std::unique_ptr setupRecommendations(); [[nodiscard]] auto setupEmptyChannels() -> object_ptr>; + + [[nodiscard]] std::unique_ptr setupRecentApps(); + [[nodiscard]] std::unique_ptr setupPopularApps(); + + [[nodiscard]] std::unique_ptr setupObjectList( + not_null scroll, + not_null parent, + not_null controller, + Fn addToScroll = nullptr); + [[nodiscard]] object_ptr> setupEmpty( not_null parent, SearchEmptyIcon icon, @@ -119,7 +157,7 @@ private: void switchTab(Tab tab); void startShownAnimation(bool shown, Fn finish); - void startSlideAnimation(); + void startSlideAnimation(Tab was, Tab now); void finishShow(); void handlePressForChatPreview(PeerId id, Fn callback); @@ -131,43 +169,30 @@ private: const std::unique_ptr _chatsScroll; const not_null _chatsContent; + const not_null*> _topPeersWrap; const not_null _topPeers; + rpl::event_stream> _topPeerChosen; + + const std::unique_ptr _recent; - rpl::variable _recentCount; - Fn _recentPeersChoose; - Fn _recentSelectJump; - Fn _recentUpdateFromParentDrag; - Fn _recentDragLeft; - Fn)> _recentProcessTouch; - const not_null*> _recentPeers; const not_null*> _emptyRecent; const std::unique_ptr _channelsScroll; const not_null _channelsContent; - rpl::variable _myChannelsCount; - Fn _myChannelsChoose; - Fn _myChannelsSelectJump; - Fn _myChannelsUpdateFromParentDrag; - Fn _myChannelsDragLeft; - Fn)> _myChannelsProcessTouch; - const not_null*> _myChannels; - - rpl::variable _recommendationsCount; - Fn _recommendationsChoose; - Fn _recommendationsSelectJump; - Fn _recommendationsUpdateFromParentDrag; - Fn _recommendationsDragLeft; - Fn)> _recommendationsProcessTouch; - const not_null*> _recommendations; + const std::unique_ptr _myChannels; + const std::unique_ptr _recommendations; const not_null*> _emptyChannels; - rpl::event_stream> _topPeerChosen; - rpl::event_stream> _recentPeerChosen; - rpl::event_stream> _myChannelChosen; - rpl::event_stream> _recommendationChosen; + const std::unique_ptr _appsScroll; + const not_null _appsContent; + + rpl::producer<> _recentAppsRefreshed; + Fn)> _recentAppsShows; + const std::unique_ptr _recentApps; + const std::unique_ptr _popularApps; Ui::Animations::Simple _shownAnimation; Fn _showFinished; @@ -179,6 +204,9 @@ private: QPixmap _slideLeft; QPixmap _slideRight; + int _slideLeftTop = 0; + int _slideRightTop = 0; + }; [[nodiscard]] rpl::producer TopPeersContent( diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 16dbfb994..f9c9806d0 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1515,6 +1515,20 @@ ServiceAction ParseServiceAction( result.content = content; }, [&](const MTPDmessageActionRequestedPeerSentMe &data) { // Should not be in user inbox. + }, [&](const MTPDmessageActionPaymentRefunded &data) { + auto content = ActionPaymentRefunded(); + content.currency = ParseString(data.vcurrency()); + content.amount = data.vtotal_amount().v; + content.peerId = ParsePeerId(data.vpeer()); + content.transactionId = data.vcharge().data().vid().v; + result.content = content; + }, [&](const MTPDmessageActionGiftStars &data) { + auto content = ActionGiftStars(); + content.cost = Ui::FillAmountAndCurrency( + data.vamount().v, + qs(data.vcurrency())).toUtf8(); + content.stars = data.vstars().v; + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index cc16c47ba..9639730e4 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -529,7 +529,7 @@ struct ActionWebViewDataSent { struct ActionGiftPremium { Utf8String cost; - int months; + int months = 0; }; struct ActionTopicCreate { @@ -576,6 +576,18 @@ struct ActionBoostApply { int boosts = 0; }; +struct ActionPaymentRefunded { + PeerId peerId = 0; + Utf8String currency; + uint64 amount = 0; + Utf8String transactionId; +}; + +struct ActionGiftStars { + Utf8String cost; + int stars = 0; +}; + struct ServiceAction { std::variant< v::null_t, @@ -617,7 +629,9 @@ struct ServiceAction { ActionGiftCode, ActionGiveawayLaunch, ActionGiveawayResults, - ActionBoostApply> content; + ActionBoostApply, + ActionPaymentRefunded, + ActionGiftStars> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index ba39e8a8e..0bbc77322 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1315,6 +1315,22 @@ auto HtmlWriter::Wrap::pushMessage( + " boosted the group " + QByteArray::number(data.boosts) + (data.boosts > 1 ? " times" : " time"); + }, [&](const ActionPaymentRefunded &data) { + const auto amount = FormatMoneyAmount(data.amount, data.currency); + auto result = peers.wrapPeerName(data.peerId) + + " refunded back " + + amount; + return result; + }, [&](const ActionGiftStars &data) { + if (!data.stars || data.cost.isEmpty()) { + return serviceFrom + " sent you a gift."; + } + return serviceFrom + + " sent you a gift for " + + data.cost + + ": " + + QString::number(data.stars).toUtf8() + + " Telegram Stars."; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index adbad2b7f..62aaba5f7 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -625,6 +625,22 @@ QByteArray SerializeMessage( pushActor(); pushAction("boost_apply"); push("boosts", data.boosts); + }, [&](const ActionPaymentRefunded &data) { + pushAction("refunded_payment"); + push("amount", data.amount); + push("currency", data.currency); + pushBare("peer_name", wrapPeerName(data.peerId)); + push("peer_id", data.peerId); + push("charge_id", data.transactionId); + }, [&](const ActionGiftStars &data) { + pushActor(); + pushAction("send_stars_gift"); + if (!data.cost.isEmpty()) { + push("cost", data.cost); + } + if (data.stars) { + push("stars", data.stars); + } }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 9e2dfd879..96d8842e8 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/history_translation.h" #include "history/history_unread_things.h" +#include "core/ui_integration.h" #include "dialogs/ui/dialogs_layout.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" @@ -1136,14 +1137,23 @@ void History::applyServiceChanges( } if (paid) { // Toast on a current active window. + const auto context = [=](not_null toast) { + return Core::MarkedTextContext{ + .session = &session(), + .customEmojiRepaint = [=] { toast->update(); }, + }; + }; Ui::Toast::Show({ .text = tr::lng_payments_success( tr::now, lt_amount, - Ui::Text::Bold(payment->amount), + Ui::Text::Wrapped( + payment->amount, + EntityType::Bold), lt_title, Ui::Text::Bold(paid->title), Ui::Text::WithEntities), + .textContext = context, }); } } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 094e99221..7a0ebaa45 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2232,8 +2232,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (!item->isService() && peerIsChannel(itemId.peer) && !_peer->isMegagroup()) { + constexpr auto kMinViewsCount = 10; if (const auto channel = _peer->asChannel()) { - if (channel->flags() & ChannelDataFlag::CanGetStatistics) { + if ((channel->flags() & ChannelDataFlag::CanGetStatistics) + || (channel->canPostMessages() + && item->viewsCount() >= kMinViewsCount)) { auto callback = crl::guard(controller, [=] { controller->showSection( Info::Statistics::Make(channel, itemId, {})); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ccfb07289..27654034e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_isolated_emoji.h" #include "ui/text/text_utilities.h" +#include "settings/settings_credits_graphics.h" // ShowRefundInfoBox. #include "storage/file_upload.h" #include "storage/storage_shared_media.h" #include "main/main_account.h" @@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "base/unixtime.h" #include "base/timer_rpl.h" +#include "boxes/send_credits_box.h" #include "api/api_text_entities.h" #include "api/api_updates.h" #include "data/components/scheduled_messages.h" @@ -143,6 +145,17 @@ template return fields; } +[[nodiscard]] TextWithEntities AmountAndStarCurrency( + not_null session, + int64 amount, + const QString ¤cy) { + if (currency == Ui::kCreditsCurrency) { + return Ui::CreditsEmojiSmall(session).append( + Lang::FormatCountDecimal(std::abs(amount))); + } + return { Ui::FillAmountAndCurrency(amount, currency) }; +} + } // namespace void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) { @@ -4060,7 +4073,10 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { payment->recurringInit = data.is_recurring_init(); payment->recurringUsed = data.is_recurring_used(); payment->isCreditsCurrency = (currency == Ui::kCreditsCurrency); - payment->amount = Ui::FillAmountAndCurrency(amount, currency); + payment->amount = AmountAndStarCurrency( + &_history->session(), + amount, + currency); payment->invoiceLink = std::make_shared([=]( ClickContext context) { using namespace Payments; @@ -4132,6 +4148,22 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { } } else if (type == mtpc_messageActionGiveawayResults) { UpdateComponents(HistoryServiceGiveawayResults::Bit()); + } else if (type == mtpc_messageActionPaymentRefunded) { + const auto &data = action.c_messageActionPaymentRefunded(); + UpdateComponents(HistoryServicePaymentRefund::Bit()); + const auto refund = Get(); + refund->peer = _history->owner().peer(peerFromMTP(data.vpeer())); + refund->amount = data.vtotal_amount().v; + refund->currency = qs(data.vcurrency()); + refund->transactionId = qs(data.vcharge().data().vid()); + const auto id = fullId(); + refund->link = std::make_shared([=]( + ClickContext context) { + const auto my = context.other.value(); + if (const auto window = my.sessionWindow.get()) { + Settings::ShowRefundInfoBox(window, id); + } + }); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -4766,21 +4798,32 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareGiftPremium = [&]( const MTPDmessageActionGiftPremium &action) { auto result = PreparedServiceText(); - const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto session = &_history->session(); + const auto isSelf = _from->isSelf(); const auto peer = isSelf ? _history->peer : _from; - _history->session().giftBoxStickersPacks().load(); + session->giftBoxStickersPacks().load(); const auto amount = action.vamount().v; const auto currency = qs(action.vcurrency()); - result.links.push_back(peer->createOpenLink()); - result.text = (isSelf - ? tr::lng_action_gift_received_me - : tr::lng_action_gift_received)( + const auto cost = AmountAndStarCurrency(session, amount, currency); + const auto anonymous = _from->isServiceUser(); + if (anonymous) { + result.text = tr::lng_action_gift_received_anonymous( tr::now, - lt_user, - Ui::Text::Link(peer->name(), 1), // Link 1. lt_cost, - { Ui::FillAmountAndCurrency(amount, currency) }, + cost, Ui::Text::WithEntities); + } else { + result.links.push_back(peer->createOpenLink()); + result.text = (isSelf + ? tr::lng_action_gift_received_me + : tr::lng_action_gift_received)( + tr::now, + lt_user, + Ui::Text::Link(peer->name(), 1), // Link 1. + lt_cost, + cost, + Ui::Text::WithEntities); + } return result; }; @@ -4992,9 +5035,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_user, Ui::Text::Link(peer->name(), 1), // Link 1. lt_cost, - { Ui::FillAmountAndCurrency( + AmountAndStarCurrency( + &_history->session(), action.vamount().value_or_empty(), - qs(action.vcurrency().value_or_empty())) }, + qs(action.vcurrency().value_or_empty())), Ui::Text::WithEntities); } @@ -5044,6 +5088,48 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { Ui::Text::WithEntities); return result; }; + auto preparePaymentRefunded = [&](const MTPDmessageActionPaymentRefunded &action) { + auto result = PreparedServiceText(); + const auto refund = Get(); + Assert(refund != nullptr); + Assert(refund->peer != nullptr); + + const auto amount = refund->amount; + const auto currency = refund->currency; + result.links.push_back(refund->peer->createOpenLink()); + result.text = tr::lng_action_payment_refunded( + tr::now, + lt_peer, + Ui::Text::Link(refund->peer->name(), 1), // Link 1. + lt_amount, + AmountAndStarCurrency(&_history->session(), amount, currency), + Ui::Text::WithEntities); + return result; + }; + + auto prepareGiftStars = [&]( + const MTPDmessageActionGiftStars &action) { + auto result = PreparedServiceText(); + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? _history->peer : _from; + _history->session().giftBoxStickersPacks().load(); + const auto amount = action.vamount().v; + const auto currency = qs(action.vcurrency()); + result.links.push_back(peer->createOpenLink()); + result.text = (isSelf + ? tr::lng_action_gift_received_me + : tr::lng_action_gift_received)( + tr::now, + lt_user, + Ui::Text::Link(peer->name(), 1), // Link 1. + lt_cost, + AmountAndStarCurrency( + &_history->session(), + amount, + currency), + Ui::Text::WithEntities); + return result; + }; setServiceText(action.match( prepareChatAddUserText, @@ -5087,6 +5173,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareGiveawayLaunch, prepareGiveawayResults, prepareBoostApply, + preparePaymentRefunded, + prepareGiftStars, PrepareEmptyText, PrepareErrorText)); @@ -5139,6 +5227,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { _media = std::make_unique( this, _from, + Data::GiftType::Premium, data.vmonths().v); }, [&](const MTPDmessageActionSuggestProfilePhoto &data) { data.vphoto().match([&](const MTPDphoto &photo) { @@ -5173,10 +5262,17 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { .channel = (boostedId ? history()->owner().channel(boostedId).get() : nullptr), - .months = data.vmonths().v, + .count = data.vmonths().v, + .type = Data::GiftType::Premium, .viaGiveaway = data.is_via_giveaway(), .unclaimed = data.is_unclaimed(), }); + }, [&](const MTPDmessageActionGiftStars &data) { + _media = std::make_unique( + this, + _from, + Data::GiftType::Stars, + data.vstars().v); }, [](const auto &) { }); } @@ -5449,7 +5545,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() { result.text = tr::lng_action_payment_used_recurring( tr::now, lt_amount, - { .text = payment->amount }, + payment->amount, Ui::Text::WithEntities); } else { result.text = (payment->recurringInit @@ -5457,7 +5553,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() { : tr::lng_action_payment_done)( tr::now, lt_amount, - { .text = payment->amount }, + payment->amount, lt_user, { .text = _history->peer->name() }, Ui::Text::WithEntities); @@ -5468,7 +5564,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() { : tr::lng_action_payment_done_for)( tr::now, lt_amount, - { .text = payment->amount }, + payment->amount, lt_user, { .text = _history->peer->name() }, lt_invoice, diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 8d262a52e..1a5e14014 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -649,7 +649,7 @@ struct HistoryServicePayment : public RuntimeComponent , public HistoryServiceDependentData { QString slug; - QString amount; + TextWithEntities amount; ClickHandlerPtr invoiceLink; bool recurringInit = false; bool recurringUsed = false; @@ -671,6 +671,15 @@ struct HistoryServiceCustomLink ClickHandlerPtr link; }; +struct HistoryServicePaymentRefund +: public RuntimeComponent { + ClickHandlerPtr link; + PeerData *peer = nullptr; + QString transactionId; + QString currency; + uint64 amount = 0; +}; + enum class HistorySelfDestructType { Photo, Video, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 7b6f70076..7939cdeb1 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5024,7 +5024,14 @@ bool HistoryWidget::updateCmdStartShown() { const auto user = _peer ? _peer->asUser() : nullptr; const auto bot = (user && user->isBot()) ? user : nullptr; if (bot && !bot->botInfo->botMenuButtonUrl.isEmpty()) { - session().attachWebView().requestMenu(controller(), bot); + session().attachWebView().open({ + .bot = bot, + .context = { .controller = controller() }, + .button = { + .url = bot->botInfo->botMenuButtonUrl.toUtf8(), + }, + .source = InlineBots::WebViewSourceBotMenu(), + }); } else if (!_fieldAutocomplete->isHidden()) { _fieldAutocomplete->hideAnimated(); } else { @@ -5753,7 +5760,7 @@ bool HistoryWidget::confirmSendingFiles( cursor.setPosition(position, QTextCursor::KeepAnchor); } _field->setTextCursor(cursor); - if (!insertTextOnCancel.isEmpty()) { + if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) { _field->textCursor().insertText(insertTextOnCancel); } })); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 5ab1829ef..acafd16cc 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -3415,7 +3415,7 @@ Fn ComposeControls::restoreTextCallback( cursor.setPosition(position, QTextCursor::KeepAnchor); } _field->setTextCursor(cursor); - if (!insertTextOnCancel.isEmpty()) { + if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) { _field->textCursor().insertText(insertTextOnCancel); } }); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index a0d3d6066..ee24c7eb4 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -370,7 +370,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { userpicTop, width(), st::msgPhotoSize); - } else if (const auto info = item->originalHiddenSenderInfo()) { + } else if (const auto info = item->displayHiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp index acf09d845..214de059c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp @@ -257,6 +257,7 @@ void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) { const auto was = _link; if (draft.removed) { _draft = draft; + _parsedLinks = _parser.list().current(); if (_parsedLinks.empty()) { _draft.removed = false; } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 1193b3ca9..d3730e611 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1300,7 +1300,14 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context) { - const auto item = controller->session().data().message(itemId); + CopyPostLink(controller->uiShow(), itemId, context); +} + +void CopyPostLink( + std::shared_ptr show, + FullMsgId itemId, + Context context) { + const auto item = show->session().data().message(itemId); if (!item || !item->hasDirectLink()) { return; } @@ -1327,7 +1334,7 @@ void CopyPostLink( return channel->hasUsername(); }(); - controller->showToast(isPublicLink + show->showToast(isPublicLink ? tr::lng_channel_public_link_copied(tr::now) : tr::lng_context_about_private_link(tr::now)); } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 8f00f4da8..efb2a5fdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -61,6 +61,10 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context); +void CopyPostLink( + std::shared_ptr show, + FullMsgId itemId, + Context context); void CopyStoryLink( std::shared_ptr show, FullStoryId storyId); diff --git a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp index 5a6538036..8390d09fa 100644 --- a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp +++ b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp @@ -59,7 +59,8 @@ PeerId GenerateUser(not_null history, const QString &name) { MTPVector(), MTPint(), // stories_max_id MTPPeerColor(), // color - MTPPeerColor())); // profile_color + MTPPeerColor(), // profile_color + MTPint())); // bot_active_users return peerId; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index dd50ca58b..e547ac5c0 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -956,9 +956,6 @@ QSize Message::performCountOptimalSize() { - st::msgPadding.left() - st::msgPadding.right(); if (withVisibleText) { - if (botTop) { - minHeight += botTop->height; - } if (maxWidth < textualWidth) { minHeight -= text().minHeight(); minHeight += text().countHeight(innerWidth); @@ -2454,10 +2451,10 @@ TextState Message::textState( if (getStateForwardedInfo(point, trect, &result, request)) { return result; } - if (getStateReplyInfo(point, trect, &result)) { + if (getStateViaBotIdInfo(point, trect, &result)) { return result; } - if (getStateViaBotIdInfo(point, trect, &result)) { + if (getStateReplyInfo(point, trect, &result)) { return result; } } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index d530e4779..eebbb1848 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -76,9 +76,9 @@ namespace { return rpl::single(ContentWithoutPreview(item, repaint)); } constexpr auto kFullLoaded = 2; - constexpr auto kSomeLoaded = 1; - constexpr auto kNotLoaded = 0; const auto loadedLevel = [=] { + constexpr auto kSomeLoaded = 1; + constexpr auto kNotLoaded = 0; const auto preview = media->replyPreview(); return media->replyPreviewLoaded() ? kFullLoaded diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 107bb9416..09fa2b43f 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -679,6 +679,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { result.link = results->lnk; } else if (const auto custom = item->Get()) { result.link = custom->link; + } else if (const auto payment = item->Get()) { + result.link = payment->link; } else if (media && data()->showSimilarChannels()) { result = media->textState(mediaPoint, request); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 310af8fa6..f2f98d05e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -200,9 +200,11 @@ auto GenerateGiveawayResults( margins, links)); }; + const auto isSingleWinner = (data->winnersCount == 1); pushText( - Ui::Text::Bold( - tr::lng_prizes_results_title(tr::now)), + (isSingleWinner + ? tr::lng_prizes_results_title_one + : tr::lng_prizes_results_title)(tr::now, Ui::Text::Bold), st::chatGiveawayPrizesTitleMargin); const auto showGiveawayHandler = JumpToMessageClickHandler( data->channel, @@ -219,7 +221,9 @@ auto GenerateGiveawayResults( st::chatGiveawayPrizesMargin, { { 1, showGiveawayHandler } }); pushText( - Ui::Text::Bold(tr::lng_prizes_results_winners(tr::now)), + (isSingleWinner + ? tr::lng_prizes_results_winner + : tr::lng_prizes_results_winners)(tr::now, Ui::Text::Bold), st::chatGiveawayPrizesTitleMargin); push(std::make_unique( @@ -235,6 +239,8 @@ auto GenerateGiveawayResults( } pushText({ data->unclaimedCount ? tr::lng_prizes_results_some(tr::now) + : isSingleWinner + ? tr::lng_prizes_results_one(tr::now) : tr::lng_prizes_results_all(tr::now) }, st::chatGiveawayEndDateMargin); }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index dd32fa00f..71b01fbf8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -241,19 +241,20 @@ ClickHandlerPtr MakePaidMediaLink(not_null item) { } } }); + const auto reactivate = controller + ? crl::guard( + controller, + [=](auto) { controller->widget()->activate(); }) + : Fn(); + const auto credits = Payments::IsCreditsInvoice(item); + const auto nonPanelPaymentFormProcess = (controller && credits) + ? Payments::ProcessNonPanelPaymentFormFactory(controller, done) + : nullptr; Payments::CheckoutProcess::Start( item, Payments::Mode::Payment, - (controller - ? crl::guard( - controller, - [=](auto) { controller->widget()->activate(); }) - : Fn()), - ((controller && Payments::IsCreditsInvoice(item)) - ? Payments::ProcessNonPanelPaymentFormFactory( - controller, - done) - : nullptr)); + reactivate, + nonPanelPaymentFormProcess); }); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 24e571ca6..73adc03cc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -129,9 +129,6 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) { if (via) { via->resize(availw); } - if (reply) { - [[maybe_unused]] int height = reply->resizeToWidth(availw); - } } return { newWidth, newHeight }; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index b832a6dc3..23d483c1f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -12,12 +12,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" // ClickHandlerContext #include "data/data_document.h" #include "data/data_channel.h" +#include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_credits.h" // Settings::CreditsId +#include "settings/settings_credits_graphics.h" // GiftedCreditsBox #include "settings/settings_premium.h" // Settings::ShowGiftPremium +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" @@ -45,6 +49,9 @@ QSize PremiumGift::size() { } QString PremiumGift::title() { + if (const auto count = stars()) { + return tr::lng_gift_stars_title(tr::now, lt_count, count); + } return gift() ? tr::lng_premium_summary_title(tr::now) : _data.unclaimed @@ -53,8 +60,16 @@ QString PremiumGift::title() { } TextWithEntities PremiumGift::subtitle() { - if (gift()) { - return { GiftDuration(_data.months) }; + if (const auto count = stars()) { + return outgoingGift() + ? tr::lng_gift_stars_outgoing( + tr::now, + lt_user, + Ui::Text::Bold(_parent->history()->peer->shortName()), + Ui::Text::WithEntities) + : tr::lng_gift_stars_incoming(tr::now, Ui::Text::WithEntities); + } else if (gift()) { + return { GiftDuration(_data.count) }; } const auto name = _data.channel ? _data.channel->name() : "channel"; auto result = (_data.unclaimed @@ -74,7 +89,7 @@ TextWithEntities PremiumGift::subtitle() { : tr::lng_prize_gift_duration)( tr::now, lt_duration, - Ui::Text::Bold(GiftDuration(_data.months)), + Ui::Text::Bold(GiftDuration(_data.count)), Ui::Text::RichLangValue)); return result; } @@ -87,20 +102,29 @@ rpl::producer PremiumGift::button() { ClickHandlerPtr PremiumGift::createViewLink() { const auto from = _gift->from(); - const auto to = _parent->history()->peer; + const auto peer = _parent->history()->peer; + const auto date = _parent->data()->date(); const auto data = _gift->data(); return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { const auto selfId = controller->session().userPeerId(); - const auto self = (from->id == selfId); - if (data.slug.isEmpty()) { - const auto peer = self ? to : from; - const auto months = data.months; - Settings::ShowGiftPremium(controller, peer, months, self); + const auto sent = (from->id == selfId); + if (data.type == Data::GiftType::Stars) { + const auto to = sent ? peer : peer->session().user(); + controller->show(Box( + Settings::GiftedCreditsBox, + controller, + from, + to, + data.count, + date)); + } else if (data.slug.isEmpty()) { + const auto months = data.count; + Settings::ShowGiftPremium(controller, peer, months, sent); } else { const auto fromId = from->id; - const auto toId = self ? to->id : selfId; + const auto toId = sent ? peer->id : selfId; ResolveGiftCode(controller, data.slug, fromId, toId); } } @@ -162,13 +186,18 @@ bool PremiumGift::gift() const { return _data.slug.isEmpty() || !_data.channel; } +int PremiumGift::stars() const { + return (_data.type == Data::GiftType::Stars) ? _data.count : 0; +} + void PremiumGift::ensureStickerCreated() const { if (_sticker) { return; } const auto &session = _parent->history()->session(); - const auto months = _gift->data().months; auto &packs = session.giftBoxStickersPacks(); + const auto count = stars(); + const auto months = count ? packs.monthsForStars(count) : _data.count; if (const auto document = packs.lookup(months)) { if (const auto sticker = document->sticker()) { const auto skipPremiumEffect = false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index af5724dfa..7240dd49e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -49,6 +49,7 @@ private: [[nodiscard]] bool incomingGift() const; [[nodiscard]] bool outgoingGift() const; [[nodiscard]] bool gift() const; + [[nodiscard]] int stars() const; void ensureStickerCreated() const; const not_null _parent; diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp index 05a03df4d..aa14b12c1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp @@ -167,7 +167,7 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { _hasHeavyPart = 1; validateLastPremiumLock(); const auto drawOne = [&](const Channel &channel) { - const auto geometry = channel.geometry.translated(-_scrollLeft, 0); + const auto geometry = channel.geometry.translated(-int(_scrollLeft), 0); const auto right = geometry.x() + geometry.width(); if (right <= 0) { return; @@ -501,7 +501,7 @@ TextState SimilarChannels::textState( return result; } for (const auto &channel : _channels) { - if (channel.geometry.translated(-_scrollLeft, 0).contains(point)) { + if (channel.geometry.translated(-int(_scrollLeft), 0).contains(point)) { result.link = channel.link; _lastPoint = point + QPoint(_scrollLeft, 0) diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index 169fe8883..cbe32faf5 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -261,7 +261,7 @@ void InnerWidget::fillHistory() { const auto sectionIndex = history->lifetime().make_state(0); - const auto fill = [=]( + const auto fill = [=, peer = _peer]( not_null premiumBot, const Data::CreditsStatusSlice &fullSlice, const Data::CreditsStatusSlice &inSlice, @@ -368,7 +368,7 @@ void InnerWidget::fillHistory() { fullSlice, fullWrap->entity(), entryClicked, - premiumBot, + peer, star, true, true); @@ -377,7 +377,7 @@ void InnerWidget::fillHistory() { inSlice, inWrap->entity(), entryClicked, - premiumBot, + peer, star, true, false); @@ -386,7 +386,7 @@ void InnerWidget::fillHistory() { outSlice, outWrap->entity(), std::move(entryClicked), - premiumBot, + peer, star, false, true); @@ -398,11 +398,11 @@ void InnerWidget::fillHistory() { const auto apiLifetime = history->lifetime().make_state(); rpl::single(rpl::empty) | rpl::then( _stateUpdated.events() - ) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=, peer = _peer] { using Api = Api::CreditsHistory; - const auto apiFull = apiLifetime->make_state(_peer, true, true); - const auto apiIn = apiLifetime->make_state(_peer, true, false); - const auto apiOut = apiLifetime->make_state(_peer, false, true); + const auto apiFull = apiLifetime->make_state(peer, true, true); + const auto apiIn = apiLifetime->make_state(peer, true, false); + const auto apiOut = apiLifetime->make_state(peer, false, true); apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) { apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) { apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) { diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp index feb6d395e..aea01c592 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp @@ -20,12 +20,17 @@ namespace Info::Statistics { not_null InfiniteRadialAnimationWidget( not_null parent, - int size) { + int size, + const style::InfiniteRadialAnimation *st) { class Widget final : public Ui::RpWidget { public: - Widget(not_null p, int size) + Widget( + not_null p, + int size, + const style::InfiniteRadialAnimation *st) : Ui::RpWidget(p) - , _animation([=] { update(); }, st::startGiveawayButtonLoading) { + , _st(st ? st : &st::startGiveawayButtonLoading) + , _animation([=] { update(); }, *_st) { resize(size, size); shownValue() | rpl::start_with_next([=](bool v) { return v @@ -39,17 +44,17 @@ not_null InfiniteRadialAnimationWidget( auto p = QPainter(this); p.setPen(st::activeButtonFg); p.setBrush(st::activeButtonFg); - const auto r = rect() - - Margins(st::startGiveawayButtonLoading.thickness); + const auto r = rect() - Margins(_st->thickness); _animation.draw(p, r.topLeft(), r.size(), width()); } private: + const style::InfiniteRadialAnimation *_st; Ui::InfiniteRadialAnimation _animation; }; - return Ui::CreateChild(parent.get(), size); + return Ui::CreateChild(parent.get(), size, st); } void AddChildToWidgetCenter( diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h index 221f6017f..bc42a9d1c 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once namespace style { +struct InfiniteRadialAnimation; struct TextStyle; } // namespace style @@ -30,7 +31,8 @@ namespace Info::Statistics { [[nodiscard]] not_null InfiniteRadialAnimationWidget( not_null parent, - int size); + int size, + const style::InfiniteRadialAnimation *st = nullptr); void AddChildToWidgetCenter( not_null parent, diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index 84ad81390..c96dac4ea 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -281,6 +281,9 @@ void InnerWidget::load() { rpl::lifetime apiPremiumBotLifetime; }; const auto state = lifetime().make_state(_peer); + using ChannelFlag = ChannelDataFlag; + const auto canViewCredits = !_peer->isChannel() + || (_peer->asChannel()->flags() & ChannelFlag::CanViewCreditsRevenue); Info::Statistics::FillLoading( this, @@ -363,7 +366,11 @@ void InnerWidget::load() { _state.premiumBotId = bot->id; state->apiCredits.request( ) | rpl::start_with_error_done([=](const QString &error) { - fail(error); + if (canViewCredits) { + fail(error); + } else { + _state.creditsEarn = {}; + } finish(); }, [=] { _state.creditsEarn = state->apiCredits.data(); @@ -1412,7 +1419,7 @@ void InnerWidget::fill() { data.creditsStatusSlice, tabCreditsList->entity(), entryClicked, - premiumBot, + _peer, star, true, true); diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 3782678b3..0c763acb1 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -469,6 +469,12 @@ infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 19px); +infoOpenApp: RoundButton(defaultActiveButton) { + textTop: 11px; + height: 40px; +} +infoOpenAppMargin: margins(16px, 12px, 16px, 12px); + infoPersonalChannelIconPosition: point(25px, 20px); infoPersonalChannelNameLabel: FlatLabel(infoProfileStatus) { textFg: windowBoldFg; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 5996f889e..6dbe4fab2 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_text.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_widget.h" +#include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_mute.h" @@ -784,6 +785,7 @@ private: object_ptr setupPersonalChannel(not_null user); object_ptr setupInfo(); object_ptr setupMuteToggle(); + void setupMainApp(); void setupMainButtons(); Ui::MultiSlideTracker fillTopicButtons(); Ui::MultiSlideTracker fillUserButtons( @@ -1331,10 +1333,21 @@ object_ptr DetailsFiller::setupInfo() { } if (!_peer->isSelf()) { // No notifications toggle for Self => no separator. + + const auto user = _peer->asUser(); + const auto app = user && user->botInfo && user->botInfo->hasMainApp; + const auto padding = app + ? QMargins( + st::infoOpenAppMargin.left(), + st::infoProfileSeparatorPadding.top(), + st::infoOpenAppMargin.right(), + 0) + : st::infoProfileSeparatorPadding; + result->add(object_ptr>( result, object_ptr(result), - st::infoProfileSeparatorPadding) + padding) )->setDuration( st::infoSlideDuration )->toggleOn( @@ -1670,6 +1683,42 @@ object_ptr DetailsFiller::setupMuteToggle() { return result; } +void DetailsFiller::setupMainApp() { + const auto button = _wrap->add( + object_ptr( + _wrap, + tr::lng_profile_open_app(), + st::infoOpenApp), + st::infoOpenAppMargin); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + + const auto user = _peer->asUser(); + const auto controller = _controller->parentController(); + button->setClickedCallback([=] { + user->session().attachWebView().open({ + .bot = user, + .context = { + .controller = controller, + .maySkipConfirmation = true, + }, + .source = InlineBots::WebViewSourceBotProfile(), + }); + }); + + const auto url = tr::lng_mini_apps_tos_url(tr::now); + Ui::AddDividerText( + _wrap, + tr::lng_profile_open_app_about( + lt_terms, + tr::lng_profile_open_app_terms() | Ui::Text::ToLink(url), + Ui::Text::WithEntities) + )->setClickHandlerFilter([=](const auto &...) { + UrlClickHandler::Open(url); + return false; + }); + Ui::AddSkip(_wrap); +} + void DetailsFiller::setupMainButtons() { auto wrapButtons = [=](auto &&callback) { auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap)); @@ -1859,6 +1908,13 @@ object_ptr DetailsFiller::fill() { add(object_ptr(_wrap)); add(CreateSkipWidget(_wrap)); add(setupInfo()); + if (const auto user = _peer->asUser()) { + if (const auto info = user->botInfo.get()) { + if (info->hasMainApp) { + setupMainApp(); + } + } + } if (!_peer->isSelf()) { add(setupMuteToggle()); } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 956c7d0c3..86640fe5b 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -131,7 +131,7 @@ struct BoostsDescriptor final { struct CreditsDescriptor final { Data::CreditsStatusSlice firstSlice; Fn entryClickedCallback; - not_null premiumBot; + not_null peer; not_null creditIcon; bool in = false; bool out = false; @@ -800,7 +800,9 @@ void CreditsRow::init() { : _entry.failed ? (joiner + tr::lng_channel_earn_history_failed(tr::now)) : QString()) - + (_entry.title.isEmpty() ? QString() : (joiner + _name))); + + ((_entry.gift && PeerListRow::special()) + ? (joiner + tr::lng_credits_box_history_entry_anonymous(tr::now)) + : (_entry.title.isEmpty() ? QString() : (joiner + _name)))); { constexpr auto kMinus = QChar(0x2212); _rightText.setText( @@ -889,7 +891,6 @@ private: void applySlice(const Data::CreditsStatusSlice &slice); const not_null _session; - const not_null _premiumBot; Fn _entryClickedCallback; not_null const _creditIcon; @@ -903,11 +904,10 @@ private: }; CreditsController::CreditsController(CreditsDescriptor d) -: _session(&d.premiumBot->session()) -, _premiumBot(d.premiumBot) +: _session(&d.peer->session()) , _entryClickedCallback(std::move(d.entryClickedCallback)) , _creditIcon(d.creditIcon) -, _api(d.premiumBot->session().user(), d.in, d.out) +, _api(d.peer, d.in, d.out) , _firstSlice(std::move(d.firstSlice)) { PeerListController::setStyleOverrides(&st::boostsListBox); } @@ -950,12 +950,9 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { delegate()->peerListUpdateRow(row); }, }; - using Type = Data::CreditsHistoryEntry::PeerType; if (const auto peerId = PeerId(item.barePeerId)) { const auto peer = session().data().peer(peerId); return std::make_unique(peer, descriptor); - } else if (item.peerType == Type::PremiumBot) { - return std::make_unique(_premiumBot, descriptor); } else { return std::make_unique(descriptor); } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h index 2d381cb7c..8423de55c 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h @@ -55,7 +55,7 @@ void AddCreditsHistoryList( const Data::CreditsStatusSlice &firstSlice, not_null container, Fn entryClickedCallback, - not_null premiumBot, + not_null peer, not_null creditIcon, bool in, bool out); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 31f66f0ff..3f549edb1 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -9,8 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_common.h" +#include "api/api_sending.h" #include "base/qthelp_url.h" +#include "boxes/share_box.h" #include "core/click_handler_types.h" +#include "core/shortcuts.h" +#include "data/components/location_pickers.h" #include "data/data_bot_app.h" #include "data/data_changes.h" #include "data/data_user.h" @@ -27,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_instance.h" #include "ui/boxes/confirm_box.h" #include "ui/chat/attach/attach_bot_webview.h" +#include "ui/controls/location_picker.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/dropdown_menu.h" #include "ui/widgets/popup_menu.h" @@ -59,15 +64,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include -// AyuGram includes -#include "ayu/ayu_settings.h" - - namespace InlineBots { namespace { constexpr auto kProlongTimeout = 60 * crl::time(1000); constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); +constexpr auto kPopularAppBotsLimit = 100; [[nodiscard]] DocumentData *ResolveIcon( not_null session, @@ -158,8 +160,36 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); return result; } +[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig( + not_null session) { + const auto &appConfig = session->appConfig(); + auto map = appConfig.get>( + u"tdesktop_config_map"_q, + base::flat_map()); + return { + .mapsToken = map[u"maps"_q], + .geoToken = map[u"geo"_q], + }; +} + +[[nodiscard]] Window::SessionController *WindowForThread( + base::weak_ptr weak, + not_null thread) { + if (const auto separate = Core::App().separateWindowFor(thread)) { + return separate->sessionController(); + } + const auto strong = weak.get(); + if (strong && strong->windowId().hasChatsList()) { + strong->showThread(thread); + return strong; + } + const auto window = Core::App().ensureSeparateWindowFor(thread); + return window ? window->sessionController() : nullptr; +} + void ShowChooseBox( - not_null controller, + std::shared_ptr show, + not_null session, PeerTypes types, Fn)> callback, rpl::producer titleOverride = nullptr) { @@ -194,26 +224,36 @@ void ShowChooseBox( box->closeBox(); }); }; - *weak = controller->show(Box( + *weak = show->show(Box( std::make_unique( - &controller->session(), + session, std::move(done), std::move(filter)), std::move(initBox))); } -[[nodiscard]] base::flat_set> &ActiveWebViews() { - static auto result = base::flat_set>(); - return result; +void ShowChooseBox( + not_null controller, + PeerTypes types, + Fn)> callback, + rpl::producer titleOverride = nullptr) { + ShowChooseBox( + controller->uiShow(), + &controller->session(), + types, + std::move(callback), + std::move(titleOverride)); } -void FillDisclaimerBox(not_null box, Fn done) { +void FillDisclaimerBox( + not_null box, + Fn done) { const auto updateCheck = std::make_shared>(); const auto validateCheck = std::make_shared>(); const auto callback = [=](Fn close) { if (validateCheck && (*validateCheck)()) { - done(); + done(true); close(); } }; @@ -224,6 +264,7 @@ void FillDisclaimerBox(not_null box, Fn done) { tr::now, Ui::Text::RichLangValue), .confirmed = callback, + .cancelled = [=](Fn close) { done(false); close(); }, .confirmText = tr::lng_box_ok(), .labelPadding = QMargins(padding.left(), 0, padding.right(), 0), .title = tr::lng_mini_apps_disclaimer_title(), @@ -279,10 +320,43 @@ void FillDisclaimerBox(not_null box, Fn done) { }; } +WebViewContext ResolveContext( + not_null bot, + WebViewContext context) { + if (!context.dialogsEntryState.key) { + if (const auto strong = context.controller.get()) { + context.dialogsEntryState = strong->currentDialogsEntryState(); + } + } + if (!context.action) { + const auto &state = context.dialogsEntryState; + if (const auto thread = state.key.thread()) { + context.action = Api::SendAction(thread); + context.action->replyTo = state.currentReplyTo; + } else { + context.action = Api::SendAction(bot->owner().history(bot)); + } + } + if (!context.dialogsEntryState.key) { + using namespace Dialogs; + using Section = EntryState::Section; + const auto history = context.action->history; + const auto topicId = context.action->replyTo.topicRootId; + const auto topic = history->peer->forumTopicFor(topicId); + context.dialogsEntryState = EntryState{ + .key = (topic ? Key{ topic } : Key{ history }), + .section = (topic ? Section::Replies : Section::History), + .currentReplyTo = context.action->replyTo, + }; + } + return context; +} + class BotAction final : public Ui::Menu::ItemBase { public: BotAction( not_null parent, + std::shared_ptr show, const style::Menu &st, const AttachWebViewBot &bot, Fn callback); @@ -305,6 +379,7 @@ private: void prepare(); void paint(Painter &p); + const std::shared_ptr _show; const not_null _dummyAction; const style::Menu &_st; const AttachWebViewBot _bot; @@ -322,10 +397,12 @@ private: BotAction::BotAction( not_null parent, + std::shared_ptr show, const style::Menu &st, const AttachWebViewBot &bot, Fn callback) : ItemBase(parent, st) +, _show(std::move(show)) , _dummyAction(new QAction(parent)) , _st(st) , _bot(bot) @@ -397,7 +474,8 @@ void BotAction::contextMenuEvent(QContextMenuEvent *e) { this, st::popupMenuWithIcons); _menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] { - _bot.user->session().attachWebView().removeFromMenu(_bot.user); + const auto bot = _bot.user; + bot->session().attachWebView().removeFromMenu(_show, bot); }, &st::menuIconDelete); QObject::connect(_menu, &QObject::destroyed, [=] { @@ -435,13 +513,10 @@ void BotAction::handleKeyPress(not_null e) { } } -QString WebviewPlatform() { - const auto settings = &AyuSettings::getInstance(); - return settings->spoofWebviewAsAndroid ? "android" : "tdesktop"; -} - } // namespace +base::weak_ptr WebViewInstance::PendingActivation; + MenuBotIcon::MenuBotIcon( QWidget *parent, std::shared_ptr media) @@ -521,82 +596,556 @@ PeerTypes ParseChooseTypes(QStringView choose) { return result; } -struct AttachWebView::Context { - base::weak_ptr controller; - Dialogs::EntryState dialogsEntryState; - Api::SendAction action; - bool fromSwitch = false; - bool fromMainMenu = false; - bool fromBotApp = false; -}; - -AttachWebView::AttachWebView(not_null session) -: _session(session) -, _refreshTimer([=] { requestBots(); }) { - _refreshTimer.callEach(kRefreshBotsTimeout); -} - -AttachWebView::~AttachWebView() { - ActiveWebViews().remove(this); -} - -void AttachWebView::request( - not_null controller, - const Api::SendAction &action, - const QString &botUsername, - const QString &startCommand) { - if (botUsername.isEmpty()) { - return; - } - const auto username = _bot ? _bot->username() : _botUsername; - const auto context = LookupContext(controller, action); - if (IsSame(_context, context) - && username.toLower() == botUsername.toLower() - && _startCommand == startCommand) { - if (_panel) { - _panel->requestActivate(); - } - return; - } - cancel(); - - _context = std::make_unique(context); - _botUsername = botUsername; - _startCommand = startCommand; +WebViewInstance::WebViewInstance(WebViewDescriptor &&descriptor) +: _parentShow(descriptor.parentShow + ? std::move(descriptor.parentShow) + : descriptor.context.controller + ? descriptor.context.controller.get()->uiShow() + : nullptr) +, _session(&descriptor.bot->session()) +, _bot(descriptor.bot) +, _context(ResolveContext(_bot, std::move(descriptor.context))) +, _button(std::move(descriptor.button)) +, _source(std::move(descriptor.source)) { resolve(); } -Webview::ThemeParams AttachWebView::botThemeParams() { +WebViewInstance::~WebViewInstance() { + _session->api().request(base::take(_requestId)).cancel(); + _session->api().request(base::take(_prolongId)).cancel(); + base::take(_panel); +} + +Main::Session &WebViewInstance::session() const { + return *_session; +} + +not_null WebViewInstance::bot() const { + return _bot; +} + +WebViewSource WebViewInstance::source() const { + return _source; +} + +void WebViewInstance::activate() { + if (_panel) { + _panel->requestActivate(); + } else { + PendingActivation = this; + } +} + +void WebViewInstance::resolve() { + v::match(_source, [&](WebViewSourceButton data) { + confirmOpen([=] { + if (data.simple) { + requestSimple(); + } else { + requestButton(); + } + }); + }, [&](WebViewSourceSwitch) { + confirmOpen([=] { + requestSimple(); + }); + }, [&](WebViewSourceLinkApp data) { + resolveApp(data.appname, data.token, !_context.maySkipConfirmation); + }, [&](WebViewSourceLinkBotProfile) { + confirmOpen([=] { + requestMain(); + }); + }, [&](WebViewSourceLinkAttachMenu data) { + requestWithMenuAdd(); + }, [&](WebViewSourceMainMenu) { + requestWithMainMenuDisclaimer(); + }, [&](WebViewSourceAttachMenu) { + requestWithMenuAdd(); + }, [&](WebViewSourceBotMenu) { + if (!openAppFromBotMenuLink()) { + confirmOpen([=] { + requestButton(); + }); + } + }, [&](WebViewSourceGame game) { + showGame(); + }, [&](WebViewSourceBotProfile) { + if (_context.maySkipConfirmation) { + requestMain(); + } else { + confirmOpen([=] { + requestMain(); + }); + } + }); +} + +bool WebViewInstance::openAppFromBotMenuLink() { + const auto url = QString::fromUtf8(_button.url); + const auto local = Core::TryConvertUrlToLocal(url); + const auto prefix = u"tg://resolve?"_q; + if (!local.startsWith(prefix)) { + return false; + } + const auto params = qthelp::url_parse_params( + local.mid(prefix.size()), + qthelp::UrlParamNameTransform::ToLower); + const auto domainParam = params.value(u"domain"_q); + const auto appnameParam = params.value(u"appname"_q); + const auto webChannelPreviewLink = (domainParam == u"s"_q) + && !appnameParam.isEmpty(); + const auto appname = webChannelPreviewLink ? QString() : appnameParam; + if (appname.isEmpty()) { + return false; + } + resolveApp(appname, params.value(u"startapp"_q), true); + return true; +} + +void WebViewInstance::resolveApp( + const QString &appname, + const QString &startparam, + bool forceConfirmation) { + const auto already = _session->data().findBotApp(_bot->id, appname); + _requestId = _session->api().request(MTPmessages_GetBotApp( + MTP_inputBotAppShortName( + _bot->inputUser, + MTP_string(appname)), + MTP_long(already ? already->hash : 0) + )).done([=](const MTPmessages_BotApp &result) { + _requestId = 0; + const auto &data = result.data(); + const auto received = _session->data().processBotApp( + _bot->id, + data.vapp()); + _app = received ? received : already; + _appStartParam = startparam; + if (!_app) { + _parentShow->showToast(tr::lng_username_app_not_found(tr::now)); + close(); + return; + } + const auto confirm = data.is_inactive() || forceConfirmation; + const auto writeAccess = result.data().is_request_write_access(); + + // Check if this app can be added to main menu. + // On fail it'll still be opened. + using Result = AttachWebView::AddToMenuResult; + const auto done = crl::guard(this, [=](Result value, auto) { + if (value == Result::Cancelled) { + close(); + } else if (value != Result::Unsupported) { + requestApp(true); + } else if (confirm) { + confirmAppOpen(writeAccess, [=](bool allowWrite) { + requestApp(allowWrite); + }); + } else { + requestApp(false); + } + }); + _session->attachWebView().requestAddToMenu(_bot, done); + }).fail([=] { + _parentShow->showToast(tr::lng_username_app_not_found(tr::now)); + close(); + }).send(); +} + +void WebViewInstance::confirmOpen(Fn done) { + if (_bot->isVerified() + || _session->local().isBotTrustedOpenWebView(_bot->id)) { + done(); + return; + } + const auto callback = [=](Fn close) { + _session->local().markBotTrustedOpenWebView(_bot->id); + close(); + done(); + }; + const auto cancel = [=](Fn close) { + botClose(); + close(); + }; + _parentShow->show(Ui::MakeConfirmBox({ + .text = tr::lng_allow_bot_webview( + tr::now, + lt_bot_name, + Ui::Text::Bold(_bot->name()), + Ui::Text::RichLangValue), + .confirmed = crl::guard(this, callback), + .cancelled = crl::guard(this, cancel), + .confirmText = tr::lng_box_ok(), + })); +} + +void WebViewInstance::confirmAppOpen( + bool writeAccess, + Fn done) { + _parentShow->show(Box([=](not_null box) { + const auto allowed = std::make_shared(); + const auto callback = [=](Fn close) { + done((*allowed) && (*allowed)->checked()); + close(); + }; + const auto cancelled = [=](Fn close) { + botClose(); + close(); + }; + Ui::ConfirmBox(box, { + tr::lng_allow_bot_webview( + tr::now, + lt_bot_name, + Ui::Text::Bold(_bot->name()), + Ui::Text::RichLangValue), + crl::guard(this, callback), + crl::guard(this, cancelled), + }); + if (writeAccess) { + (*allowed) = box->addRow( + object_ptr( + box, + tr::lng_url_auth_allow_messages( + tr::now, + lt_bot, + Ui::Text::Bold(_bot->name()), + Ui::Text::WithEntities), + true, + st::urlAuthCheckbox), + style::margins( + st::boxRowPadding.left(), + st::boxPhotoCaptionSkip, + st::boxRowPadding.right(), + st::boxPhotoCaptionSkip)); + (*allowed)->setAllowTextLines(); + } + })); +} + +void WebViewInstance::requestButton() { + Expects(_context.action.has_value()); + + const auto &action = *_context.action; + using Flag = MTPmessages_RequestWebView::Flag; + _requestId = _session->api().request(MTPmessages_RequestWebView( + MTP_flags(Flag::f_theme_params + | (_button.url.isEmpty() ? Flag(0) : Flag::f_url) + | (_button.startCommand.isEmpty() + ? Flag(0) + : Flag::f_start_param) + | (v::is(_source) + ? Flag::f_from_bot_menu + : Flag(0)) + | (action.replyTo ? Flag::f_reply_to : Flag(0)) + | (action.options.sendAs ? Flag::f_send_as : Flag(0)) + | (action.options.silent ? Flag::f_silent : Flag(0))), + action.history->peer->input, + _bot->inputUser, + MTP_bytes(_button.url), + MTP_string(_button.startCommand), + MTP_dataJSON(MTP_bytes(botThemeParams().json)), + MTP_string("tdesktop"), + action.mtpReplyTo(), + (action.options.sendAs + ? action.options.sendAs->input + : MTP_inputPeerEmpty()) + )).done([=](const MTPWebViewResult &result) { + const auto &data = result.data(); + show(qs(data.vurl()), data.vquery_id().value_or_empty()); + }).fail([=](const MTP::Error &error) { + _parentShow->showToast(error.type()); + if (error.type() == u"BOT_INVALID"_q) { + _session->attachWebView().requestBots(); + } + close(); + }).send(); +} + +void WebViewInstance::requestSimple() { + using Flag = MTPmessages_RequestSimpleWebView::Flag; + _requestId = _session->api().request(MTPmessages_RequestSimpleWebView( + MTP_flags(Flag::f_theme_params + | (v::is(_source) + ? (Flag::f_url | Flag::f_from_switch_webview) + : v::is(_source) + ? (Flag::f_from_side_menu + | (_button.startCommand.isEmpty() // from LinkMainMenu + ? Flag() + : Flag::f_start_param)) + : Flag::f_url)), + _bot->inputUser, + MTP_bytes(_button.url), + MTP_string(_button.startCommand), + MTP_dataJSON(MTP_bytes(botThemeParams().json)), + MTP_string("tdesktop") + )).done([=](const MTPWebViewResult &result) { + show(qs(result.data().vurl())); + }).fail([=](const MTP::Error &error) { + _parentShow->showToast(error.type()); + close(); + }).send(); +} + +void WebViewInstance::requestMain() { + using Flag = MTPmessages_RequestMainWebView::Flag; + _requestId = _session->api().request(MTPmessages_RequestMainWebView( + MTP_flags(Flag::f_theme_params + | (_button.startCommand.isEmpty() + ? Flag() + : Flag::f_start_param) + | (v::is(_source) + ? (v::get(_source).compact + ? Flag::f_compact + : Flag(0)) + : Flag(0))), + _context.action->history->peer->input, + _bot->inputUser, + MTP_string(_button.startCommand), + MTP_dataJSON(MTP_bytes(botThemeParams().json)), + MTP_string("tdesktop") + )).done([=](const MTPWebViewResult &result) { + show(qs(result.data().vurl())); + }).fail([=](const MTP::Error &error) { + _parentShow->showToast(error.type()); + close(); + }).send(); +} + +void WebViewInstance::requestApp(bool allowWrite) { + Expects(_app != nullptr); + Expects(_context.action.has_value()); + + using Flag = MTPmessages_RequestAppWebView::Flag; + const auto app = _app; + const auto flags = Flag::f_theme_params + | (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param) + | (allowWrite ? Flag::f_write_allowed : Flag(0)); + _requestId = _session->api().request(MTPmessages_RequestAppWebView( + MTP_flags(flags), + _context.action->history->peer->input, + MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)), + MTP_string(_appStartParam), + MTP_dataJSON(MTP_bytes(botThemeParams().json)), + MTP_string("tdesktop") + )).done([=](const MTPWebViewResult &result) { + _requestId = 0; + show(qs(result.data().vurl())); + }).fail([=](const MTP::Error &error) { + _requestId = 0; + if (error.type() == u"BOT_INVALID"_q) { + _session->attachWebView().requestBots(); + } + close(); + }).send(); +} + +void WebViewInstance::requestWithMainMenuDisclaimer() { + using Result = AttachWebView::AddToMenuResult; + const auto done = crl::guard(this, [=](Result value, auto) { + if (value == Result::Cancelled) { + close(); + } else if (value == Result::Unsupported) { + _parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now)); + close(); + } else { + requestSimple(); + } + }); + _session->attachWebView().acceptMainMenuDisclaimer( + _parentShow, + _bot, + done); +} + +void WebViewInstance::requestWithMenuAdd() { + using Result = AttachWebView::AddToMenuResult; + const auto done = crl::guard(this, [=](Result value, PeerTypes types) { + if (value == Result::Cancelled) { + close(); + } else if (value == Result::Unsupported) { + _parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now)); + close(); + } else if (v::is(_source)) { + maybeChooseAndRequestButton(types); + } else if (v::is(_source)) { + requestButton(); + } else { + requestSimple(); + } + }); + _session->attachWebView().requestAddToMenu(_bot, done); +} + +void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) { + Expects(v::is(_source)); + + const auto link = v::get(_source); + const auto chooseFrom = (link.choose & supported); + if (!chooseFrom) { + requestButton(); + return; + } + const auto bot = _bot; + const auto button = _button; + const auto weak = _context.controller; + const auto done = [=](not_null thread) { + if (const auto controller = WindowForThread(weak, thread)) { + thread->session().attachWebView().open({ + .bot = bot, + .context = { + .controller = controller, + .action = Api::SendAction(thread), + }, + .button = button, + .source = InlineBots::WebViewSourceLinkAttachMenu{ + .thread = thread, + .token = button.startCommand, + }, + }); + } + }; + ShowChooseBox(_parentShow, _session, chooseFrom, done); + close(); +} + +void WebViewInstance::show(const QString &url, uint64 queryId) { + auto title = Info::Profile::NameValue(_bot); + + const auto &bots = _session->attachWebView().attachBots(); + + using Button = Ui::BotWebView::MenuButton; + const auto attached = ranges::find( + bots, + not_null{ _bot }, + &AttachWebViewBot::user); + const auto hasOpenBot = v::is(_source) + || (_context.action->history->peer != _bot); + const auto hasRemoveFromMenu = (attached != end(bots)) + && (!attached->inactive || attached->inMainMenu) + && (v::is(_source) + || v::is(_source) + || v::is(_source)); + const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None) + | (!hasRemoveFromMenu + ? Button::None + : attached->inMainMenu + ? Button::RemoveFromMainMenu + : Button::RemoveFromMenu); + const auto allowClipboardRead = v::is(_source) + || v::is(_source) + || (attached != end(bots) + && (attached->inAttachMenu || attached->inMainMenu)); + _panelUrl = url; + _panel = Ui::BotWebView::Show({ + .url = url, + .storageId = _session->local().resolveStorageIdBots(), + .title = std::move(title), + .bottom = rpl::single('@' + _bot->username()), + .delegate = static_cast(this), + .menuButtons = buttons, + .allowClipboardRead = allowClipboardRead, + }); + started(queryId); + + if (const auto strong = PendingActivation.get()) { + if (strong == this) { + PendingActivation = nullptr; + _panel->requestActivate(); + } + } +} + +void WebViewInstance::showGame() { + Expects(v::is(_source)); + + const auto game = v::get(_source); + _panelUrl = QString::fromUtf8(_button.url); + _panel = Ui::BotWebView::Show({ + .url = _panelUrl, + .storageId = _session->local().resolveStorageIdBots(), + .title = rpl::single(game.title), + .bottom = rpl::single('@' + _bot->username()), + .delegate = static_cast(this), + .menuButtons = Ui::BotWebView::MenuButton::ShareGame, + }); +} + +void WebViewInstance::close() { + _session->attachWebView().close(this); +} + +void WebViewInstance::started(uint64 queryId) { + Expects(_context.action.has_value()); + + if (!queryId) { + return; + } + + _session->data().webViewResultSent( + ) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) { + return (sent.queryId == queryId); + }) | rpl::start_with_next([=] { + close(); + }, _panel->lifetime()); + + const auto action = *_context.action; + base::timer_each( + kProlongTimeout + ) | rpl::start_with_next([=] { + using Flag = MTPmessages_ProlongWebView::Flag; + _session->api().request(base::take(_prolongId)).cancel(); + _prolongId = _session->api().request(MTPmessages_ProlongWebView( + MTP_flags(Flag(0) + | (action.replyTo ? Flag::f_reply_to : Flag(0)) + | (action.options.sendAs ? Flag::f_send_as : Flag(0)) + | (action.options.silent ? Flag::f_silent : Flag(0))), + action.history->peer->input, + _bot->inputUser, + MTP_long(queryId), + action.mtpReplyTo(), + (action.options.sendAs + ? action.options.sendAs->input + : MTP_inputPeerEmpty()) + )).done([=] { + _prolongId = 0; + }).send(); + }, _panel->lifetime()); +} + +Webview::ThemeParams WebViewInstance::botThemeParams() { return Window::Theme::WebViewParams(); } -bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) { +bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) { const auto local = Core::TryConvertUrlToLocal(uri); - if (uri == local || Core::InternalPassportLink(local)) { - return local.startsWith(u"tg://"_q); - } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { + if (Core::InternalPassportLink(local)) { + return true; + } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive) + && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) { return false; } + const auto bot = _bot; + const auto context = std::make_shared(_context); if (!keepOpen) { botClose(); } - crl::on_main([=, shownUrl = _lastShownUrl, bot = _bot] { + crl::on_main([=] { if (bot->session().windows().empty()) { Core::App().domain().activate(&bot->session().account()); } const auto window = !bot->session().windows().empty() ? bot->session().windows().front().get() : nullptr; + context->controller = window; const auto variant = QVariant::fromValue(ClickHandlerContext{ - .attachBotWebviewUrl = shownUrl, .sessionWindow = window, + .botWebviewContext = context, }); UrlClickHandler::Open(local, variant); }); return true; } -void AttachWebView::botHandleInvoice(QString slug) { +void WebViewInstance::botHandleInvoice(QString slug) { Expects(_panel != nullptr); using Result = Payments::CheckoutResult; @@ -614,20 +1163,42 @@ void AttachWebView::botHandleInvoice(QString slug) { }()); } }; - _panel->hideForPayment(); Payments::CheckoutProcess::Start( &_bot->session(), slug, reactivate, - _context - ? Payments::ProcessNonPanelPaymentFormFactory( - _context->controller.get(), - reactivate) - : nullptr); + nonPanelPaymentFormFactory(reactivate)); } -void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { - Expects(_bot != nullptr); +auto WebViewInstance::nonPanelPaymentFormFactory( + Fn reactivate) +-> Fn { + using namespace Payments; + const auto panel = base::make_weak(_panel.get()); + const auto weak = _context.controller; + return [=](Payments::NonPanelPaymentForm form) { + using CreditsFormDataPtr = std::shared_ptr; + using CreditsReceiptPtr = std::shared_ptr; + v::match(form, [&](const CreditsFormDataPtr &form) { + if (const auto strong = panel.get()) { + ProcessCreditsPayment( + uiShow(), + strong->toastParent().get(), + form, + reactivate); + } + }, [&](const CreditsReceiptPtr &receipt) { + if (const auto controller = weak.get()) { + ProcessCreditsReceipt(controller, receipt, reactivate); + } + }, [&](RealFormPresentedNotification) { + _panel->hideForPayment(); + }); + }; +} + +void WebViewInstance::botHandleMenuButton( + Ui::BotWebView::MenuButton button) { Expects(_panel != nullptr); using Button = Ui::BotWebView::MenuButton; @@ -646,17 +1217,23 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { break; case Button::RemoveFromMenu: case Button::RemoveFromMainMenu: + const auto &bots = _session->attachWebView().attachBots(); const auto attached = ranges::find( - _attachBots, - not_null{ _bot }, + bots, + _bot, &AttachWebViewBot::user); - const auto name = (attached != end(_attachBots)) + const auto name = (attached != end(bots)) ? attached->name : _bot->name(); const auto done = crl::guard(this, [=] { - removeFromMenu(bot); + const auto session = _session; + const auto was = _parentShow; botClose(); - if (const auto active = Core::App().activeWindow()) { + + const auto active = Core::App().activeWindow(); + const auto show = active ? active->uiShow() : was; + session->attachWebView().removeFromMenu(show, bot); + if (active) { active->activate(); } }); @@ -675,7 +1252,7 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { } } -bool AttachWebView::botValidateExternalLink(QString uri) { +bool WebViewInstance::botValidateExternalLink(QString uri) { const auto lower = uri.toLower(); const auto allowed = _session->appConfig().get>( "web_app_allowed_protocols", @@ -688,8 +1265,8 @@ bool AttachWebView::botValidateExternalLink(QString uri) { return false; } -void AttachWebView::botOpenIvLink(QString uri) { - const auto window = _context ? _context->controller.get() : nullptr; +void WebViewInstance::botOpenIvLink(QString uri) { + const auto window = _context.controller.get(); if (window) { Core::App().iv().openWithIvPreferred(window, uri); } else { @@ -697,33 +1274,32 @@ void AttachWebView::botOpenIvLink(QString uri) { } } -void AttachWebView::botSendData(QByteArray data) { - if (!_context - || _context->fromSwitch - || _context->fromBotApp - || _context->fromMainMenu - || _context->action.history->peer != _bot - || _lastShownQueryId) { +void WebViewInstance::botSendData(QByteArray data) { + Expects(_context.action.has_value()); + + const auto button = std::get_if(&_source); + if (!button + || !button->simple + || _context.action->history->peer != _bot + || _dataSent) { return; } - const auto randomId = base::RandomValue(); + _dataSent = true; _session->api().request(MTPmessages_SendWebViewData( _bot->inputUser, - MTP_long(randomId), - MTP_string(_lastShownButtonText), + MTP_long(base::RandomValue()), + MTP_string(_button.text), MTP_bytes(data) - )).done([=](const MTPUpdates &result) { - _session->api().applyUpdates(result); + )).done([session = _session](const MTPUpdates &result) { + session->api().applyUpdates(result); }).send(); - crl::on_main(this, [=] { cancel(); }); + botClose(); } -void AttachWebView::botSwitchInlineQuery( +void WebViewInstance::botSwitchInlineQuery( std::vector chatTypes, QString query) { - const auto controller = _context - ? _context->controller.get() - : nullptr; + const auto controller = _context.controller.get(); const auto types = PeerTypesFromNames(chatTypes); if (!_bot || !_bot->isBot() @@ -731,9 +1307,9 @@ void AttachWebView::botSwitchInlineQuery( || !controller) { return; } else if (!types) { - if (_context->dialogsEntryState.key.owningHistory()) { + if (_context.dialogsEntryState.key.owningHistory()) { controller->switchInlineQuery( - _context->dialogsEntryState, + _context.dialogsEntryState, _bot, query); } @@ -748,10 +1324,10 @@ void AttachWebView::botSwitchInlineQuery( done, tr::lng_inline_switch_choose()); } - crl::on_main(this, [=] { cancel(); }); + botClose(); } -void AttachWebView::botCheckWriteAccess(Fn callback) { +void WebViewInstance::botCheckWriteAccess(Fn callback) { _session->api().request(MTPbots_CanSendMessage( _bot->inputUser )).done([=](const MTPBool &result) { @@ -761,46 +1337,42 @@ void AttachWebView::botCheckWriteAccess(Fn callback) { }).send(); } -void AttachWebView::botAllowWriteAccess(Fn callback) { +void WebViewInstance::botAllowWriteAccess(Fn callback) { _session->api().request(MTPbots_AllowSendMessage( _bot->inputUser - )).done([=](const MTPUpdates &result) { - _session->api().applyUpdates(result); + )).done([session = _session, callback](const MTPUpdates &result) { + session->api().applyUpdates(result); callback(true); }).fail([=] { callback(false); }).send(); } -void AttachWebView::botSharePhone(Fn callback) { - const auto bot = _bot; +void WebViewInstance::botSharePhone(Fn callback) { const auto history = _bot->owner().history(_bot); if (_bot->isBlocked()) { - const auto done = [=](bool success) { - if (success && _bot == bot) { - Assert(!_bot->isBlocked()); + const auto done = crl::guard(this, [=](bool success) { + if (success) { botSharePhone(callback); } else { callback(false); } - }; - _bot->session().api().blockedPeers().unblock( - _bot, - crl::guard(this, done)); + }); + _session->api().blockedPeers().unblock(_bot, done); return; } auto action = Api::SendAction(history); action.clearDraft = false; - history->session().api().shareContact( - _bot->session().user(), + _session->api().shareContact( + _session->user(), action, std::move(callback)); } -void AttachWebView::botInvokeCustomMethod( +void WebViewInstance::botInvokeCustomMethod( Ui::BotWebView::CustomMethodRequest request) { const auto callback = request.callback; - _bot->session().api().request(MTPbots_InvokeWebViewCustomMethod( + _session->api().request(MTPbots_InvokeWebViewCustomMethod( _bot->inputUser, MTP_string(request.method), MTP_dataJSON(MTP_bytes(request.params)) @@ -811,138 +1383,178 @@ void AttachWebView::botInvokeCustomMethod( }).send(); } -void AttachWebView::botClose() { - crl::on_main(this, [=] { cancel(); }); +void WebViewInstance::botShareGameScore() { + const auto itemId = v::is(_source) + ? v::get(_source).messageId + : FullMsgId(); + if (!_panel || !itemId) { + return; + } else if (const auto item = _session->data().message(itemId)) { + FastShareMessage(uiShow(), item); + } else { + _panel->showToast({ tr::lng_message_not_found(tr::now) }); + } } -AttachWebView::Context AttachWebView::LookupContext( - not_null controller, - const Api::SendAction &action) { - return { - .controller = controller, - .dialogsEntryState = controller->currentDialogsEntryState(), - .action = action, +void WebViewInstance::botClose() { + crl::on_main(this, [=] { close(); }); +} + +std::shared_ptr WebViewInstance::uiShow() { + class Show final : public Main::SessionShow { + public: + explicit Show(not_null that) : _that(that) { + } + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override { + using UniqueLayer = std::unique_ptr; + using ObjectBox = object_ptr; + const auto panel = _that ? _that->_panel.get() : nullptr; + if (v::is(layer)) { + Unexpected("Layers in WebView are not implemented."); + } else if (auto box = std::get_if(&layer)) { + if (panel) { + panel->showBox(std::move(*box), options, animated); + } + } else if (panel) { + panel->hideLayer(animated); + } + } + [[nodiscard]] not_null toastParent() const override { + const auto panel = _that ? _that->_panel.get() : nullptr; + + Ensures(panel != nullptr); + return panel->toastParent(); + } + [[nodiscard]] bool valid() const override { + return _that && (_that->_panel != nullptr); + } + operator bool() const override { + return valid(); + } + + [[nodiscard]] Main::Session &session() const override { + Expects(_that.get() != nullptr); + + return *_that->_session; + } + + private: + const base::weak_ptr _that; + }; + return std::make_shared(this); } -bool AttachWebView::IsSame( - const std::unique_ptr &a, - const Context &b) { - // Check fields that are sent to API in bot attach webview requests. - return a - && (a->controller == b.controller) - && (a->dialogsEntryState == b.dialogsEntryState) - && (a->fromSwitch == b.fromSwitch) - && (a->fromMainMenu == b.fromMainMenu) - && (a->action.history == b.action.history) - && (a->action.replyTo == b.action.replyTo) - && (a->action.options.sendAs == b.action.options.sendAs) - && (a->action.options.silent == b.action.options.silent); +AttachWebView::AttachWebView(not_null session) +: _session(session) +, _refreshTimer([=] { requestBots(); }) { + _refreshTimer.callEach(kRefreshBotsTimeout); } -void AttachWebView::request( +AttachWebView::~AttachWebView() { + closeAll(); + _session->api().request(_popularAppBotsRequestId).cancel(); +} + +void AttachWebView::openByUsername( not_null controller, const Api::SendAction &action, - not_null bot, - const WebViewButton &button) { - requestWithOptionalConfirm( - bot, - button, - LookupContext(controller, action), - button.fromAttachMenu ? nullptr : controller.get()); -} - -void AttachWebView::requestWithOptionalConfirm( - not_null bot, - const WebViewButton &button, - const Context &context, - Window::SessionController *controllerForConfirm) { - if (IsSame(_context, context) && _bot == bot) { - if (_panel) { - _panel->requestActivate(); - } else if (_requestId) { - return; - } + const QString &botUsername, + const QString &startCommand) { + if (botUsername.isEmpty() + || (_botUsername == botUsername && _startCommand == startCommand)) { + return; } cancel(); - _bot = bot; - _context = std::make_unique(context); - if (controllerForConfirm) { - confirmOpen(controllerForConfirm, [=] { - request(button); + _botUsername = botUsername; + _startCommand = startCommand; + const auto weak = base::make_weak(controller); + const auto show = controller->uiShow(); + resolveUsername(show, crl::guard(weak, [=](not_null peer) { + _botUsername = QString(); + const auto token = base::take(_startCommand); + + const auto bot = peer->asUser(); + if (!bot || !bot->isBot()) { + if (const auto strong = weak.get()) { + strong->showToast(tr::lng_bot_menu_not_supported(tr::now)); + } + return; + } + + open({ + .bot = bot, + .context = { + .controller = controller, + .action = action, + }, + .button = { .startCommand = token }, + .source = InlineBots::WebViewSourceLinkAttachMenu{}, }); - } else { - request(button); + })); +} + +void AttachWebView::close(not_null instance) { + const auto i = ranges::find( + _instances, + instance.get(), + &std::unique_ptr::get); + if (i != end(_instances)) { + const auto taken = base::take(*i); + _instances.erase(i); } } -void AttachWebView::request(const WebViewButton &button) { - Expects(_context != nullptr && _bot != nullptr); +void AttachWebView::closeAll() { + cancel(); + base::take(_instances); +} - if (button.fromAttachMenu) { - const auto bot = ranges::find( - _attachBots, - not_null{ _bot }, - &AttachWebViewBot::user); - if (bot == end(_attachBots) || bot->inactive) { - requestAddToMenu(_bot, AddToMenuOpenAttach{ - .startCommand = button.startCommand, - }); - return; - } +void AttachWebView::loadPopularAppBots() { + if (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) { + return; } + _popularAppBotsRequestId = _session->api().request( + MTPbots_GetPopularAppBots( + MTP_string(), + MTP_int(kPopularAppBotsLimit)) + ).done([=](const MTPbots_PopularAppBots &result) { + _popularAppBotsRequestId = 0; - _startCommand = button.startCommand; - const auto &action = _context->action; - - using Flag = MTPmessages_RequestWebView::Flag; - const auto flags = Flag::f_theme_params - | (button.url.isEmpty() ? Flag(0) : Flag::f_url) - | (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param) - | (action.replyTo ? Flag::f_reply_to : Flag(0)) - | (action.options.sendAs ? Flag::f_send_as : Flag(0)) - | (action.options.silent ? Flag::f_silent : Flag(0)); - _requestId = _session->api().request(MTPmessages_RequestWebView( - MTP_flags(flags), - action.history->peer->input, - _bot->inputUser, - MTP_bytes(button.url), - MTP_string(_startCommand), - MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), - MTP_string(WebviewPlatform()), - action.mtpReplyTo(), - (action.options.sendAs - ? action.options.sendAs->input - : MTP_inputPeerEmpty()) - )).done([=](const MTPWebViewResult &result) { - _requestId = 0; - const auto &data = result.data(); - show( - data.vquery_id().value_or_empty(), - qs(data.vurl()), - button.text, - button.fromAttachMenu || button.url.isEmpty()); - }).fail([=](const MTP::Error &error) { - _requestId = 0; - if (error.type() == u"BOT_INVALID"_q) { - requestBots(); + const auto &list = result.data().vusers().v; + auto parsed = std::vector>(); + parsed.reserve(list.size()); + for (const auto &user : list) { + const auto bot = _session->data().processUser(user); + if (bot->isBot()) { + parsed.push_back(bot); + } } + _popularAppBots = std::move(parsed); + _popularAppBotsLoaded = true; }).send(); } -void AttachWebView::cancel() { - Expects(!_catchingCancelInShowCall); +auto AttachWebView::popularAppBots() const +-> const std::vector> & { + return _popularAppBots; +} - ActiveWebViews().remove(this); +rpl::producer<> AttachWebView::popularAppBotsLoaded() const { + return _popularAppBotsLoaded.changes() | rpl::to_empty; +} + +void AttachWebView::cancel() { _session->api().request(base::take(_requestId)).cancel(); - _session->api().request(base::take(_prolongId)).cancel(); - base::take(_panel); - _lastShownContext = base::take(_context); - _bot = nullptr; - _app = nullptr; _botUsername = QString(); - _botAppName = QString(); _startCommand = QString(); } @@ -994,190 +1606,83 @@ bool AttachWebView::showMainMenuNewBadge( void AttachWebView::requestAddToMenu( not_null bot, - AddToMenuOpen open) { - requestAddToMenu(bot, open, nullptr, std::nullopt); -} - -void AttachWebView::requestAddToMenu( - not_null bot, - AddToMenuOpen open, - Window::SessionController *controller, - std::optional action) { - Expects(controller != nullptr || _context != nullptr); - - const auto wasController = (controller != nullptr); - _addToMenuChooseController = base::make_weak(controller); - _addToMenuOpen = open; - if (!controller) { - _addToMenuContext = base::take(_context); - } else if (action) { - _addToMenuContext = std::make_unique( - LookupContext(controller, *action)); + Fn done) { + auto &process = _addToMenu[bot]; + if (done) { + process.done.push_back(std::move(done)); } - - const auto unsupported = [=] { - auto context = base::take(_addToMenuContext); - const auto open = base::take(_addToMenuOpen); - if (const auto openApp = std::get_if(&open)) { - _app = openApp->app; - _startCommand = openApp->startCommand; - _context = std::move(context); - if (_appConfirmationRequired) { - confirmAppOpen(_appRequestWriteAccess); - } else { - requestAppView(false); - } - } else { - showToast( - tr::lng_bot_menu_not_supported(tr::now), - _addToMenuChooseController.get()); - } - }; - if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) { - unsupported(); + if (process.requestId) { return; } - if (_addToMenuId) { - if (_addToMenuBot == bot) { + const auto finish = [=](AddToMenuResult result, PeerTypes supported) { + if (auto process = _addToMenu.take(bot)) { + for (const auto &done : process->done) { + done(result, supported); + } + } + }; + if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) { + finish(AddToMenuResult::Unsupported, {}); + return; + } + + process.requestId = _session->api().request( + MTPmessages_GetAttachMenuBot(bot->inputUser) + ).done([=](const MTPAttachMenuBotsBot &result) { + _addToMenu[bot].requestId = 0; + const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + const auto parsed = ParseAttachBot(_session, data.vbot()); + if (!parsed || bot != parsed->user) { + finish(AddToMenuResult::Unsupported, {}); return; } - _session->api().request(base::take(_addToMenuId)).cancel(); - } - _addToMenuBot = bot; - _addToMenuId = _session->api().request(MTPmessages_GetAttachMenuBot( - bot->inputUser - )).done([=](const MTPAttachMenuBotsBot &result) { - _addToMenuId = 0; - const auto bot = base::take(_addToMenuBot); - const auto context = std::shared_ptr(base::take(_addToMenuContext)); - const auto open = base::take(_addToMenuOpen); - const auto chooseController = base::take(_addToMenuChooseController); - const auto launch = [=](PeerTypes types) { - const auto openAttach = v::is(open) - ? v::get(open) - : AddToMenuOpenAttach(); - const auto chooseTypes = openAttach.chooseTypes; - const auto strong = chooseController.get(); - if (v::is(open)) { - if (!context) { - return false; - } - const auto &openApp = v::get(open); - _app = openApp.app; - _startCommand = openApp.startCommand; - _context = std::make_unique(*context); - requestAppView(true); - return true; - } else if (!strong) { - if (wasController || !v::is(open)) { - // Just ignore the click if controller was destroyed. - return true; - } - } else if (v::is(open)) { - const auto &openMenu = v::get(open); - _bot = bot; - requestSimple(strong, bot, { - .startCommand = openMenu.startCommand, - .fromMainMenu = true, - }); - return true; - } else if (const auto useTypes = chooseTypes & types) { - const auto done = [=](not_null thread) { - strong->showThread(thread); - requestWithOptionalConfirm( - bot, - { .startCommand = openAttach.startCommand }, - LookupContext(strong, Api::SendAction(thread))); - }; - ShowChooseBox(strong, useTypes, done); - return true; - } - if (!context) { - return false; - } - requestWithOptionalConfirm( - bot, - { .startCommand = openAttach.startCommand }, - *context); - return true; - }; - result.match([&](const MTPDattachMenuBotsBot &data) { - _session->data().processUsers(data.vusers()); - if (const auto parsed = ParseAttachBot(_session, data.vbot())) { - if (bot == parsed->user) { - const auto i = ranges::find( - _attachBots, - not_null(bot), - &AttachWebViewBot::user); - if (i != end(_attachBots)) { - // Save flags in our list, like 'inactive'. - *i = *parsed; - } - const auto types = parsed->types; - if (parsed->inactive) { - confirmAddToMenu(*parsed, [=] { - launch(types); - }); - } else { - requestBots(); - if (!launch(types)) { - showToast( - tr::lng_bot_menu_already_added(tr::now)); - } - } - } - } - }); + const auto i = ranges::find( + _attachBots, + not_null(bot), + &AttachWebViewBot::user); + if (i != end(_attachBots)) { + // Save flags in our list, like 'inactive'. + *i = *parsed; + } + const auto types = parsed->types; + if (parsed->inactive) { + confirmAddToMenu(*parsed, [=](bool added) { + const auto result = added + ? AddToMenuResult::Added + : AddToMenuResult::Cancelled; + finish(result, types); + }); + } else { + requestBots(); + finish(AddToMenuResult::AlreadyInMenu, types); + } }).fail([=] { - _addToMenuId = 0; - _addToMenuBot = nullptr; - unsupported(); + finish(AddToMenuResult::Unsupported, {}); }).send(); } -void AttachWebView::removeFromMenu(not_null bot) { - toggleInMenu(bot, ToggledState::Removed, [=] { - showToast(tr::lng_bot_remove_from_menu_done(tr::now)); - }); -} - -std::optional AttachWebView::lookupLastAction( - const QString &url) const { - if (_lastShownUrl == url && _lastShownContext) { - return _lastShownContext->action; - } - return std::nullopt; -} - -void AttachWebView::resolve() { - Expects(!_panel); - - resolveUsername(_botUsername, [=](not_null bot) { - if (!_context) { - return; +void AttachWebView::removeFromMenu( + std::shared_ptr show, + not_null bot) { + toggleInMenu(bot, ToggledState::Removed, [=](bool success) { + if (success) { + show->showToast(tr::lng_bot_remove_from_menu_done(tr::now)); } - _bot = bot->asUser(); - if (!_bot) { - showToast(tr::lng_bot_menu_not_supported(tr::now)); - return; - } - requestAddToMenu(_bot, AddToMenuOpenAttach{ - .startCommand = _startCommand, - }); }); } void AttachWebView::resolveUsername( - const QString &username, + std::shared_ptr show, Fn)> done) { - if (const auto peer = _session->data().peerByUsername(username)) { + if (const auto peer = _session->data().peerByUsername(_botUsername)) { done(peer); return; } _session->api().request(base::take(_requestId)).cancel(); _requestId = _session->api().request(MTPcontacts_ResolveUsername( - MTP_string(username) + MTP_string(_botUsername) )).done([=](const MTPcontacts_ResolvedPeer &result) { _requestId = 0; result.match([&](const MTPDcontacts_resolvedPeer &data) { @@ -1190,449 +1695,64 @@ void AttachWebView::resolveUsername( }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.code() == 400) { - showToast( - tr::lng_username_not_found(tr::now, lt_user, username)); + show->showToast( + tr::lng_username_not_found(tr::now, lt_user, _botUsername)); } }).send(); } -void AttachWebView::requestSimple( - not_null controller, - not_null bot, - const WebViewButton &button) { - cancel(); - _bot = bot; - _context = std::make_unique(LookupContext( - controller, - Api::SendAction(bot->owner().history(bot)))); - _context->fromSwitch = button.fromSwitch; - _context->fromMainMenu = button.fromMainMenu; - if (button.fromMainMenu) { - acceptMainMenuDisclaimer(controller, button); - } else { - confirmOpen(controller, [=] { - requestSimple(button); - }); - } -} - -void AttachWebView::requestSimple(const WebViewButton &button) { - using Flag = MTPmessages_RequestSimpleWebView::Flag; - _requestId = _session->api().request(MTPmessages_RequestSimpleWebView( - MTP_flags(Flag::f_theme_params - | (button.fromMainMenu - ? (Flag::f_from_side_menu - | (button.startCommand.isEmpty() - ? Flag() - : Flag::f_start_param)) - : Flag::f_url) - | (button.fromSwitch ? Flag::f_from_switch_webview : Flag())), - _bot->inputUser, - MTP_bytes(button.url), - MTP_string(button.startCommand), - MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), - MTP_string(WebviewPlatform()) - )).done([=](const MTPWebViewResult &result) { - _requestId = 0; - const auto &data = result.data(); - const auto queryId = uint64(); - show( - queryId, - qs(data.vurl()), - button.text, - false, - nullptr, - button.fromMainMenu); - }).fail([=](const MTP::Error &error) { - _requestId = 0; - }).send(); -} - -bool AttachWebView::openAppFromMenuLink( - not_null controller, - not_null bot) { - Expects(bot->botInfo != nullptr); - - const auto &url = bot->botInfo->botMenuButtonUrl; - const auto local = Core::TryConvertUrlToLocal(url); - const auto prefix = u"tg://resolve?"_q; - if (!local.startsWith(prefix)) { - return false; - } - const auto params = qthelp::url_parse_params( - local.mid(prefix.size()), - qthelp::UrlParamNameTransform::ToLower); - const auto domainParam = params.value(u"domain"_q); - const auto appnameParam = params.value(u"appname"_q); - const auto webChannelPreviewLink = (domainParam == u"s"_q) - && !appnameParam.isEmpty(); - const auto appname = webChannelPreviewLink ? QString() : appnameParam; - if (appname.isEmpty()) { - return false; - } - requestApp( - controller, - Api::SendAction(bot->owner().history(bot)), - bot, - appname, - params.value(u"startapp"_q), - true); - return true; -} - -void AttachWebView::requestMenu( - not_null controller, - not_null bot) { - if (openAppFromMenuLink(controller, bot)) { - return; - } - - cancel(); - _bot = bot; - _context = std::make_unique(LookupContext( - controller, - Api::SendAction(bot->owner().history(bot)))); - const auto url = bot->botInfo->botMenuButtonUrl; - const auto text = bot->botInfo->botMenuButtonText; - confirmOpen(controller, [=] { - const auto &action = _context->action; - using Flag = MTPmessages_RequestWebView::Flag; - _requestId = _session->api().request(MTPmessages_RequestWebView( - MTP_flags(Flag::f_theme_params - | Flag::f_url - | Flag::f_from_bot_menu - | (action.replyTo? Flag::f_reply_to : Flag(0)) - | (action.options.sendAs ? Flag::f_send_as : Flag(0)) - | (action.options.silent ? Flag::f_silent : Flag(0))), - action.history->peer->input, - _bot->inputUser, - MTP_string(url), - MTPstring(), // start_param - MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), - MTP_string(WebviewPlatform()), - action.mtpReplyTo(), - (action.options.sendAs - ? action.options.sendAs->input - : MTP_inputPeerEmpty()) - )).done([=](const MTPWebViewResult &result) { - _requestId = 0; - const auto &data = result.data(); - show(data.vquery_id().value_or_empty(), qs(data.vurl()), text); - }).fail([=](const MTP::Error &error) { - _requestId = 0; - if (error.type() == u"BOT_INVALID"_q) { - requestBots(); - } - }).send(); - }); -} - -void AttachWebView::requestApp( - not_null controller, - const Api::SendAction &action, - not_null bot, - const QString &appName, - const QString &startParam, - bool forceConfirmation) { - const auto context = LookupContext(controller, action); - if (_requestId - && _bot == bot - && _startCommand == startParam - && _botAppName == appName - && IsSame(_context, context)) { - return; - } - cancel(); - _bot = bot; - _startCommand = startParam; - _botAppName = appName; - _context = std::make_unique(context); - _context->fromBotApp = true; - const auto already = _session->data().findBotApp(_bot->id, appName); - _requestId = _session->api().request(MTPmessages_GetBotApp( - MTP_inputBotAppShortName( - bot->inputUser, - MTP_string(appName)), - MTP_long(already ? already->hash : 0) - )).done([=](const MTPmessages_BotApp &result) { - _requestId = 0; - if (!_bot || !_context) { +void AttachWebView::open(WebViewDescriptor &&descriptor) { + for (const auto &instance : _instances) { + if (instance->bot() == descriptor.bot + && instance->source() == descriptor.source) { + instance->activate(); return; } - const auto &data = result.data(); - const auto firstTime = data.is_inactive(); - const auto received = _session->data().processBotApp( - _bot->id, - data.vapp()); - _app = received ? received : already; - if (!_app) { - cancel(); - showToast(tr::lng_username_app_not_found(tr::now)); - return; - } - // Check if this app can be added to main menu. - // On fail it'll still be opened. - _appConfirmationRequired = firstTime || forceConfirmation; - _appRequestWriteAccess = result.data().is_request_write_access(); - requestAddToMenu(_bot, AddToMenuOpenApp{ - .app = _app, - .startCommand = _startCommand, - }); - }).fail([=] { - showToast(tr::lng_username_app_not_found(tr::now)); - cancel(); - }).send(); -} - -void AttachWebView::confirmAppOpen(bool requestWriteAccess) { - const auto controller = _context ? _context->controller.get() : nullptr; - if (!controller || !_bot) { - return; } - controller->show(Box([=](not_null box) { - const auto allowed = std::make_shared(); - const auto done = [=](Fn close) { - requestAppView((*allowed) && (*allowed)->checked()); - close(); - }; - Ui::ConfirmBox(box, { - tr::lng_allow_bot_webview( - tr::now, - lt_bot_name, - Ui::Text::Bold(_bot->name()), - Ui::Text::RichLangValue), - done, - }); - if (requestWriteAccess) { - (*allowed) = box->addRow( - object_ptr( - box, - tr::lng_url_auth_allow_messages( - tr::now, - lt_bot, - Ui::Text::Bold(_bot->name()), - Ui::Text::WithEntities), - true, - st::urlAuthCheckbox), - style::margins( - st::boxRowPadding.left(), - st::boxPhotoCaptionSkip, - st::boxRowPadding.right(), - st::boxPhotoCaptionSkip)); - (*allowed)->setAllowTextLines(); - } - })); -} - -void AttachWebView::requestAppView(bool allowWrite) { - if (!_context || !_app) { - return; - } - using Flag = MTPmessages_RequestAppWebView::Flag; - const auto app = _app; - const auto flags = Flag::f_theme_params - | (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param) - | (allowWrite ? Flag::f_write_allowed : Flag(0)); - _requestId = _session->api().request(MTPmessages_RequestAppWebView( - MTP_flags(flags), - _context->action.history->peer->input, - MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)), - MTP_string(_startCommand), - MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), - MTP_string(WebviewPlatform()) - )).done([=](const MTPWebViewResult &result) { - _requestId = 0; - const auto &data = result.data(); - const auto queryId = uint64(); - show(queryId, qs(data.vurl()), QString(), false, app); - }).fail([=](const MTP::Error &error) { - _requestId = 0; - if (error.type() == u"BOT_INVALID"_q) { - requestBots(); - } - }).send(); -} - -void AttachWebView::confirmOpen( - not_null controller, - Fn done) { - if (!_bot) { - return; - } else if (_bot->isVerified() - || _bot->session().local().isBotTrustedOpenWebView(_bot->id)) { - done(); - return; - } - const auto callback = [=] { - _bot->session().local().markBotTrustedOpenWebView(_bot->id); - controller->hideLayer(); - done(); - }; - controller->show(Ui::MakeConfirmBox({ - .text = tr::lng_allow_bot_webview( - tr::now, - lt_bot_name, - Ui::Text::Bold(_bot->name()), - Ui::Text::RichLangValue), - .confirmed = callback, - .confirmText = tr::lng_box_ok(), - })); + _instances.push_back( + std::make_unique(std::move(descriptor))); + _instances.back()->activate(); } void AttachWebView::acceptMainMenuDisclaimer( - not_null controller, - const WebViewButton &button) { - Expects(button.fromMainMenu); - - const auto local = _bot ? &_bot->session().local() : nullptr; - if (!local) { - return; - } - const auto i = ranges::find( - _attachBots, - not_null(_bot), - &AttachWebViewBot::user); + std::shared_ptr show, + not_null bot, + Fn done) { + const auto i = ranges::find(_attachBots, bot, &AttachWebViewBot::user); if (i == end(_attachBots)) { _attachBotsUpdates.fire({}); return; } else if (i->inactive) { - requestAddToMenu(_bot, AddToMenuOpenMenu{ - .startCommand = button.startCommand, - }, controller, {}); + requestAddToMenu(bot, std::move(done)); return; } else if (!i->disclaimerRequired || disclaimerAccepted(*i)) { - requestSimple(button); + done(AddToMenuResult::AlreadyInMenu, i->types); return; } - - const auto weak = base::make_weak(this); - controller->show(Box(FillDisclaimerBox, crl::guard(this, [=] { - _disclaimerAccepted.emplace(_bot); - _attachBotsUpdates.fire({}); - requestSimple(button); + const auto types = i->types; + show->show(Box(FillDisclaimerBox, crl::guard(this, [=](bool accepted) { + if (accepted) { + _disclaimerAccepted.emplace(bot); + _attachBotsUpdates.fire({}); + done(AddToMenuResult::AlreadyInMenu, types); + } else { + done(AddToMenuResult::Cancelled, {}); + } }))); } -void AttachWebView::ClearAll() { - while (!ActiveWebViews().empty()) { - ActiveWebViews().front()->cancel(); - } -} - -void AttachWebView::show( - uint64 queryId, - const QString &url, - const QString &buttonText, - bool allowClipboardRead, - const BotAppData *app, - bool fromMainMenu) { - Expects(_bot != nullptr && _context != nullptr); - - auto title = Info::Profile::NameValue(_bot); - ActiveWebViews().emplace(this); - - using Button = Ui::BotWebView::MenuButton; - const auto attached = ranges::find( - _attachBots, - not_null{ _bot }, - &AttachWebViewBot::user); - const auto hasOpenBot = !_context - || (_bot != _context->action.history->peer) - || fromMainMenu; - const auto hasRemoveFromMenu = !app - && (attached != end(_attachBots)) - && (!attached->inactive || attached->inMainMenu); - const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None) - | (!hasRemoveFromMenu - ? Button::None - : attached->inMainMenu - ? Button::RemoveFromMainMenu - : Button::RemoveFromMenu); - if (attached != end(_attachBots) - && (attached->inAttachMenu || attached->inMainMenu)) { - allowClipboardRead = true; - } - - _lastShownUrl = url; - _lastShownQueryId = queryId; - _lastShownButtonText = buttonText; - base::take(_panel); - _catchingCancelInShowCall = true; - _panel = Ui::BotWebView::Show({ - .url = url, - .storageId = _session->local().resolveStorageIdBots(), - .title = std::move(title), - .bottom = rpl::single('@' + _bot->username()), - .delegate = static_cast(this), - .menuButtons = buttons, - .allowClipboardRead = allowClipboardRead, - }); - _catchingCancelInShowCall = false; - started(queryId); -} - -void AttachWebView::started(uint64 queryId) { - Expects(_bot != nullptr); - Expects(_context != nullptr); - - if (_context->fromSwitch || !queryId) { - return; - } - - _session->data().webViewResultSent( - ) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) { - return (sent.queryId == queryId); - }) | rpl::start_with_next([=] { - cancel(); - }, _panel->lifetime()); - - const auto action = _context->action; - base::timer_each( - kProlongTimeout - ) | rpl::start_with_next([=] { - using Flag = MTPmessages_ProlongWebView::Flag; - _session->api().request(base::take(_prolongId)).cancel(); - _prolongId = _session->api().request(MTPmessages_ProlongWebView( - MTP_flags(Flag(0) - | (action.replyTo ? Flag::f_reply_to : Flag(0)) - | (action.options.sendAs ? Flag::f_send_as : Flag(0)) - | (action.options.silent ? Flag::f_silent : Flag(0))), - action.history->peer->input, - _bot->inputUser, - MTP_long(queryId), - action.mtpReplyTo(), - (action.options.sendAs - ? action.options.sendAs->input - : MTP_inputPeerEmpty()) - )).done([=] { - _prolongId = 0; - }).send(); - }, _panel->lifetime()); -} - -void AttachWebView::showToast( - const QString &text, - Window::SessionController *controller) { - const auto strong = controller - ? controller - : _context - ? _context->controller.get() - : _addToMenuContext - ? _addToMenuContext->controller.get() - : nullptr; - if (strong) { - strong->showToast(text); - } -} - void AttachWebView::confirmAddToMenu( AttachWebViewBot bot, - Fn callback) { + Fn callback) { const auto active = Core::App().activeWindow(); if (!active) { + if (callback) { + callback(false); + } return; } - _confirmAddBox = active->show(Box([=](not_null box) { + const auto weak = base::make_weak(active); + active->show(Box([=](not_null box) { const auto allowed = std::make_shared(); const auto disclaimer = !disclaimerAccepted(bot); const auto done = [=](Fn close) { @@ -1640,21 +1760,27 @@ void AttachWebView::confirmAddToMenu( || ((*allowed) && (*allowed)->checked())) ? ToggledState::AllowedToWrite : ToggledState::Added; - toggleInMenu(bot.user, state, [=] { + toggleInMenu(bot.user, state, [=](bool success) { if (callback) { - callback(); + callback(success); + } + if (const auto strong = weak.get()) { + strong->showToast((bot.inMainMenu + ? tr::lng_bot_add_to_side_menu_done + : tr::lng_bot_add_to_menu_done)(tr::now)); } - showToast((bot.inMainMenu - ? tr::lng_bot_add_to_side_menu_done - : tr::lng_bot_add_to_menu_done)(tr::now)); }); close(); }; if (disclaimer) { - FillDisclaimerBox(box, [=] { - _disclaimerAccepted.emplace(bot.user); - _attachBotsUpdates.fire({}); - done([] {}); + FillDisclaimerBox(box, [=](bool accepted) { + if (accepted) { + _disclaimerAccepted.emplace(bot.user); + _attachBotsUpdates.fire({}); + done([] {}); + } else if (callback) { + callback(false); + } }); box->addRow(object_ptr( box, @@ -1676,6 +1802,9 @@ void AttachWebView::confirmAddToMenu( Ui::Text::Bold(bot.name), Ui::Text::WithEntities), done, + (callback + ? [=](Fn close) { callback(false); close(); } + : Fn)>()), }); if (bot.requestWriteAccess) { (*allowed) = box->addRow( @@ -1704,7 +1833,7 @@ void AttachWebView::confirmAddToMenu( void AttachWebView::toggleInMenu( not_null bot, ToggledState state, - Fn callback) { + Fn callback) { using Flag = MTPmessages_ToggleBotInAttachMenu::Flag; _session->api().request(MTPmessages_ToggleBotInAttachMenu( MTP_flags((state == ToggledState::AllowedToWrite) @@ -1715,12 +1844,45 @@ void AttachWebView::toggleInMenu( )).done([=] { _requestId = 0; _session->api().request(base::take(_botsRequestId)).cancel(); - requestBots(std::move(callback)); + requestBots(callback ? [=] { callback(true); } : Fn()); }).fail([=] { cancel(); + if (callback) { + callback(false); + } }).send(); } +void ChooseAndSendLocation( + not_null controller, + const Ui::LocationPickerConfig &config, + Api::SendAction action) { + const auto session = &controller->session(); + if (const auto picker = session->locationPickers().lookup(action)) { + picker->activate(); + return; + } + const auto callback = [=](Data::InputVenue venue) { + if (venue.justLocation()) { + Api::SendLocation(action, venue.lat, venue.lon); + } else { + Api::SendVenue(action, venue); + } + }; + const auto picker = Ui::LocationPicker::Show({ + .parent = controller->widget(), + .config = config, + .chooseLabel = tr::lng_maps_point_send(), + .recipient = action.history->peer, + .session = session, + .callback = crl::guard(session, callback), + .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, + .storageId = session->local().resolveStorageIdBots(), + .closeRequests = controller->content()->death(), + }); + session->locationPickers().emplace(action, picker); +} + std::unique_ptr MakeAttachBotsMenu( not_null parent, not_null controller, @@ -1775,20 +1937,33 @@ std::unique_ptr MakeAttachBotsMenu( { sendMenuType }); }, &st::menuIconCreatePoll); } + const auto session = &controller->session(); + const auto locationType = ChatRestriction::SendOther; + const auto config = ResolveMapsConfig(session); + if (Data::CanSendAnyOf(peer, locationType) + && Ui::LocationPicker::Available(config)) { + raw->addAction(tr::lng_maps_point(tr::now), [=] { + ChooseAndSendLocation(controller, config, actionFactory()); + }, &st::menuIconAddress); + } for (const auto &bot : bots->attachBots()) { if (!bot.inAttachMenu || !PeerMatchesTypes(peer, bot.user, bot.types)) { continue; } const auto callback = [=] { - bots->request( - controller, - actionFactory(), - bot.user, - { .fromAttachMenu = true }); + bots->open({ + .bot = bot.user, + .context = { + .controller = controller, + .action = actionFactory(), + }, + .source = InlineBots::WebViewSourceAttachMenu(), + }); }; auto action = base::make_unique_q( raw, + controller->uiShow(), raw->menu()->st(), bot, callback); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 0f8cb6e13..4a4f5ca08 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -10,15 +10,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flags.h" #include "base/timer.h" #include "base/weak_ptr.h" +#include "dialogs/dialogs_key.h" +#include "api/api_common.h" #include "mtproto/sender.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/rp_widget.h" -namespace Api { -struct SendAction; -} // namespace Api +namespace Data { +class Thread; +} // namespace Data namespace Ui { +class Show; class GenericBox; class DropdownMenu; } // namespace Ui @@ -29,6 +32,7 @@ class Panel; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Window { @@ -39,8 +43,15 @@ namespace Data { class DocumentMedia; } // namespace Data +namespace Payments { +struct NonPanelPaymentForm; +enum class CheckoutResult; +} // namespace Payments + namespace InlineBots { +class WebViewInstance; + enum class PeerType : uint8 { SameBot = 0x01, Bot = 0x02, @@ -80,50 +91,225 @@ struct AddToMenuOpenApp { not_null app; QString startCommand; }; -using AddToMenuOpen = std::variant< +struct AddToMenuOpen : std::variant< AddToMenuOpenAttach, AddToMenuOpenMenu, - AddToMenuOpenApp>; + AddToMenuOpenApp> { + using variant::variant; +}; -class AttachWebView final +struct WebViewSourceButton { + bool simple = false; + + friend inline bool operator==( + WebViewSourceButton, + WebViewSourceButton) = default; +}; + +struct WebViewSourceSwitch { + friend inline bool operator==( + const WebViewSourceSwitch &, + const WebViewSourceSwitch &) = default; +}; + +struct WebViewSourceLinkApp { // t.me/botusername/appname + base::weak_ptr from; + QString appname; + QString token; + + friend inline bool operator==( + const WebViewSourceLinkApp &, + const WebViewSourceLinkApp &) = default; +}; + +struct WebViewSourceLinkAttachMenu { // ?startattach + base::weak_ptr from; + base::weak_ptr thread; + PeerTypes choose; + QString token; + + friend inline bool operator==( + const WebViewSourceLinkAttachMenu &, + const WebViewSourceLinkAttachMenu &) = default; +}; + +struct WebViewSourceLinkBotProfile { // t.me/botusername?startapp + base::weak_ptr from; + QString token; + bool compact = false; + + friend inline bool operator==( + const WebViewSourceLinkBotProfile &, + const WebViewSourceLinkBotProfile &) = default; +}; + +struct WebViewSourceMainMenu { + friend inline bool operator==( + WebViewSourceMainMenu, + WebViewSourceMainMenu) = default; +}; + +struct WebViewSourceAttachMenu { + base::weak_ptr thread; + + friend inline bool operator==( + const WebViewSourceAttachMenu &, + const WebViewSourceAttachMenu &) = default; +}; + +struct WebViewSourceBotMenu { + friend inline bool operator==( + WebViewSourceBotMenu, + WebViewSourceBotMenu) = default; +}; + +struct WebViewSourceGame { + FullMsgId messageId; + QString title; + + friend inline bool operator==( + WebViewSourceGame, + WebViewSourceGame) = default; +}; + +struct WebViewSourceBotProfile { + friend inline bool operator==( + WebViewSourceBotProfile, + WebViewSourceBotProfile) = default; +}; + +struct WebViewSource : std::variant< + WebViewSourceButton, + WebViewSourceSwitch, + WebViewSourceLinkApp, + WebViewSourceLinkAttachMenu, + WebViewSourceLinkBotProfile, + WebViewSourceMainMenu, + WebViewSourceAttachMenu, + WebViewSourceBotMenu, + WebViewSourceGame, + WebViewSourceBotProfile> { + using variant::variant; +}; + +struct WebViewButton { + QString text; + QString startCommand; + QByteArray url; + bool fromAttachMenu = false; + bool fromMainMenu = false; + bool fromSwitch = false; +}; + +struct WebViewContext { + base::weak_ptr controller; + Dialogs::EntryState dialogsEntryState; + std::optional action; + bool maySkipConfirmation = false; +}; + +struct WebViewDescriptor { + not_null bot; + std::shared_ptr parentShow; + WebViewContext context; + WebViewButton button; + WebViewSource source; +}; + +class WebViewInstance final : public base::has_weak_ptr , public Ui::BotWebView::Delegate { +public: + explicit WebViewInstance(WebViewDescriptor &&descriptor); + ~WebViewInstance(); + + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null bot() const; + [[nodiscard]] WebViewSource source() const; + + void activate(); + void close(); + + [[nodiscard]] std::shared_ptr uiShow(); + +private: + void resolve(); + + bool openAppFromBotMenuLink(); + + void requestButton(); + void requestSimple(); + void requestMain(); + void requestApp(bool allowWrite); + void requestWithMainMenuDisclaimer(); + void requestWithMenuAdd(); + void maybeChooseAndRequestButton(PeerTypes supported); + + void resolveApp( + const QString &appname, + const QString &startparam, + bool forceConfirmation); + void confirmOpen(Fn done); + void confirmAppOpen(bool writeAccess, Fn done); + + void show(const QString &url, uint64 queryId = 0); + void showGame(); + void started(uint64 queryId); + + auto nonPanelPaymentFormFactory( + Fn reactivate) + -> Fn; + + Webview::ThemeParams botThemeParams() override; + bool botHandleLocalUri(QString uri, bool keepOpen) override; + void botHandleInvoice(QString slug) override; + void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; + bool botValidateExternalLink(QString uri) override; + void botOpenIvLink(QString uri) override; + void botSendData(QByteArray data) override; + void botSwitchInlineQuery( + std::vector chatTypes, + QString query) override; + void botCheckWriteAccess(Fn callback) override; + void botAllowWriteAccess(Fn callback) override; + void botSharePhone(Fn callback) override; + void botInvokeCustomMethod( + Ui::BotWebView::CustomMethodRequest request) override; + void botShareGameScore() override; + void botClose() override; + + const std::shared_ptr _parentShow; + const not_null _session; + const not_null _bot; + const WebViewContext _context; + const WebViewButton _button; + const WebViewSource _source; + + BotAppData *_app = nullptr; + QString _appStartParam; + bool _dataSent = false; + + mtpRequestId _requestId = 0; + mtpRequestId _prolongId = 0; + + QString _panelUrl; + std::unique_ptr _panel; + + static base::weak_ptr PendingActivation; + +}; + +class AttachWebView final : public base::has_weak_ptr { public: explicit AttachWebView(not_null session); ~AttachWebView(); - struct WebViewButton { - QString text; - QString startCommand; - QByteArray url; - bool fromAttachMenu = false; - bool fromMainMenu = false; - bool fromSwitch = false; - }; - void request( + void open(WebViewDescriptor &&descriptor); + void openByUsername( not_null controller, const Api::SendAction &action, const QString &botUsername, const QString &startCommand); - void request( - not_null controller, - const Api::SendAction &action, - not_null bot, - const WebViewButton &button); - void requestSimple( - not_null controller, - not_null bot, - const WebViewButton &button); - void requestMenu( - not_null controller, - not_null bot); - void requestApp( - not_null controller, - const Api::SendAction &action, - not_null bot, - const QString &appName, - const QString &startParam, - bool forceConfirmation); void cancel(); @@ -142,72 +328,37 @@ public: [[nodiscard]] bool showMainMenuNewBadge( const AttachWebViewBot &bot) const; + void removeFromMenu( + std::shared_ptr show, + not_null bot); + + enum class AddToMenuResult { + AlreadyInMenu, + Added, + Unsupported, + Cancelled, + }; void requestAddToMenu( not_null bot, - AddToMenuOpen open); - void requestAddToMenu( + Fn done); + void acceptMainMenuDisclaimer( + std::shared_ptr show, not_null bot, - AddToMenuOpen open, - Window::SessionController *controller, - std::optional action); - void removeFromMenu(not_null bot); + Fn done); - [[nodiscard]] std::optional lookupLastAction( - const QString &url) const; + void close(not_null instance); + void closeAll(); - static void ClearAll(); + void loadPopularAppBots(); + [[nodiscard]] auto popularAppBots() const + -> const std::vector> &; + [[nodiscard]] rpl::producer<> popularAppBotsLoaded() const; private: - struct Context; - - - Webview::ThemeParams botThemeParams() override; - bool botHandleLocalUri(QString uri, bool keepOpen) override; - void botHandleInvoice(QString slug) override; - void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; - bool botValidateExternalLink(QString uri) override; - void botOpenIvLink(QString uri) override; - void botSendData(QByteArray data) override; - void botSwitchInlineQuery( - std::vector chatTypes, - QString query) override; - void botCheckWriteAccess(Fn callback) override; - void botAllowWriteAccess(Fn callback) override; - void botSharePhone(Fn callback) override; - void botInvokeCustomMethod( - Ui::BotWebView::CustomMethodRequest request) override; - void botClose() override; - - [[nodiscard]] static Context LookupContext( - not_null controller, - const Api::SendAction &action); - [[nodiscard]] static bool IsSame( - const std::unique_ptr &a, - const Context &b); - - bool openAppFromMenuLink( - not_null controller, - not_null bot); - void requestWithOptionalConfirm( - not_null bot, - const WebViewButton &button, - const Context &context, - Window::SessionController *controllerForConfirm = nullptr); - - void resolve(); - void request(const WebViewButton &button); - void requestSimple(const WebViewButton &button); void resolveUsername( - const QString &username, + std::shared_ptr show, Fn)> done); - void confirmOpen( - not_null controller, - Fn done); - void acceptMainMenuDisclaimer( - not_null controller, - const WebViewButton &button); - enum class ToggledState { Removed, Added, @@ -216,63 +367,39 @@ private: void toggleInMenu( not_null bot, ToggledState state, - Fn callback = nullptr); - - void show( - uint64 queryId, - const QString &url, - const QString &buttonText = QString(), - bool allowClipboardRead = false, - const BotAppData *app = nullptr, - bool fromMainMenu = false); + Fn callback = nullptr); void confirmAddToMenu( AttachWebViewBot bot, - Fn callback = nullptr); - void confirmAppOpen(bool requestWriteAccess); - void requestAppView(bool allowWrite); - void started(uint64 queryId); - - void showToast( - const QString &text, - Window::SessionController *controller = nullptr); + Fn callback = nullptr); const not_null _session; base::Timer _refreshTimer; - std::unique_ptr _context; - std::unique_ptr _lastShownContext; - QString _lastShownUrl; - uint64 _lastShownQueryId = 0; - QString _lastShownButtonText; - UserData *_bot = nullptr; QString _botUsername; - QString _botAppName; QString _startCommand; - BotAppData *_app = nullptr; - QPointer _confirmAddBox; - bool _appConfirmationRequired = false; - bool _appRequestWriteAccess = false; mtpRequestId _requestId = 0; - mtpRequestId _prolongId = 0; uint64 _botsHash = 0; mtpRequestId _botsRequestId = 0; std::vector> _botsRequestCallbacks; - std::unique_ptr _addToMenuContext; - UserData *_addToMenuBot = nullptr; - mtpRequestId _addToMenuId = 0; - AddToMenuOpen _addToMenuOpen; - base::weak_ptr _addToMenuChooseController; + struct AddToMenuProcess { + mtpRequestId requestId = 0; + std::vector> done; + }; + base::flat_map, AddToMenuProcess> _addToMenu; std::vector _attachBots; rpl::event_stream<> _attachBotsUpdates; base::flat_set> _disclaimerAccepted; - std::unique_ptr _panel; - bool _catchingCancelInShowCall = false; + std::vector> _instances; + + std::vector> _popularAppBots; + mtpRequestId _popularAppBotsRequestId = 0; + rpl::variable _popularAppBotsLoaded = false; }; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index 7aa324ad9..4214c5a70 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/painter.h" #include "history/view/history_view_cursor_state.h" +#include "history/history.h" #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" @@ -677,10 +678,13 @@ void Inner::switchPm() { if (!_inlineBot || !_inlineBot->isBot()) { return; } else if (!_switchPmUrl.isEmpty()) { - _inlineBot->session().attachWebView().requestSimple( - _controller, - _inlineBot, - { .url = _switchPmUrl, .fromSwitch = true }); + const auto bot = _inlineBot; + _inlineBot->session().attachWebView().open({ + .bot = bot, + .context = { .controller = _controller }, + .button = { .url = _switchPmUrl }, + .source = InlineBots::WebViewSourceSwitch(), + }); } else { _inlineBot->botInfo->startToken = _switchPmStartToken; _inlineBot->botInfo->inlineReturnTo diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index c8c9c6b69..6a5757276 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -22,12 +22,21 @@ ivMenuToggle: IconButton(defaultIconButton) { } } ivMenuPosition: point(-2px, 40px); +ivBackIcon: icon {{ "box_button_back", menuIconColor }}; ivBack: IconButton(ivMenuToggle) { width: 60px; - icon: icon {{ "box_button_back", menuIconColor }}; - iconOver: icon {{ "box_button_back", menuIconColor }}; + icon: ivBackIcon; + iconOver: ivBackIcon; rippleAreaPosition: point(12px, 6px); } +ivBackIconDisabled: icon {{ "box_button_back", menuIconFg }}; +ivForwardIcon: icon {{ "box_button_back-flip_horizontal", menuIconColor }}; +ivForward: IconButton(ivBack) { + width: 48px; + icon: ivForwardIcon; + iconOver: ivForwardIcon; + rippleAreaPosition: point(0px, 6px); +} ivSubtitleFont: font(16px semibold); ivSubtitle: FlatLabel(defaultFlatLabel) { textFg: boxTitleFg; diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index 10e1e506b..5cbdc3eb8 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/invoke_queued.h" #include "base/qt_signal_producer.h" #include "base/qthelp_url.h" +#include "core/file_utilities.h" #include "iv/iv_data.h" #include "lang/lang_keys.h" +#include "ui/chat/attach/attach_bot_webview.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -21,12 +23,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "ui/basic_click_handlers.h" #include "ui/painter.h" +#include "ui/webview_helpers.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" #include "styles/palette.h" #include "styles/style_iv.h" #include "styles/style_menu_icons.h" +#include "styles/style_payments.h" // paymentsCriticalError #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -35,11 +39,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include #include #include #include -#include "base/call_delayed.h" +#include // AyuGram includes #include "ayu/features/streamer_mode/streamer_mode.h" @@ -66,7 +71,7 @@ namespace { { "box-divider-bg", &st::boxDividerBg }, { "box-divider-fg", &st::boxDividerFg }, { "light-button-fg", &st::lightButtonFg }, - { "light-button-bg-over", &st::lightButtonBgOver }, + //{ "light-button-bg-over", &st::lightButtonBgOver }, { "menu-icon-fg", &st::menuIconFg }, { "menu-icon-fg-over", &st::menuIconFgOver }, { "menu-bg", &st::menuBg }, @@ -83,67 +88,12 @@ namespace { static const auto phrases = base::flat_map>{ { "iv-join-channel", tr::lng_iv_join_channel }, }; - static const auto serialize = [](const style::color *color) { - const auto qt = (*color)->c; - if (qt.alpha() == 255) { - return '#' - + QByteArray::number(qt.red(), 16).right(2) - + QByteArray::number(qt.green(), 16).right(2) - + QByteArray::number(qt.blue(), 16).right(2); - } - return "rgba(" - + QByteArray::number(qt.red()) + "," - + QByteArray::number(qt.green()) + "," - + QByteArray::number(qt.blue()) + "," - + QByteArray::number(qt.alpha() / 255.) + ")"; - }; - static const auto escape = [](tr::phrase<> phrase) { - const auto text = phrase(tr::now); - - auto result = QByteArray(); - for (auto i = 0; i != text.size(); ++i) { - uint ucs4 = text[i].unicode(); - if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) { - ushort low = text[i + 1].unicode(); - if (QChar::isLowSurrogate(low)) { - ucs4 = QChar::surrogateToUcs4(ucs4, low); - ++i; - } - } - if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') { - result.append('\\').append(char(ucs4)); - } else if (ucs4 < 32 || ucs4 > 127) { - result.append('\\' + QByteArray::number(ucs4, 16) + ' '); - } else { - result.append(char(ucs4)); - } - } - return result; - }; - auto result = QByteArray(); - for (const auto &[name, phrase] : phrases) { - result += "--td-lng-" + name + ":'" + escape(phrase) + "'; "; - } - for (const auto &[name, color] : map) { - result += "--td-" + name + ':' + serialize(color) + ';'; - } - return result; -} - -[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) { - return value - .replace('&', "&") - .replace('"', """) - .replace('\'', "'") - .replace('<', "<") - .replace('>', ">"); -} - -[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) { - return value - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\'', "\\\'"); + return Ui::ComputeStyles(map, phrases) + + ';' + + Ui::ComputeSemiTransparentOverStyle( + "light-button-bg-over", + st::lightButtonBgOver, + st::windowBg); } [[nodiscard]] QByteArray WrapPage(const Prepared &page) { @@ -163,7 +113,7 @@ namespace { @@ -191,6 +141,67 @@ namespace { return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray(); } +[[nodiscard]] QString TonsiteToHttps(QString value) { + const auto ChangeHost = [](QString tonsite) { + const auto fake = "http://" + tonsite.toStdString(); + const auto parsed = ada::parse(fake); + if (!parsed) { + return QString(); + } + tonsite = QString::fromStdString(parsed->get_hostname()); + tonsite = tonsite.replace('-', "-h"); + tonsite = tonsite.replace('.', "-d"); + return tonsite + ".magic.org"; + }; + const auto prefix = u"tonsite://"_q; + if (!value.toLower().startsWith(prefix)) { + return QString(); + } + const auto part = value.mid(prefix.size()); + const auto split = part.indexOf('/'); + const auto host = ChangeHost((split < 0) ? part : part.left(split)); + if (host.isEmpty()) { + return QString(); + } + return "https://" + host + ((split < 0) ? u"/"_q : part.mid(split)); +} + +[[nodiscard]] QString HttpsToTonsite(QString value) { + const auto ChangeHost = [](QString https) { + const auto dot = https.indexOf('.'); + if (dot < 0 || https.mid(dot).toLower() != u".magic.org"_q) { + return QString(); + } + https = https.mid(0, dot); + https = https.replace("-d", "."); + https = https.replace("-h", "-"); + auto parts = https.split('.'); + for (auto &part : parts) { + if (part.startsWith(u"xn--"_q)) { + const auto utf8 = part.mid(4).toStdString(); + auto out = std::u32string(); + if (ada::idna::punycode_to_utf32(utf8, out)) { + part = QString::fromUcs4(out.data(), out.size()); + } + } + } + return parts.join('.'); + }; + const auto prefix = u"https://"_q; + if (!value.toLower().startsWith(prefix)) { + return value; + } + const auto part = value.mid(prefix.size()); + const auto split = part.indexOf('/'); + const auto host = ChangeHost((split < 0) ? part : part.left(split)); + if (host.isEmpty()) { + return value; + } + return "tonsite://" + + host + + ((split < 0) ? u"/"_q : part.mid(split)); +} + } // namespace Controller::Controller( @@ -198,7 +209,7 @@ Controller::Controller( Fn showShareBox) : _delegate(delegate) , _updateStyles([=] { - const auto str = EscapeForScriptString(ComputeStyles()); + const auto str = Ui::EscapeForScriptString(ComputeStyles()); if (_webview) { _webview->eval("IV.updateStyles('" + str + "');"); } @@ -213,8 +224,9 @@ Controller::~Controller() { _window->hide(); } _ready = false; - _webview = nullptr; + base::take(_webview); _back.destroy(); + _forward.destroy(); _menu = nullptr; _menuToggle.destroy(); _subtitle = nullptr; @@ -232,15 +244,22 @@ void Controller::updateTitleGeometry(int newWidth) const { QPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg); }, _subtitleWrap->lifetime()); - const auto progress = _subtitleLeft.value(_back->toggled() ? 1. : 0.); - const auto left = anim::interpolate( - st::ivSubtitleLeft, - _back->width() + st::ivSubtitleSkip, - progress); + const auto progressBack = _subtitleBackShift.value( + _back->toggled() ? 1. : 0.); + const auto progressForward = _subtitleForwardShift.value( + _forward->toggled() ? 1. : 0.); + const auto backAdded = _back->width() + + st::ivSubtitleSkip + - st::ivSubtitleLeft; + const auto forwardAdded = _forward->width(); + const auto left = st::ivSubtitleLeft + + anim::interpolate(0, backAdded, progressBack) + + anim::interpolate(0, forwardAdded, progressForward); _subtitle->resizeToWidth(newWidth - left - _menuToggle->width()); _subtitle->moveToLeft(left, st::ivSubtitleTop); _back->moveToLeft(0, 0); + _forward->moveToLeft(_back->width() * progressBack, 0); _menuToggle->moveToRight(0, 0); } @@ -255,12 +274,17 @@ void Controller::initControls() { _subtitleWrap.get(), _subtitleText.value(), st::ivSubtitle); - _subtitleText.value( - ) | rpl::start_with_next([=](const QString &subtitle) { + _subtitle->setSelectable(true); + + _windowTitleText = _subtitleText.value( + ) | rpl::map([=](const QString &subtitle) { const auto prefix = tr::lng_iv_window_title(tr::now); - _window->setWindowTitle(prefix + ' ' + QChar(0x2014) + ' ' + subtitle); + return prefix + ' ' + QChar(0x2014) + ' ' + subtitle; + }); + _windowTitleText.value( + ) | rpl::start_with_next([=](const QString &title) { + _window->setWindowTitle(title); }, _subtitle->lifetime()); - _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); _menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle); _menuToggle->setClickedCallback([=] { showMenu(); }); @@ -270,15 +294,25 @@ void Controller::initControls() { object_ptr(_subtitleWrap.get(), st::ivBack)); _back->entity()->setClickedCallback([=] { if (_webview) { - _webview->eval("IV.back();"); + _webview->eval("window.history.back();"); } else { _back->hide(anim::type::normal); } }); + _forward.create( + _subtitleWrap.get(), + object_ptr(_subtitleWrap.get(), st::ivForward)); + _forward->entity()->setClickedCallback([=] { + if (_webview) { + _webview->eval("window.history.forward();"); + } else { + _forward->hide(anim::type::normal); + } + }); _back->toggledValue( ) | rpl::start_with_next([=](bool toggled) { - _subtitleLeft.start( + _subtitleBackShift.start( [=] { updateTitleGeometry(_window->body()->width()); }, toggled ? 0. : 1., toggled ? 1. : 0., @@ -286,7 +320,18 @@ void Controller::initControls() { }, _back->lifetime()); _back->hide(anim::type::instant); - _subtitleLeft.stop(); + _forward->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + _subtitleForwardShift.start( + [=] { updateTitleGeometry(_window->body()->width()); }, + toggled ? 0. : 1., + toggled ? 1. : 0., + st::fadeWrapDuration); + }, _forward->lifetime()); + _forward->hide(anim::type::instant); + + _subtitleBackShift.stop(); + _subtitleForwardShift.stop(); } void Controller::show( @@ -315,6 +360,34 @@ void Controller::update(Prepared page) { } } +bool Controller::IsGoodTonSiteUrl(const QString &uri) { + return !TonsiteToHttps(uri).isEmpty(); +} + +void Controller::showTonSite( + const Webview::StorageId &storageId, + QString uri) { + const auto url = TonsiteToHttps(uri); + Assert(!url.isEmpty()); + + if (!_webview) { + createWebview(storageId); + } + if (_webview && _webview->widget()) { + _webview->navigate(url); + activate(); + } + _url = url; + _subtitleText = _url.value( + ) | rpl::filter([=](const QString &url) { + return !url.isEmpty() && url != u"about:blank"_q; + }) | rpl::map([=](QString value) { + return HttpsToTonsite(value); + }); + _windowTitleText = _subtitleText.value(); + _menuToggle->hide(); +} + QByteArray Controller::fillInChannelValuesScript( base::flat_map> inChannelValues) { auto result = QByteArray(); @@ -347,10 +420,11 @@ void Controller::createWindow() { const auto window = _window.get(); base::qt_signal_producer( - window->window()->windowHandle(), - &QWindow::activeChanged - ) | rpl::filter([=] { - return _webview && window->window()->windowHandle()->isActive(); + qApp, + &QGuiApplication::focusWindowChanged + ) | rpl::filter([=](QWindow *focused) { + const auto handle = window->window()->windowHandle(); + return _webview && handle && (focused == handle); }) | rpl::start_with_next([=] { setInnerFocus(); }, window->lifetime()); @@ -406,7 +480,7 @@ void Controller::createWebview(const Webview::StorageId &storageId) { window->lifetime().add([=] { _ready = false; - _webview = nullptr; + base::take(_webview); }); window->events( @@ -420,15 +494,41 @@ void Controller::createWebview(const Webview::StorageId &storageId) { } } }, window->lifetime()); - raw->widget()->show(); + + const auto widget = raw->widget(); + if (!widget) { + base::take(_webview); + showWebviewError(); + return; + } + widget->show(); + + QObject::connect(widget, &QObject::destroyed, [=] { + if (!_webview) { + // If we destroyed _webview ourselves, + // we don't show any message, nothing crashed. + return; + } + crl::on_main(window, [=] { + showWebviewError({ "Error: WebView has crashed." }); + }); + base::take(_webview); + }); _container->sizeValue( ) | rpl::start_with_next([=](QSize size) { - raw->widget()->setGeometry(QRect(QPoint(), size)); + if (const auto widget = raw->widget()) { + widget->setGeometry(QRect(QPoint(), size)); + } }, _container->lifetime()); raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { - return true; + if (uri.startsWith(u"http://desktop-app-resource/"_q) + || QUrl(uri).host().toLower().endsWith(u".magic.org"_q)) { + return true; + } + _events.fire({ .type = Event::Type::OpenLink, .url = uri }); + return false; }); raw->setNavigationDoneHandler([=](bool success) { }); @@ -475,9 +575,7 @@ void Controller::createWebview(const Webview::StorageId &storageId) { } else if (event == u"location_change"_q) { _index = object.value("index").toInt(); _hash = object.value("hash").toString(); - _back->toggle( - (object.value("position").toInt() > 0), - anim::type::normal); + _webview->refreshNavigationHistoryState(); } }); }); @@ -520,7 +618,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) { || index >= _pages.size()) { return Webview::DataResult::Failed; } - return finishWith(WrapPage(_pages[index]), "text/html; charset=utf-8"); + return finishWith( + WrapPage(_pages[index]), "text/html; charset=utf-8"); } else if (id.starts_with("page") && id.ends_with(".json")) { auto index = 0; const auto result = std::from_chars( @@ -558,9 +657,58 @@ void Controller::createWebview(const Webview::StorageId &storageId) { return Webview::DataResult::Failed; }); + raw->navigationHistoryState( + ) | rpl::start_with_next([=](Webview::NavigationHistoryState state) { + _back->toggle( + state.canGoBack || state.canGoForward, + anim::type::normal); + _forward->toggle(state.canGoForward, anim::type::normal); + _back->entity()->setDisabled(!state.canGoBack); + _back->entity()->setIconOverride( + state.canGoBack ? nullptr : &st::ivBackIconDisabled, + state.canGoBack ? nullptr : &st::ivBackIconDisabled); + _back->setAttribute( + Qt::WA_TransparentForMouseEvents, + !state.canGoBack); + _url = QString::fromStdString(state.url); + }, _webview->lifetime()); + raw->init(R"()"); } +void Controller::showWebviewError() { + const auto available = Webview::Availability(); + if (available.error != Webview::Available::Error::None) { + showWebviewError(Ui::BotWebView::ErrorText(available)); + } else { + showWebviewError({ "Error: Could not initialize WebView." }); + } +} + +void Controller::showWebviewError(TextWithEntities text) { + auto error = Ui::CreateChild>( + _container, + object_ptr( + _container, + rpl::single(text), + st::paymentsCriticalError), + st::paymentsCriticalErrorPadding); + error->entity()->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton) { + const auto entity = handler->getTextEntity(); + if (entity.type != EntityType::CustomUrl) { + return true; + } + File::OpenUrl(entity.data); + return false; + }); + error->show(); + _container->sizeValue() | rpl::start_with_next([=](QSize size) { + error->setGeometry(0, 0, size.width(), size.height() * 2 / 3); + }, error->lifetime()); +} + void Controller::showInWindow( const Webview::StorageId &storageId, Prepared page) { @@ -621,7 +769,7 @@ QByteArray Controller::navigateScript(int index, const QString &hash) { return "IV.navigateTo(" + QByteArray::number(index) + ", '" - + EscapeForScriptString(qthelp::url_decode(hash).toUtf8()) + + Ui::EscapeForScriptString(qthelp::url_decode(hash).toUtf8()) + "');"; } @@ -688,7 +836,7 @@ bool Controller::active() const { void Controller::showJoinedTooltip() { if (_webview && _ready) { _webview->eval("IV.showTooltip('" - + EscapeForScriptString( + + Ui::EscapeForScriptString( tr::lng_action_you_joined(tr::now).toUtf8()) + "');"); } diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h index 473813fca..9b5af4e6b 100644 --- a/Telegram/SourceFiles/iv/iv_controller.h +++ b/Telegram/SourceFiles/iv/iv_controller.h @@ -76,6 +76,9 @@ public: base::flat_map> inChannelValues); void update(Prepared page); + [[nodiscard]] static bool IsGoodTonSiteUrl(const QString &uri); + void showTonSite(const Webview::StorageId &storageId, QString uri); + [[nodiscard]] bool active() const; void showJoinedTooltip(); void minimize(); @@ -121,15 +124,22 @@ private: void showShareMenu(); void destroyShareMenu(); + void showWebviewError(); + void showWebviewError(TextWithEntities text); + const not_null _delegate; std::unique_ptr _window; std::unique_ptr _subtitleWrap; + rpl::variable _url; rpl::variable _subtitleText; + rpl::variable _windowTitleText; std::unique_ptr _subtitle; - Ui::Animations::Simple _subtitleLeft; + Ui::Animations::Simple _subtitleBackShift; + Ui::Animations::Simple _subtitleForwardShift; object_ptr _menuToggle = { nullptr }; object_ptr> _back = { nullptr }; + object_ptr> _forward = { nullptr }; base::unique_qptr _menu; Ui::RpWidget *_container = nullptr; std::unique_ptr _webview; diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index d070c93d2..8078828d6 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/platform/base_platform_info.h" +#include "base/qt_signal_producer.h" #include "boxes/share_box.h" #include "core/application.h" #include "core/file_utilities.h" @@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/layers/layer_widget.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/basic_click_handlers.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_interface.h" @@ -49,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller_link_info.h" #include +#include namespace Iv { namespace { @@ -169,6 +172,39 @@ private: }; +class TonSite final : public base::has_weak_ptr { +public: + TonSite(not_null delegate, QString uri); + + [[nodiscard]] bool active() const; + + void moveTo(QString uri); + + void minimize(); + + [[nodiscard]] rpl::producer events() const { + return _events.events(); + } + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + void createController(); + + void showWindowed(); + + const not_null _delegate; + QString _uri; + std::unique_ptr _controller; + + rpl::event_stream _events; + + rpl::lifetime _lifetime; + +}; + Shown::Shown( not_null delegate, not_null session, @@ -298,12 +334,33 @@ ShareBoxResult Shown::shareBox(ShareBoxDescriptor &&descriptor) { state->destroyRequests.fire({}); }, wrap->lifetime()); + const auto waiting = layer->lifetime().make_state(); const auto focus = crl::guard(layer, [=] { - if (!layer->window()->isActiveWindow()) { - layer->window()->activateWindow(); + const auto set = [=] { layer->window()->setFocus(); + layer->setInnerFocus(); + }; + + const auto handle = layer->window()->windowHandle(); + if (!handle) { + waiting->destroy(); + return; + } else if (QGuiApplication::focusWindow() == handle) { + waiting->destroy(); + set(); + } else { + *waiting = base::qt_signal_producer( + qApp, + &QGuiApplication::focusWindowChanged + ) | rpl::filter([=](QWindow *focused) { + const auto handle = layer->window()->windowHandle(); + return handle && (focused == handle); + }) | rpl::start_with_next([=] { + waiting->destroy(); + set(); + }); + layer->window()->activateWindow(); } - layer->setInnerFocus(); }); auto result = ShareBoxResult{ .focus = focus, @@ -719,6 +776,48 @@ void Shown::minimize() { } } +TonSite::TonSite(not_null delegate, QString uri) +: _delegate(delegate) +, _uri(uri) { + showWindowed(); +} + +void TonSite::createController() { + Expects(!_controller); + + const auto showShareBox = [=](ShareBoxDescriptor &&descriptor) { + return ShareBoxResult(); + }; + _controller = std::make_unique( + _delegate, + std::move(showShareBox)); + + _controller->events( + ) | rpl::start_to_stream(_events, _controller->lifetime()); +} + +void TonSite::showWindowed() { + if (!_controller) { + createController(); + } + + _controller->showTonSite(Storage::TonSiteStorageId(), _uri); +} + +bool TonSite::active() const { + return _controller && _controller->active(); +} + +void TonSite::moveTo(QString uri) { + _controller->showTonSite({}, uri); +} + +void TonSite::minimize() { + if (_controller) { + _controller->minimize(); + } +} + Instance::Instance(not_null delegate) : _delegate(delegate) { } @@ -762,6 +861,7 @@ void Instance::show( const auto lower = event.url.toLower(); const auto urlChecked = lower.startsWith("http://") || lower.startsWith("https://"); + const auto tonsite = lower.startsWith("tonsite://"); switch (event.type) { case Type::Close: _shown = nullptr; @@ -778,8 +878,10 @@ void Instance::show( case Type::OpenLinkExternal: if (urlChecked) { File::OpenUrl(event.url); + closeAll(); + } else if (tonsite) { + showTonSite(event.url); } - closeAll(); break; case Type::OpenMedia: if (const auto window = Core::App().activeWindow()) { @@ -817,7 +919,10 @@ void Instance::show( break; case Type::OpenPage: case Type::OpenLink: { - if (!urlChecked) { + if (tonsite) { + showTonSite(event.url); + break; + } else if (!urlChecked) { break; } const auto session = _shownSession; @@ -967,6 +1072,54 @@ void Instance::openWithIvPreferred( }).send(); } +void Instance::showTonSite( + const QString &uri, + QVariant context) { + if (!Controller::IsGoodTonSiteUrl(uri)) { + Ui::Toast::Show(tr::lng_iv_not_supported(tr::now)); + return; + } else if (Platform::IsMac()) { + // Otherwise IV is not visible under the media viewer. + Core::App().hideMediaView(); + } + if (_tonSite) { + _tonSite->moveTo(uri); + return; + } + _tonSite = std::make_unique(_delegate, uri); + _tonSite->events() | rpl::start_with_next([=](Controller::Event event) { + using Type = Controller::Event::Type; + const auto lower = event.url.toLower(); + const auto urlChecked = lower.startsWith("http://") + || lower.startsWith("https://"); + const auto tonsite = lower.startsWith("tonsite://"); + switch (event.type) { + case Type::Close: + _tonSite = nullptr; + break; + case Type::Quit: + Shortcuts::Launch(Shortcuts::Command::Quit); + break; + case Type::OpenLinkExternal: + if (urlChecked) { + File::OpenUrl(event.url); + closeAll(); + } else if (tonsite) { + showTonSite(event.url); + } + break; + case Type::OpenPage: + case Type::OpenLink: + if (urlChecked) { + UrlClickHandler::Open(event.url); + } else if (tonsite) { + showTonSite(event.url); + } + break; + } + }, _tonSite->lifetime()); +} + void Instance::requestFull( not_null session, const QString &id) { @@ -1062,23 +1215,30 @@ bool Instance::hasActiveWindow(not_null session) const { } bool Instance::closeActive() { - if (!_shown || !_shown->active()) { - return false; + if (_shown && _shown->active()) { + _shown = nullptr; + return true; + } else if (_tonSite && _tonSite->active()) { + _tonSite = nullptr; + return true; } - _shown = nullptr; - return true; + return false; } bool Instance::minimizeActive() { - if (!_shown || !_shown->active()) { - return false; + if (_shown && _shown->active()) { + _shown->minimize(); + return true; + } else if (_tonSite && _tonSite->active()) { + _tonSite->minimize(); + return true; } - _shown->minimize(); - return true; + return false; } void Instance::closeAll() { _shown = nullptr; + _tonSite = nullptr; } bool PreferForUri(const QString &uri) { diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index 9083734ad..c48cf569e 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -22,6 +22,7 @@ namespace Iv { class Data; class Shown; +class TonSite; class Instance final { public: @@ -50,6 +51,10 @@ public: QString uri, QVariant context = {}); + void showTonSite( + const QString &uri, + QVariant context = {}); + [[nodiscard]] bool hasActiveWindow( not_null session) const; @@ -97,6 +102,7 @@ private: QString _ivRequestUri; mtpRequestId _ivRequestId = 0; + std::unique_ptr _tonSite; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 9e366ca54..bfcab3fc7 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -171,7 +171,8 @@ void Account::createSession( MTPVector(), MTPint(), // stories_max_id MTPPeerColor(), // color - MTPPeerColor()), // profile_color + MTPPeerColor(), // profile_color + MTPint()), // bot_active_users serialized, streamVersion, std::move(settings)); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 2507dc8f1..6a4c63c35 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -144,28 +144,22 @@ std::vector AppConfig::getStringArray( }); } -std::vector> AppConfig::getStringMapArray( +base::flat_map AppConfig::getStringMap( const QString &key, - std::vector> &&fallback) const { + base::flat_map &&fallback) const { return getValue(key, [&](const MTPJSONValue &value) { - return value.match([&](const MTPDjsonArray &data) { - auto result = std::vector>(); + return value.match([&](const MTPDjsonObject &data) { + auto result = base::flat_map(); result.reserve(data.vvalue().v.size()); for (const auto &entry : data.vvalue().v) { - if (entry.type() != mtpc_jsonObject) { + const auto &data = entry.data(); + const auto &value = data.vvalue(); + if (value.type() != mtpc_jsonString) { return std::move(fallback); } - auto element = std::map(); - for (const auto &field : entry.c_jsonObject().vvalue().v) { - const auto &data = field.c_jsonObjectValue(); - if (data.vvalue().type() != mtpc_jsonString) { - return std::move(fallback); - } - element.emplace( - qs(data.vkey()), - qs(data.vvalue().c_jsonString().vvalue())); - } - result.push_back(std::move(element)); + result.emplace( + qs(data.vkey()), + qs(value.c_jsonString().vvalue())); } return result; }, [&](const auto &data) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 67b985348..f09d5f120 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -35,12 +35,11 @@ public: return getString(key, fallback); } else if constexpr (std::is_same_v>) { return getStringArray(key, std::move(fallback)); + } else if constexpr ( + std::is_same_v>) { + return getStringMap(key, std::move(fallback)); } else if constexpr (std::is_same_v>) { return getIntArray(key, std::move(fallback)); - } else if constexpr (std::is_same_v< - Type, - std::vector>>) { - return getStringMapArray(key, std::move(fallback)); } else if constexpr (std::is_same_v) { return getBool(key, fallback); } @@ -78,9 +77,9 @@ private: [[nodiscard]] std::vector getStringArray( const QString &key, std::vector &&fallback) const; - [[nodiscard]] std::vector> getStringMapArray( + [[nodiscard]] base::flat_map getStringMap( const QString &key, - std::vector> &&fallback) const; + base::flat_map &&fallback) const; [[nodiscard]] std::vector getIntArray( const QString &key, std::vector &&fallback) const; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index f56562b5b..f9e51985c 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "storage/storage_facade.h" #include "data/components/factchecks.h" +#include "data/components/location_pickers.h" #include "data/components/recent_peers.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" @@ -78,10 +79,14 @@ constexpr auto kTmpPasswordReserveTime = TimeId(10); if (domain.startsWith(prefix, Qt::CaseInsensitive)) { return domain.endsWith('/') ? domain - : MTP::ConfigFields().internalLinksDomain; + : MTP::ConfigFields( + session->mtp().environment() + ).internalLinksDomain; } } - return MTP::ConfigFields().internalLinksDomain; + return MTP::ConfigFields( + session->mtp().environment() + ).internalLinksDomain; } void InitializeBlockedPeers(not_null session) { @@ -147,8 +152,11 @@ Session::Session( , _recentPeers(std::make_unique(this)) , _scheduledMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) -, _topPeers(std::make_unique(this)) +, _topPeers(std::make_unique(this, Data::TopPeerType::Chat)) +, _topBotApps( + std::make_unique(this, Data::TopPeerType::BotApp)) , _factchecks(std::make_unique(this)) +, _locationPickers(std::make_unique()) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 9581e7cd4..ba5dbcd99 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -36,6 +36,7 @@ class ScheduledMessages; class SponsoredMessages; class TopPeers; class Factchecks; +class LocationPickers; } // namespace Data namespace HistoryView::Reactions { @@ -128,9 +129,15 @@ public: [[nodiscard]] Data::TopPeers &topPeers() const { return *_topPeers; } + [[nodiscard]] Data::TopPeers &topBotApps() const { + return *_topBotApps; + } [[nodiscard]] Data::Factchecks &factchecks() const { return *_factchecks; } + [[nodiscard]] Data::LocationPickers &locationPickers() const { + return *_locationPickers; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -258,7 +265,9 @@ private: const std::unique_ptr _scheduledMessages; const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; + const std::unique_ptr _topBotApps; const std::unique_ptr _factchecks; + const std::unique_ptr _locationPickers; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory; diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 6349f6b6a..253116dea 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -34,7 +34,7 @@ SessionSettings::SessionSettings() QByteArray SessionSettings::serialize() const { const auto autoDownload = _autoDownload.serialize(); - auto size = sizeof(qint32) * 4 + const auto size = sizeof(qint32) * 4 + _groupStickersSectionHidden.size() * sizeof(quint64) + sizeof(qint32) * 4 + Serialize::bytearraySize(autoDownload) @@ -103,6 +103,8 @@ QByteArray SessionSettings::serialize() const { << qint32(_lastNonPremiumLimitDownload) << qint32(_lastNonPremiumLimitUpload); } + + Ensures(result.size() == size); return result; } diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index df1f2ef25..0cfdeece2 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -193,7 +193,7 @@ void MainWindow::setupPasscodeLock() { setInnerFocus(); } if (const auto sessionController = controller().sessionController()) { - sessionController->session().attachWebView().cancel(); + sessionController->session().attachWebView().closeAll(); } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 36900fead..751108a21 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_controller.h" +#include "base/platform/base_platform_info.h" #include "base/power_save_blocker.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" @@ -128,6 +129,13 @@ struct SameDayRange { int(base::SafeRound(asin * point.x() + acos * point.y()))); } +[[nodiscard]] bool ResolveWeatherInCelsius() { + const auto saved = Core::App().settings().weatherInCelsius(); + return saved.value_or(!ranges::contains( + std::array{ u"US"_q, u"BS"_q, u"KY"_q, u"LR"_q, u"BZ"_q }, + Platform::SystemCountry().toUpper())); +} + } // namespace class Controller::PhotoPlayback final { @@ -284,7 +292,8 @@ Controller::Controller(not_null delegate) , _slider(std::make_unique(this)) , _replyArea(std::make_unique(this)) , _reactions(std::make_unique(this)) -, _recentViews(std::make_unique(this)) { +, _recentViews(std::make_unique(this)) +, _weatherInCelsius(ResolveWeatherInCelsius()){ initLayout(); using namespace rpl::mappers; @@ -536,8 +545,9 @@ void Controller::rebuildActiveAreas(const Layout &layout) const { int(base::SafeRound(general.width() * scale.width())), int(base::SafeRound(general.height() * scale.height())) ).translated(origin); - if (const auto reaction = area.reaction.get()) { - reaction->setAreaGeometry(area.geometry); + area.radius = scale.width() * area.radiusOriginal / 100.; + if (const auto view = area.view.get()) { + view->setAreaGeometry(area.geometry, area.radius); } } } @@ -1050,6 +1060,9 @@ void Controller::updateAreas(Data::Story *story) { const auto &urlAreas = story ? story->urlAreas() : std::vector(); + const auto &weatherAreas = story + ? story->weatherAreas() + : std::vector(); if (_locations != locations) { _locations = locations; _areas.clear(); @@ -1062,13 +1075,18 @@ void Controller::updateAreas(Data::Story *story) { _urlAreas = urlAreas; _areas.clear(); } + if (_weatherAreas != weatherAreas) { + _weatherAreas = weatherAreas; + _areas.clear(); + } const auto reactionsCount = int(suggestedReactions.size()); if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) { for (auto i = 0; i != reactionsCount; ++i) { const auto count = suggestedReactions[i].count; if (_suggestedReactions[i].count != count) { _suggestedReactions[i].count = count; - _areas[i + _locations.size()].reaction->updateCount(count); + const auto view = _areas[i + _locations.size()].view.get(); + view->updateReactionsCount(count); } if (_suggestedReactions[i] != suggestedReactions[i]) { _suggestedReactions = suggestedReactions; @@ -1206,7 +1224,8 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { || (_locations.empty() && _suggestedReactions.empty() && _channelPosts.empty() - && _urlAreas.empty())) { + && _urlAreas.empty() + && _weatherAreas.empty())) { return nullptr; } else if (_areas.empty()) { const auto now = story(); @@ -1240,7 +1259,7 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { } } }), - .reaction = std::move(widget), + .view = std::move(widget), }); } if (const auto session = now ? &now->session() : nullptr) { @@ -1261,19 +1280,27 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { .handler = std::make_shared(url.url), }); } + for (const auto &weather : _weatherAreas) { + _areas.push_back({ + .original = weather.area.geometry, + .radiusOriginal = weather.area.radius, + .rotation = weather.area.rotation, + .handler = std::make_shared([=] { + toggleWeatherMode(); + }), + .view = _reactions->makeWeatherAreaWidget( + weather, + _weatherInCelsius.value()), + }); + } rebuildActiveAreas(*layout); } - const auto circleContains = [&](QRect circle) { - const auto radius = std::min(circle.width(), circle.height()) / 2; - const auto delta = circle.center() - point; - return QPoint::dotProduct(delta, delta) < (radius * radius); - }; for (const auto &area : _areas) { const auto center = area.geometry.center(); const auto angle = -area.rotation; - const auto contains = area.reaction - ? circleContains(area.geometry) + const auto contains = area.view + ? area.view->contains(point) : area.geometry.contains(Rotated(point, center, angle)); if (contains) { return area.handler; @@ -1282,6 +1309,13 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { return nullptr; } +void Controller::toggleWeatherMode() const { + const auto now = !_weatherInCelsius.current(); + Core::App().settings().setWeatherInCelsius(now); + Core::App().saveSettingsDelayed(); + _weatherInCelsius = now; +} + void Controller::maybeMarkAsRead(const Player::TrackState &state) { const auto length = state.length; const auto position = Player::IsStoppedAtEnd(state.state) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 3d486fa87..860908b96 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -68,7 +68,7 @@ struct ContentLayout; class CaptionFullView; class RepostView; enum class ReactionsMode; -class SuggestedReactionView; +class StoryAreaView; struct RepostClickHandler; enum class HeaderLayout { @@ -208,10 +208,12 @@ private: }; struct ActiveArea { QRectF original; + float64 radiusOriginal = 0.; QRect geometry; float64 rotation = 0.; + float64 radius = 0.; ClickHandlerPtr handler; - std::unique_ptr reaction; + std::unique_ptr view; }; void initLayout(); @@ -227,6 +229,7 @@ private: void updatePlayingAllowed(); void setPlayingAllowed(bool allowed); void rebuildActiveAreas(const Layout &layout) const; + void toggleWeatherMode() const; void hideSiblings(); void showSiblings(not_null session); @@ -303,7 +306,9 @@ private: std::vector _suggestedReactions; std::vector _channelPosts; std::vector _urlAreas; + std::vector _weatherAreas; mutable std::vector _areas; + mutable rpl::variable _weatherInCelsius; std::vector _cachedSourcesList; int _cachedSourceIndex = -1; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index af45d9150..a8e044cc9 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/stickers_lottie.h" +#include "chat_helpers/stickers_emoji_pack.h" #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_document_media.h" @@ -20,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/admin_log/history_admin_log_item.h" #include "history/view/media/history_view_custom_emoji.h" #include "history/view/media/history_view_media_unwrapped.h" +#include "history/view/media/history_view_sticker_player.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/history_view_element.h" #include "history/history_item_reply_markup.h" @@ -61,7 +64,7 @@ constexpr auto kStoppingFadeDuration = crl::time(150); class ReactionView final : public Ui::RpWidget - , public SuggestedReactionView + , public StoryAreaView , public HistoryView::DefaultElementDelegate { public: ReactionView( @@ -69,9 +72,10 @@ public: not_null session, const Data::SuggestedReaction &reaction); - void setAreaGeometry(QRect geometry) override; - void updateCount(int count) override; + void setAreaGeometry(QRect geometry, float64 radius) override; + void updateReactionsCount(int count) override; void playEffect() override; + bool contains(QPoint point) override; private: using Element = HistoryView::Element; @@ -108,6 +112,7 @@ private: Ui::Text::String _counter; Ui::Animations::Simple _counterAnimation; QRectF _bubbleGeometry; + QRect _apiGeometry; int _size = 0; int _mediaLeft = 0; int _mediaTop = 0; @@ -126,6 +131,58 @@ private: }; +class WeatherView final : public Ui::RpWidget, public StoryAreaView { +public: + WeatherView( + QWidget *parent, + not_null session, + const Data::WeatherArea &data, + rpl::producer weatherInCelsius); + + void setAreaGeometry(QRect geometry, float64 radius) override; + void updateReactionsCount(int count) override; + void playEffect() override; + bool contains(QPoint point) override; + +private: + void paintEvent(QPaintEvent *e) override; + + void cacheBackground(); + void watchForSticker(); + void setStickerFrom(not_null document); + [[nodiscard]] QSize stickerSize() const; + + const not_null _session; + Data::WeatherArea _data; + EmojiPtr _emoji; + QColor _fg; + QImage _background; + QFont _font; + QRectF _rect; + QRect _wrapped; + float64 _radius = 0.; + int _emojiSize = 0; + int _padding = 0; + bool _celsius = true; + + std::shared_ptr _sticker; + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) { + if (std::abs(angle) < 1.) { + return point; + } + const auto alpha = angle / 180. * M_PI; + const auto acos = cos(alpha); + const auto asin = sin(alpha); + point -= origin; + return origin + QPoint( + int(base::SafeRound(acos * point.x() - asin * point.y())), + int(base::SafeRound(asin * point.x() + acos * point.y()))); +} + [[nodiscard]] AdminLog::OwnedItem GenerateFakeItem( not_null delegate, not_null history) { @@ -140,6 +197,13 @@ private: return AdminLog::OwnedItem(delegate, item); } +[[nodiscard]] QColor ChooseWeatherFg(const QColor &bg) { + const auto luminance = (0.2126 * bg.redF()) + + (0.7152 * bg.greenF()) + + (0.0722 * bg.blueF()); + return (luminance > 0.705) ? QColor(0, 0, 0) : QColor(255, 255, 255); +} + ReactionView::ReactionView( QWidget *parent, not_null session, @@ -198,7 +262,7 @@ ReactionView::ReactionView( }, lifetime()); _data.count = 0; - updateCount(reaction.count); + updateReactionsCount(reaction.count); _counterAnimation.stop(); setupCustomChatStylePalette(); @@ -212,7 +276,8 @@ void ReactionView::setupCustomChatStylePalette() { _chatStyle->applyCustomPalette(_chatStyle.get()); } -void ReactionView::setAreaGeometry(QRect geometry) { +void ReactionView::setAreaGeometry(QRect geometry, float64 radius) { + _apiGeometry = geometry; _size = std::min(geometry.width(), geometry.height()); _bubble = _size * kSuggestedBubbleSize; _bigOffset = _bubble * kSuggestedTailBigOffset; @@ -228,7 +293,7 @@ void ReactionView::setAreaGeometry(QRect geometry) { updateEffectGeometry(); } -void ReactionView::updateCount(int count) { +void ReactionView::updateReactionsCount(int count) { if (_data.count == count) { return; } @@ -283,6 +348,13 @@ void ReactionView::playEffect() { } } +bool ReactionView::contains(QPoint point) { + const auto circle = _apiGeometry; + const auto radius = std::min(circle.width(), circle.height()) / 2; + const auto delta = circle.center() - point; + return QPoint::dotProduct(delta, delta) < (radius * radius); +} + void ReactionView::paintEffectFrame( QPainter &p, not_null effect, @@ -457,6 +529,206 @@ void ReactionView::cacheBackground() { paintShape(_data.dark ? dark : QColor(255, 255, 255)); } +WeatherView::WeatherView( + QWidget *parent, + not_null session, + const Data::WeatherArea &data, + rpl::producer weatherInCelsius) +: RpWidget(parent) +, _session(session) +, _data(data) +, _emoji(Ui::Emoji::Find(_data.emoji)) +, _fg(ChooseWeatherFg(_data.color)) { + watchForSticker(); + setAttribute(Qt::WA_TransparentForMouseEvents); + show(); + + std::move(weatherInCelsius) | rpl::start_with_next([=](bool celsius) { + _celsius = celsius; + _background = {}; + update(); + }, lifetime()); +} + +void WeatherView::watchForSticker() { + if (!_emoji) { + return; + } + const auto emojiStickers = &_session->emojiStickersPack(); + if (const auto sticker = emojiStickers->stickerForEmoji(_emoji)) { + setStickerFrom(sticker.document); + } else { + emojiStickers->refreshed() | rpl::map([=] { + return emojiStickers->stickerForEmoji(_emoji).document; + }) | rpl::filter([=](DocumentData *document) { + return document != nullptr; + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](not_null document) { + setStickerFrom(document); + update(); + }, lifetime()); + } +} + +void WeatherView::setAreaGeometry(QRect geometry, float64 radius) { + const auto diagxdiag = (geometry.width() * geometry.width()) + + (geometry.height() * geometry.height()); + const auto diag = std::sqrt(diagxdiag); + const auto topleft = QRectF(geometry).center() + - QPointF(diag / 2., diag / 2.); + const auto bottomright = topleft + QPointF(diag, diag); + const auto left = int(std::floor(topleft.x())); + const auto top = int(std::floor(topleft.y())); + const auto right = int(std::ceil(bottomright.x())); + const auto bottom = int(std::ceil(bottomright.y())); + setGeometry(left, top, right - left, bottom - top); + _rect = QRectF(geometry).translated(-left, -top); + _radius = radius; + + _emojiSize = int(base::SafeRound(_rect.height() * 2 / 3.)); + _font = st::semiboldFont->f; + _font.setPixelSize(_emojiSize); + _background = {}; +} + +void WeatherView::updateReactionsCount(int count) { + Unexpected("WeatherView::updateRactionsCount."); +} + +void WeatherView::playEffect() { + Unexpected("WeatherView::playEffect."); +} + +bool WeatherView::contains(QPoint point) { + const auto geometry = _rect.translated(pos()).toRect(); + const auto angle = -_data.area.rotation; + return geometry.contains(Rotated(point, geometry.center(), angle)); +} + +void WeatherView::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + if (_background.size() != size() * style::DevicePixelRatio()) { + cacheBackground(); + } + p.drawImage(0, 0, _background); + if (_sticker && _sticker->ready()) { + auto hq = PainterHighQualityEnabler(p); + const auto rcenter = _wrapped.center(); + p.translate(rcenter); + p.rotate(_data.area.rotation); + p.translate(-rcenter); + + const auto image = _sticker->frame( + stickerSize(), + QColor(0, 0, 0, 0), + false, + crl::now(), + false).image; + const auto size = image.size() / style::DevicePixelRatio(); + const auto rect = QRectF( + _wrapped.x() + _padding + (_emojiSize - size.width()) / 2., + _wrapped.y() + (_wrapped.height() - size.height()) / 2., + size.width(), + size.height()); + const auto scenter = rect.center(); + const auto scale = (_emojiSize * 1.) / stickerSize().width(); + p.translate(scenter); + p.scale(scale, scale); + p.translate(-scenter); + p.drawImage(rect, image); + _sticker->markFrameShown(); + } +} + +QSize WeatherView::stickerSize() const { + return QSize(st::chatIntroStickerSize, st::chatIntroStickerSize); +} + +void WeatherView::setStickerFrom(not_null document) { + if (_sticker || !_emoji) { + return; + } + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + rpl::single() | rpl::then( + document->owner().session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::take(1) | rpl::start_with_next([=] { + const auto sticker = document->sticker(); + if (sticker->isLottie()) { + _sticker = std::make_shared( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::StickerSet, + stickerSize(), + Lottie::Quality::High)); + } else if (sticker->isWebm()) { + _sticker = std::make_shared( + media->owner()->location(), + media->bytes(), + stickerSize()); + } else { + _sticker = std::make_shared( + media->owner()->location(), + media->bytes(), + stickerSize()); + } + _sticker->setRepaintCallback([=] { update(); }); + update(); + }, lifetime()); +} + +void WeatherView::cacheBackground() { + const auto ratio = style::DevicePixelRatio(); + _background = QImage( + size() * ratio, + QImage::Format_ARGB32_Premultiplied); + _background.setDevicePixelRatio(ratio); + _background.fill(Qt::transparent); + + auto p = QPainter(&_background); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(_data.color); + p.setPen(Qt::NoPen); + const auto center = _rect.center(); + p.translate(center); + p.rotate(_data.area.rotation); + p.translate(-center); + + const auto format = [](float64 value) { + return QString::number(int(base::SafeRound(value * 10)) / 10.); + }; + const auto text = [&] { + const auto celsius = _data.millicelsius / 1000.; + if (_celsius) { + return format(celsius); + } + const auto fahrenheit = (celsius * 9.0 / 5.0) + 32; + return format(fahrenheit); + }().append(QChar(0xb0)).append(_celsius ? "C" : "F"); + const auto metrics = QFontMetrics(_font); + const auto textWidth = metrics.horizontalAdvance(text); + _padding = int(_rect.height() / 6); + const auto fullWidth = (_emoji ? _emojiSize : 0) + + textWidth + + (2 * _padding); + const auto left = _rect.x() + (_rect.width() - fullWidth) / 2; + _wrapped = QRect(left, _rect.y(), fullWidth, _rect.height()); + + p.drawRoundedRect(_wrapped, _radius, _radius); + + p.setPen(_fg); + p.setFont(_font); + p.drawText(_wrapped.marginsRemoved( + { _padding + (_emoji ? _emojiSize : 0), 0, _padding, 0 }), + text, + style::al_center); +} + [[nodiscard]] Data::ReactionId HeartReactionId() { return { QString() + QChar(10084) }; } @@ -804,13 +1076,24 @@ auto Reactions::chosen() const -> rpl::producer { auto Reactions::makeSuggestedReactionWidget( const Data::SuggestedReaction &reaction) --> std::unique_ptr { +-> std::unique_ptr { return std::make_unique( _controller->wrap(), &_controller->uiShow()->session(), reaction); } +auto Reactions::makeWeatherAreaWidget( + const Data::WeatherArea &data, + rpl::producer weatherInCelsius) +-> std::unique_ptr { + return std::make_unique( + _controller->wrap(), + &_controller->uiShow()->session(), + data, + std::move(weatherInCelsius)); +} + void Reactions::setReplyFieldState( rpl::producer focused, rpl::producer hasSendText) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h index de9f0e0ce..11da4c456 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h @@ -16,6 +16,7 @@ struct ReactionId; class Session; class Story; struct SuggestedReaction; +struct WeatherArea; } // namespace Data namespace HistoryView::Reactions { @@ -41,13 +42,14 @@ enum class ReactionsMode { Reaction, }; -class SuggestedReactionView { +class StoryAreaView { public: - virtual ~SuggestedReactionView() = default; + virtual ~StoryAreaView() = default; - virtual void setAreaGeometry(QRect geometry) = 0; - virtual void updateCount(int count) = 0; + virtual void setAreaGeometry(QRect geometry, float64 radius) = 0; + virtual void updateReactionsCount(int count) = 0; virtual void playEffect() = 0; + virtual bool contains(QPoint point) = 0; }; class Reactions final { @@ -79,7 +81,11 @@ public: [[nodiscard]] auto makeSuggestedReactionWidget( const Data::SuggestedReaction &reaction) - -> std::unique_ptr; + -> std::unique_ptr; + [[nodiscard]] auto makeWeatherAreaWidget( + const Data::WeatherArea &data, + rpl::producer weatherInCelsius) + -> std::unique_ptr; void setReplyFieldState( rpl::producer focused, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 8895cde1f..6ed3f76cd 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -733,25 +733,27 @@ void OverlayWidget::orderWidgets() { void OverlayWidget::setupWindow() { _window->setBodyTitleArea([=](QPoint widgetPoint) { using Flag = Ui::WindowTitleHitTestFlag; - if (!_windowed - || !_widget->rect().contains(widgetPoint) + Ui::WindowTitleHitTestFlags result; + if (!_widget->rect().contains(widgetPoint) || _helper->skipTitleHitTest(widgetPoint)) { - return Flag::None | Flag(0); + return result; } - const auto inControls = (_over != Over::None) && (_over != Over::Video); + if (widgetPoint.y() <= st::mediaviewTitleButton.height) { + result |= Flag::Menu; + } + const auto inControls = ((_over != Over::None) && (_over != Over::Video)); if (inControls || (_streamed && _streamed->controls && _streamed->controls->dragging())) { - return Flag::None | Flag(0); } else if ((_w > _widget->width() || _h > _maxUsedHeight) && (widgetPoint.y() > st::mediaviewHeaderTop) && QRect(_x, _y, _w, _h).contains(widgetPoint)) { - return Flag::None | Flag(0); } else if (_stories && _stories->ignoreWindowMove(widgetPoint)) { - return Flag::None | Flag(0); + } else if (_windowed) { + result |= Flag::Move; } - return Flag::Move | Flag(0); + return result; }); _window->setAttribute(Qt::WA_NoSystemBackground, true); @@ -5941,8 +5943,11 @@ void OverlayWidget::handleMouseRelease( } bool OverlayWidget::handleContextMenu(std::optional position) { - if (position && !QRect(_x, _y, _w, _h).contains(*position)) { - return false; + if (position) { + if (!QRect(_x, _y, _w, _h).contains(*position) + || position->y() <= st::mediaviewTitleButton.height) { + return false; + } } _menu = base::make_unique_q( _window, diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index 75312e2fb..a0bf6d916 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -355,10 +355,14 @@ void PipPanel::init() { widget()->resize(0, 0); widget()->hide(); - rp()->shownValue( - ) | rpl::filter([=](bool shown) { - return shown; - }) | rpl::start_with_next([=] { + rpl::merge( + rp()->shownValue() | rpl::to_empty, + rp()->paintRequest() | rpl::to_empty + ) | rpl::map([=] { + return widget()->windowHandle() + && widget()->windowHandle()->isExposed(); + }) | rpl::distinct_until_changed( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { // Workaround Qt's forced transient parent. Ui::Platform::ClearTransientParent(widget()); }, rp()->lifetime()); diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.cpp b/Telegram/SourceFiles/mtproto/mtproto_config.cpp index 454339f49..1da5325ef 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_config.cpp @@ -16,18 +16,30 @@ namespace { constexpr auto kVersion = 1; -} // namespace - -QString ConfigDefaultReactionEmoji() { +[[nodiscard]] QString ConfigDefaultReactionEmoji() { static const auto result = QString::fromUtf8("\xf0\x9f\x91\x8d"); return result; } -Config::Config(Environment environment) : _dcOptions(environment) { - _fields.webFileDcId = _dcOptions.isTestMode() ? 2 : 4; - _fields.txtDomainString = _dcOptions.isTestMode() - ? u"tapv3.stel.com"_q - : u"apv3.stel.com"_q; +} // namespace + +ConfigFields::ConfigFields(Environment environment) +: webFileDcId(environment == Environment::Test ? 2 : 4) +, txtDomainString(environment == Environment::Test + ? u"tapv3.stel.com"_q + : u"apv3.stel.com"_q) +, reactionDefaultEmoji(ConfigDefaultReactionEmoji()) +, gifSearchUsername(environment == Environment::Test + ? u"izgifbot"_q + : u"gif"_q) +, venueSearchUsername(environment == Environment::Test + ? u"foursquarebot"_q + : u"foursquare"_q) { +} + +Config::Config(Environment environment) +: _dcOptions(environment) +, _fields(environment) { } Config::Config(const Config &other) @@ -46,7 +58,9 @@ QByteArray Config::serialize() const { + 3 * sizeof(qint32) + Serialize::stringSize(_fields.reactionDefaultEmoji) + sizeof(quint64) - + sizeof(qint32); + + sizeof(qint32) + + Serialize::stringSize(_fields.gifSearchUsername) + + Serialize::stringSize(_fields.venueSearchUsername); auto result = QByteArray(); result.reserve(size); @@ -91,7 +105,9 @@ QByteArray Config::serialize() const { << qint32(_fields.captionLengthMax) << _fields.reactionDefaultEmoji << quint64(_fields.reactionDefaultCustom) - << qint32(_fields.ratingDecay); + << qint32(_fields.ratingDecay) + << _fields.gifSearchUsername + << _fields.venueSearchUsername; } return result; } @@ -190,6 +206,10 @@ std::unique_ptr Config::FromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { read(raw->_fields.ratingDecay); } + if (!stream.atEnd()) { + read(raw->_fields.gifSearchUsername); + read(raw->_fields.venueSearchUsername); + } if (stream.status() != QDataStream::Ok || !raw->_dcOptions.constructFromSerialized(dcOptionsSerialized)) { @@ -256,8 +276,12 @@ void Config::apply(const MTPDconfig &data) { _fields.autologinToken = qs(data.vautologin_token().value_or_empty()); _fields.ratingDecay = data.vrating_e_decay().v; if (_fields.ratingDecay <= 0) { - _fields.ratingDecay = ConfigFields().ratingDecay; + _fields.ratingDecay = ConfigFields( + _dcOptions.environment() + ).ratingDecay; } + _fields.gifSearchUsername = qs(data.vgif_search_username().value_or_empty()); + _fields.venueSearchUsername = qs(data.vvenue_search_username().value_or_empty()); if (data.vdc_options().v.empty()) { LOG(("MTP Error: config with empty dc_options received!")); diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.h b/Telegram/SourceFiles/mtproto/mtproto_config.h index 8a4db80df..289230b64 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.h +++ b/Telegram/SourceFiles/mtproto/mtproto_config.h @@ -11,9 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace MTP { -[[nodiscard]] QString ConfigDefaultReactionEmoji(); - struct ConfigFields { + explicit ConfigFields(Environment environment); + int chatSizeMax = 200; int megagroupSizeMax = 10000; int forwardedCountMax = 100; @@ -40,9 +40,12 @@ struct ConfigFields { bool blockedMode = false; int captionLengthMax = 1024; int ratingDecay = 2419200; - QString reactionDefaultEmoji = ConfigDefaultReactionEmoji(); - uint64 reactionDefaultCustom; + QString reactionDefaultEmoji; + uint64 reactionDefaultCustom = 0; QString autologinToken; + + QString gifSearchUsername; + QString venueSearchUsername; }; class Config final { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 7b8700811..2d35a0c52 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -83,7 +83,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; +user#83314fca flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -102,7 +102,7 @@ channel#aadfc8f flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5 channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull; +channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -179,6 +179,8 @@ messageActionGiveawayLaunch#332ba9ed = MessageAction; messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = MessageAction; messageActionBoostApply#cc02aa6d boosts:int = MessageAction; messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector = MessageAction; +messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction; +messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -567,7 +569,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#d38ff1c2 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute; +documentAttributeVideo#17399fad flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int video_start_ts:flags.4?double = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -628,7 +630,7 @@ messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; -botInfo#8f300b57 flags:# user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton = BotInfo; +botInfo#8f300b57 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton = BotInfo; keyboardButton#a2fa4880 text:string = KeyboardButton; keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; @@ -788,6 +790,7 @@ topPeerCategoryChannels#161d9628 = TopPeerCategory; topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory; topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory; topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory; +topPeerCategoryBotsApp#fd9e7bec = TopPeerCategory; topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector = TopPeerCategoryPeers; @@ -1452,7 +1455,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; -inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice; +inputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1464,7 +1467,8 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#a3805f3f flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; -inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; @@ -1612,6 +1616,7 @@ mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?tr mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea; inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea; mediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea; +mediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea; peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; @@ -1805,7 +1810,7 @@ starsTransactionPeerAds#60682812 = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#2db5418f flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector = StarsTransaction; +starsTransaction#2db5418f flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector = StarsTransaction; payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; @@ -1825,6 +1830,14 @@ payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAd inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction; +starsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption; + +bots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector = bots.PopularAppBots; + +botPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia; + +bots.previewInfo#ca71d64 media:Vector lang_codes:Vector = bots.PreviewInfo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1991,7 +2004,7 @@ contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bo contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; -contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; +contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true offset:int limit:int hash:long = contacts.TopPeers; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; contacts.getSaved#82f1e39f = Vector; @@ -2220,6 +2233,7 @@ messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; +messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2348,6 +2362,13 @@ bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; +bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots; +bots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia; +bots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia; +bots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector = Bool; +bots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector = Bool; +bots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo; +bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2374,6 +2395,7 @@ payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl; payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector = payments.StarsStatus; +payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2493,4 +2515,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 183 +// LAYER 185 diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index cbb435389..9a5daf9a4 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -536,6 +536,8 @@ void CheckoutProcess::handleError(const Error &error) { showToast({ tr::lng_payments_payment_failed(tr::now) }); } else if (id == u"BOT_PRECHECKOUT_FAILED"_q) { showToast({ tr::lng_payments_precheckout_failed(tr::now) }); + } else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) { + showToast({ tr::lng_payments_precheckout_timeout(tr::now) }); } else if (id == u"REQUESTED_INFO_INVALID"_q || id == u"SHIPPING_OPTION_INVALID"_q || id == u"PAYMENT_CREDENTIALS_INVALID"_q @@ -764,6 +766,14 @@ void CheckoutProcess::showForm() { _form->information(), _form->paymentMethod().ui, _form->shippingOptions()); + if (_nonPanelPaymentFormProcess && !_realFormNotified) { + _realFormNotified = true; + const auto weak = base::make_weak(_panel.get()); + _nonPanelPaymentFormProcess(RealFormPresentedNotification()); + if (weak) { + requestActivate(); + } + } } void CheckoutProcess::showEditInformation(Ui::InformationField field) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 3c7f112ca..e783bba58 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -55,9 +55,13 @@ enum class CheckoutResult { Failed, }; -struct NonPanelPaymentForm : std::variant< - std::shared_ptr, - std::shared_ptr> { +struct RealFormPresentedNotification { +}; +struct NonPanelPaymentForm + : std::variant< + std::shared_ptr, + std::shared_ptr, + RealFormPresentedNotification> { using variant::variant; }; @@ -183,6 +187,7 @@ private: Fn _nonPanelPaymentFormProcess; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; + bool _realFormNotified = false; bool _sendFormPending = false; bool _sendFormFailed = false; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index c09b41c36..8c0a44e10 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -318,20 +318,21 @@ MTPInputInvoice Form::inputInvoice() const { } else if (const auto slug = std::get_if(&_id.value)) { return MTP_inputInvoiceSlug(MTP_string(slug->slug)); } else if (const auto credits = std::get_if(&_id.value)) { - using Flag = MTPDstarsTopupOption::Flag; - const auto emptyFlag = MTPDstarsTopupOption::Flags(0); - return MTP_inputInvoiceStars(MTP_starsTopupOption( - MTP_flags(emptyFlag - | (credits->product.isEmpty() - ? Flag::f_store_product - : emptyFlag) - | (credits->extended - ? Flag::f_extended - : emptyFlag)), - MTP_long(credits->credits), - MTP_string(credits->product), - MTP_string(credits->currency), - MTP_long(credits->amount))); + if (const auto userId = peerToUser(credits->giftPeerId)) { + if (const auto user = _session->data().user(userId)) { + return MTP_inputInvoiceStars( + MTP_inputStorePaymentStarsGift( + user->inputUser, + MTP_long(credits->credits), + MTP_string(credits->currency), + MTP_long(credits->amount))); + } + } + return MTP_inputInvoiceStars( + MTP_inputStorePaymentStarsTopup( + MTP_long(credits->credits), + MTP_string(credits->currency), + MTP_long(credits->amount))); } const auto &giftCode = v::get(_id.value); using Flag = MTPDpremiumGiftCodeOption::Flag; diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index c85d946ed..42b6d00b3 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -167,6 +167,7 @@ struct InvoiceCredits { QString currency; uint64 amount = 0; bool extended = false; + PeerId giftPeerId = PeerId(0); }; struct InvoiceId { diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index 69cf7abde..b62987a82 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/boost_box.h" // Ui::StartFireworks. #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" namespace Payments { @@ -37,84 +38,98 @@ bool IsCreditsInvoice(not_null item) { return invoice && (invoice->currency == Ui::kCreditsCurrency); } +void ProcessCreditsPayment( + std::shared_ptr show, + QPointer fireworks, + std::shared_ptr form, + Fn maybeReturnToBot) { + const auto lifetime = std::make_shared(); + const auto api = lifetime->make_state( + show->session().user()); + const auto sendBox = [=] { + const auto unsuccessful = std::make_shared(true); + const auto box = show->show(Box( + Ui::SendCreditsBox, + form, + [=] { + *unsuccessful = false; + if (const auto widget = fireworks.data()) { + Ui::StartFireworks(widget); + } + if (maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Paid); + } + })); + box->boxClosing() | rpl::start_with_next([=] { + crl::on_main([=] { + if ((*unsuccessful) && maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Cancelled); + } + }); + }, box->lifetime()); + }; + api->request({}, [=](Data::CreditsStatusSlice slice) { + show->session().setCredits(slice.balance); + const auto creditsNeeded = int64(form->invoice.credits) + - int64(slice.balance); + if (creditsNeeded <= 0) { + sendBox(); + } else if (show->session().premiumPossible()) { + show->show(Box( + Settings::SmallBalanceBox, + show, + creditsNeeded, + form->botId, + sendBox)); + } else { + show->showToast( + tr::lng_credits_purchase_blocked(tr::now)); + if (maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Failed); + } + } + lifetime->destroy(); + }); +} + +void ProcessCreditsReceipt( + not_null controller, + std::shared_ptr receipt, + Fn maybeReturnToBot) { + const auto entry = Data::CreditsHistoryEntry{ + .id = receipt->id, + .title = receipt->title, + .description = receipt->description, + .date = base::unixtime::parse(receipt->date), + .photoId = receipt->photo ? receipt->photo->id : 0, + .credits = receipt->credits, + .bareMsgId = uint64(), + .barePeerId = receipt->peerId.value, + .peerType = Data::CreditsHistoryEntry::PeerType::Peer, + }; + controller->uiShow()->show(Box( + Settings::ReceiptCreditsBox, + controller, + nullptr, + entry)); + controller->window().activate(); +} + Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot) { return [=](NonPanelPaymentForm form) { using CreditsFormDataPtr = std::shared_ptr; using CreditsReceiptPtr = std::shared_ptr; - if (const auto creditsData = std::get_if(&form)) { - const auto form = *creditsData; - const auto lifetime = std::make_shared(); - const auto api = lifetime->make_state( - controller->session().user()); - const auto sendBox = [=, weak = base::make_weak(controller)] { - if (const auto strong = weak.get()) { - const auto unsuccessful = std::make_shared(true); - const auto box = controller->uiShow()->show(Box( - Ui::SendCreditsBox, - form, - crl::guard(strong, [=] { - *unsuccessful = false; - Ui::StartFireworks(strong->content()); - if (maybeReturnToBot) { - maybeReturnToBot(CheckoutResult::Paid); - } - }))); - box->boxClosing() | rpl::start_with_next([=] { - crl::on_main([=] { - if ((*unsuccessful) && maybeReturnToBot) { - maybeReturnToBot(CheckoutResult::Cancelled); - } - }); - }, box->lifetime()); - } - }; - const auto weak = base::make_weak(controller); - api->request({}, [=](Data::CreditsStatusSlice slice) { - if (const auto strong = weak.get()) { - strong->session().setCredits(slice.balance); - const auto creditsNeeded = int64(form->invoice.credits) - - int64(slice.balance); - if (creditsNeeded <= 0) { - sendBox(); - } else if (strong->session().premiumPossible()) { - strong->uiShow()->show(Box( - Settings::SmallBalanceBox, - strong, - creditsNeeded, - form->botId, - sendBox)); - } else { - strong->uiShow()->showToast( - tr::lng_credits_purchase_blocked(tr::now)); - if (maybeReturnToBot) { - maybeReturnToBot(CheckoutResult::Failed); - } - } - } - lifetime->destroy(); - }); - } - if (const auto r = std::get_if(&form)) { - const auto receipt = *r; - const auto entry = Data::CreditsHistoryEntry{ - .id = receipt->id, - .title = receipt->title, - .description = receipt->description, - .date = base::unixtime::parse(receipt->date), - .photoId = receipt->photo ? receipt->photo->id : 0, - .credits = receipt->credits, - .bareMsgId = uint64(), - .barePeerId = receipt->peerId.value, - .peerType = Data::CreditsHistoryEntry::PeerType::Peer, - }; - controller->uiShow()->show(Box( - Settings::ReceiptCreditsBox, - controller, - nullptr, - entry)); - } + v::match(form, [&](const CreditsFormDataPtr &form) { + ProcessCreditsPayment( + controller->uiShow(), + controller->content().get(), + form, + maybeReturnToBot); + }, [&](const CreditsReceiptPtr &receipt) { + ProcessCreditsReceipt(controller, receipt, maybeReturnToBot); + }, [](RealFormPresentedNotification) {}); }; } diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h index e8ab9375c..53a31f81c 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.h +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HistoryItem; +namespace Main { +class SessionShow; +} // namespace Main + namespace Window { class SessionController; } // namespace Window @@ -16,10 +20,23 @@ class SessionController; namespace Payments { enum class CheckoutResult; +struct CreditsFormData; +struct CreditsReceiptData; struct NonPanelPaymentForm; [[nodiscard]] bool IsCreditsInvoice(not_null item); +void ProcessCreditsPayment( + std::shared_ptr show, + QPointer fireworks, + std::shared_ptr form, + Fn maybeReturnToBot = nullptr); + +void ProcessCreditsReceipt( + not_null controller, + std::shared_ptr receipt, + Fn maybeReturnToBot = nullptr); + Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot = nullptr); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index d97628285..2bc023e8c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/wrap/fade_wrap.h" #include "ui/boxes/single_choice_box.h" +#include "ui/chat/attach/attach_bot_webview.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/effects/radial_animation.h" @@ -908,32 +909,9 @@ std::shared_ptr Panel::uiShow() { void Panel::showWebviewError( const QString &text, const Webview::Available &information) { - using Error = Webview::Available::Error; - Expects(information.error != Error::None); - - auto rich = TextWithEntities{ text }; - rich.append("\n\n"); - switch (information.error) { - case Error::NoWebview2: { - rich.append(tr::lng_payments_webview_install_edge( - tr::now, - lt_link, - Text::Link( - "Microsoft Edge WebView2 Runtime", - "https://go.microsoft.com/fwlink/p/?LinkId=2124703"), - Ui::Text::WithEntities)); - } break; - case Error::NoWebKitGTK: - rich.append(tr::lng_payments_webview_install_webkit(tr::now)); - break; - case Error::OldWindows: - rich.append(tr::lng_payments_webview_update_windows(tr::now)); - break; - default: - rich.append(QString::fromStdString(information.details)); - break; - } - showCriticalError(rich); + showCriticalError(TextWithEntities{ text }.append( + "\n\n" + ).append(BotWebView::ErrorText(information))); } void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) { diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp new file mode 100644 index 000000000..f87c343d3 --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp @@ -0,0 +1,24 @@ +/* +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 "platform/linux/current_geo_location_linux.h" + +#include "core/current_geo_location.h" + +namespace Platform { + +void ResolveCurrentExactLocation(Fn callback) { + callback({}); +} +void ResolveLocationAddress( + const Core::GeoLocation &location, + const QString &language, + Fn callback) { + callback({}); +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h new file mode 100644 index 000000000..6bb819543 --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h @@ -0,0 +1,10 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "platform/platform_current_geo_location.h" diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 25ac88992..baa33da99 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -18,8 +18,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "core/launcher.h" #include "core/sandbox.h" +#include "core/application.h" #include "core/core_settings.h" #include "core/update_checker.h" +#include "window/window_controller.h" #include "webview/platform/linux/webview_linux_webkitgtk.h" #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION @@ -92,10 +94,24 @@ void PortalAutostart(bool enabled, Fn done) { uniqueName.erase(0, 1); uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - const auto window = std::make_shared(); - window->setAttribute(Qt::WA_DontShowOnScreen); - window->setWindowModality(Qt::ApplicationModal); - window->show(); + const auto parent = []() -> QPointer { + const auto active = Core::App().activeWindow(); + if (!active) { + return nullptr; + } + + return active->widget().get(); + }(); + + const auto window = std::make_shared>( + std::in_place, + parent); + + auto &raw = **window; + raw.setAttribute(Qt::WA_DontShowOnScreen); + raw.setWindowFlag(Qt::Window); + raw.setWindowModality(Qt::WindowModal); + raw.show(); XdpRequest::RequestProxy::new_( proxy->get_connection(), @@ -146,7 +162,6 @@ void PortalAutostart(bool enabled, Fn done) { }); }); - std::vector commandline; commandline.push_back(executable.toStdString()); if (Core::Launcher::Instance().customWorkingDir()) { @@ -156,7 +171,9 @@ void PortalAutostart(bool enabled, Fn done) { commandline.push_back("-autostart"); interface.call_request_background( - base::Platform::XDP::ParentWindowID(), + base::Platform::XDP::ParentWindowID(parent + ? parent->windowHandle() + : nullptr), GLib::Variant::new_array({ GLib::Variant::new_dict_entry( GLib::Variant::new_string("handle_token"), diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h new file mode 100644 index 000000000..6bb819543 --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h @@ -0,0 +1,10 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "platform/platform_current_geo_location.h" diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm new file mode 100644 index 000000000..0ea554463 --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm @@ -0,0 +1,154 @@ +/* +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 "platform/mac/current_geo_location_mac.h" + +#include "base/platform/mac/base_utilities_mac.h" +#include "core/current_geo_location.h" + +#include + +@interface LocationDelegate : NSObject + +- (id) initWithCallback:(Fn)callback; +- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations; +- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error; +- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status; +- (void) dealloc; + +@end + +@implementation LocationDelegate { +CLLocationManager *_manager; +Fn _callback; +} + +- (void) fail { + [_manager stopUpdatingLocation]; + + const auto onstack = _callback; + [self release]; + + onstack({}); +} + +- (void) processWithStatus:(CLAuthorizationStatus)status { + switch (status) { + case kCLAuthorizationStatusNotDetermined: + if (@available(macOS 10.15, *)) { + [_manager requestWhenInUseAuthorization]; + } else { + [_manager startUpdatingLocation]; + } + break; + case kCLAuthorizationStatusAuthorizedAlways: + [_manager startUpdatingLocation]; + return; + case kCLAuthorizationStatusRestricted: + case kCLAuthorizationStatusDenied: + default: + [self fail]; + return; + } +} + +- (id) initWithCallback:(Fn)callback { + if (self = [super init]) { + _callback = std::move(callback); + _manager = [[CLLocationManager alloc] init]; + _manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers; + _manager.delegate = self; + if ([CLLocationManager locationServicesEnabled]) { + if (@available(macOS 11, *)) { + [self processWithStatus:[_manager authorizationStatus]]; + } else { + [self processWithStatus:[CLLocationManager authorizationStatus]]; + } + } else { + [self fail]; + } + } + return self; +} + +- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray*)locations { + [_manager stopUpdatingLocation]; + + auto result = Core::GeoLocation(); + if ([locations count] > 0) { + const auto coordinate = [locations lastObject].coordinate; + result.accuracy = Core::GeoLocationAccuracy::Exact; + result.point = QPointF(coordinate.latitude, coordinate.longitude); + } + + const auto onstack = _callback; + [self release]; + + onstack(result); +} + +- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { + if (error.code != kCLErrorLocationUnknown) { + [self fail]; + } +} + +- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status { + [self processWithStatus:status]; +} + +- (void) dealloc { + if (_manager) { + _manager.delegate = nil; + [_manager release]; + } + [super dealloc]; +} + +@end + +namespace Platform { + +void ResolveCurrentExactLocation(Fn callback) { + [[LocationDelegate alloc] initWithCallback:std::move(callback)]; +} + +void ResolveLocationAddress( + const Core::GeoLocation &location, + const QString &language, + Fn callback) { + CLGeocoder *geocoder = [[CLGeocoder alloc] init]; + CLLocation *request = [[CLLocation alloc] + initWithLatitude:location.point.x() + longitude:location.point.y()]; + [geocoder reverseGeocodeLocation:request completionHandler:^( + NSArray * __nullable placemarks, + NSError * __nullable error) { + if (placemarks && [placemarks count] > 0) { + CLPlacemark *placemark = [placemarks firstObject]; + auto list = QStringList(); + const auto push = [&](NSString *text) { + if (text) { + const auto qt = NS2QString(text); + if (!qt.isEmpty()) { + list.push_back(qt); + } + } + }; + push([placemark thoroughfare]); + push([placemark locality]); + push([placemark country]); + callback({ .name = list.join(u", "_q) }); + } else { + callback({}); + } + [geocoder release]; + }]; + [request release]; +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/platform_current_geo_location.h b/Telegram/SourceFiles/platform/platform_current_geo_location.h new file mode 100644 index 000000000..269ee81f3 --- /dev/null +++ b/Telegram/SourceFiles/platform/platform_current_geo_location.h @@ -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 + +namespace Core { +struct GeoLocation; +struct GeoAddress; +} // namespace Core + +namespace Platform { + +void ResolveCurrentExactLocation(Fn callback); +void ResolveLocationAddress( + const Core::GeoLocation &location, + const QString &language, + Fn callback); + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp new file mode 100644 index 000000000..9c7967588 --- /dev/null +++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp @@ -0,0 +1,69 @@ +/* +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 "platform/win/current_geo_location_win.h" + +#include "base/platform/win/base_windows_winrt.h" +#include "core/current_geo_location.h" + +#include +#include + +#include +#include + +namespace Platform { + +void ResolveCurrentExactLocation(Fn callback) { + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::Devices::Geolocation; + + const auto success = base::WinRT::Try([&] { + Geolocator geolocator; + geolocator.DesiredAccuracy(PositionAccuracy::High); + if (geolocator.LocationStatus() == PositionStatus::NotAvailable) { + callback({}); + return; + } + geolocator.GetGeopositionAsync().Completed([=]( + IAsyncOperation that, + AsyncStatus status) { + if (status != AsyncStatus::Completed) { + crl::on_main([=] { + callback({}); + }); + return; + } + const auto point = base::WinRT::Try([&] { + const auto coordinate = that.GetResults().Coordinate(); + return coordinate.Point().Position(); + }); + crl::on_main([=] { + if (!point) { + callback({}); + } else { + callback({ + .point = { point->Latitude, point->Longitude }, + .accuracy = Core::GeoLocationAccuracy::Exact, + }); + } + }); + }); + }); + if (!success) { + callback({}); + } +} + +void ResolveLocationAddress( + const Core::GeoLocation &location, + const QString &language, + Fn callback) { + callback({}); +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.h b/Telegram/SourceFiles/platform/win/current_geo_location_win.h new file mode 100644 index 000000000..6bb819543 --- /dev/null +++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.h @@ -0,0 +1,10 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "platform/platform_current_geo_location.h" diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index aa8534019..653521330 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -58,6 +58,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include + #ifndef DCX_USESTYLE #define DCX_USESTYLE 0x00010000 #endif @@ -98,7 +100,6 @@ BOOL CALLBACK FindToActivate(HWND hwnd, LPARAM lParam) { return TRUE; } // Found a Top-Level window. - auto level = 0; if (WindowIdFromHWND(hwnd) == request->windowId) { request->result = hwnd; request->resultLevel = 3; @@ -310,8 +311,8 @@ void psDoFixPrevious() { if (oldKeyRes2 == ERROR_SUCCESS) RegCloseKey(oldKey2); if (existNew1 || existNew2) { - const auto deleteKeyRes1 = existOld1 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str()) : ERROR_SUCCESS; - const auto deleteKeyRes2 = existOld2 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str()) : ERROR_SUCCESS; + if (existOld1) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str()); + if (existOld2) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str()); } QString userDesktopLnk, commonDesktopLnk; @@ -326,7 +327,7 @@ void psDoFixPrevious() { } QFile userDesktopFile(userDesktopLnk), commonDesktopFile(commonDesktopLnk); if (QFile::exists(userDesktopLnk) && QFile::exists(commonDesktopLnk) && userDesktopLnk != commonDesktopLnk) { - bool removed = QFile::remove(commonDesktopLnk); + QFile::remove(commonDesktopLnk); } } catch (...) { } @@ -696,3 +697,18 @@ bool psLaunchMaps(const Data::LocationPoint &point) { return QDesktopServices::openUrl( url.arg(point.latAsString()).arg(point.lonAsString())); } + +// Stub while we still support Windows 7. +extern "C" { + +STDAPI GetDpiForMonitor( + _In_ HMONITOR hmonitor, + _In_ MONITOR_DPI_TYPE dpiType, + _Out_ UINT *dpiX, + _Out_ UINT *dpiY) { + return Dlls::GetDpiForMonitor + ? Dlls::GetDpiForMonitor(hmonitor, dpiType, dpiX, dpiY) + : E_FAIL; +} + +} // extern "C" diff --git a/Telegram/SourceFiles/platform/win/tray_win.h b/Telegram/SourceFiles/platform/win/tray_win.h index 0297378f5..4df52df0b 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.h +++ b/Telegram/SourceFiles/platform/win/tray_win.h @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" namespace Window { -class CounterLayerArgs; +struct CounterLayerArgs; } // namespace Window namespace Ui { diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.cpp b/Telegram/SourceFiles/platform/win/windows_dlls.cpp index 0e03904f7..ded5dd0d1 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.cpp +++ b/Telegram/SourceFiles/platform/win/windows_dlls.cpp @@ -63,6 +63,9 @@ SafeIniter::SafeIniter() { const auto LibUser32 = LoadLibrary(L"user32.dll"); LOAD_SYMBOL(LibUser32, SetWindowCompositionAttribute); + + const auto LibShCore = LoadLibrary(L"Shcore.dll"); + LOAD_SYMBOL(LibShCore, GetDpiForMonitor); } SafeIniter kSafeIniter; diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.h b/Telegram/SourceFiles/platform/win/windows_dlls.h index 26f478376..9ebed92fc 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.h +++ b/Telegram/SourceFiles/platform/win/windows_dlls.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include #include @@ -124,5 +125,12 @@ inline BOOL(__stdcall *SetWindowCompositionAttribute)( HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*); +// SHCORE.DLL +inline HRESULT(__stdcall *GetDpiForMonitor)( + _In_ HMONITOR hmonitor, + _In_ MONITOR_DPI_TYPE dpiType, + _Out_ UINT *dpiX, + _Out_ UINT *dpiY); + } // namespace Dlls } // namespace Platform diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index e9bc2f73e..688a6940d 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -632,7 +632,6 @@ void ChatIntro::setupContent( } void ChatIntro::save() { - const auto show = controller()->uiShow(); const auto fail = [=](QString error) { }; controller()->session().data().businessInfo().saveChatIntro( diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index 4a2f14e73..6b659d044 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -8,16 +8,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_location.h" #include "core/application.h" +#include "core/shortcuts.h" +#include "data/business/data_business_info.h" +#include "data/data_file_origin.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 "mainwidget.h" +#include "mainwindow.h" #include "settings/business/settings_recipients_helper.h" +#include "settings/settings_common.h" +#include "storage/storage_account.h" +#include "ui/controls/location_picker.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_chat.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" namespace Settings { @@ -40,16 +54,38 @@ private: void setupContent(not_null controller); void save(); - [[nodiscard]] bool mapSupported() const; + void setupPicker(not_null content); + void setupUnsupported(not_null content); + [[nodiscard]] bool mapSupported() const; + void chooseOnMap(); + + const Ui::LocationPickerConfig _config; + rpl::variable _data; + rpl::variable _map = nullptr; + base::weak_ptr _picker; + std::shared_ptr _view; Ui::RoundRect _bottomSkipRounding; }; +[[nodiscard]] Ui::LocationPickerConfig ResolveBusinessMapsConfig( + not_null session) { + const auto &appConfig = session->appConfig(); + auto map = appConfig.get>( + u"tdesktop_config_map"_q, + base::flat_map()); + return { + .mapsToken = map[u"bmaps"_q], + .geoToken = map[u"bgeo"_q], + }; +} + Location::Location( QWidget *parent, not_null controller) : BusinessSection(parent, controller) +, _config(ResolveBusinessMapsConfig(&controller->session())) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } @@ -65,12 +101,23 @@ rpl::producer Location::title() { } void Location::setupContent( - not_null controller) { + not_null controller) { using namespace rpl::mappers; const auto content = Ui::CreateChild(this); -#if 0 // #TODO location choosing + if (mapSupported()) { + setupPicker(content); + } else { + setupUnsupported(content); + } + + Ui::ResizeFitChild(this, content); +} + +void Location::setupPicker(not_null content) { + _data = controller()->session().user()->businessDetails().location; + AddDividerTextWithLottie(content, { .lottie = u"location"_q, .lottieSize = st::settingsCloudPasswordIconSize, @@ -86,36 +133,162 @@ void Location::setupContent( st::settingsLocationAddress, Ui::InputField::Mode::MultiLine, tr::lng_location_address(), - QString()), + _data.current().address), st::settingsChatbotsUsernameMargins); + _data.value( + ) | rpl::start_with_next([=](const Data::BusinessLocation &location) { + address->setText(location.address); + }, address->lifetime()); + + address->changes() | rpl::start_with_next([=] { + auto copy = _data.current(); + copy.address = address->getLastText(); + _data = std::move(copy); + }, address->lifetime()); + + AddDivider(content); + AddSkip(content); + + const auto maptoggle = AddButtonWithIcon( + content, + tr::lng_location_set_map(), + st::settingsButton, + { &st::menuIconAddress } + )->toggleOn(_data.value( + ) | rpl::map([](const Data::BusinessLocation &location) { + return location.point.has_value(); + })); + + maptoggle->toggledValue() | rpl::start_with_next([=](bool toggled) { + if (!toggled) { + auto copy = _data.current(); + if (copy.point.has_value()) { + copy.point = std::nullopt; + _data = std::move(copy); + } + } else if (!_data.current().point.has_value()) { + _data.force_assign(_data.current()); + chooseOnMap(); + } + }, maptoggle->lifetime()); + + const auto mapSkip = st::defaultVerticalListSkip; + const auto mapWrap = content->add( + object_ptr>( + content, + object_ptr(content), + st::boxRowPadding + QMargins(0, mapSkip, 0, mapSkip))); + mapWrap->toggle(_data.current().point.has_value(), anim::type::instant); + + const auto map = mapWrap->entity(); + map->resize(map->width(), st::locationSize.height()); + + _data.value( + ) | rpl::start_with_next([=](const Data::BusinessLocation &location) { + const auto image = location.point.has_value() + ? controller()->session().data().location(*location.point).get() + : nullptr; + if (image) { + image->load(&controller()->session(), {}); + _view = image->createView(); + } + mapWrap->toggle(image != nullptr, anim::type::normal); + _map = image; + }, mapWrap->lifetime()); + + map->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(map); + + const auto left = (map->width() - st::locationSize.width()) / 2; + const auto rect = QRect(QPoint(left, 0), st::locationSize); + const auto &image = _view ? *_view : QImage(); + if (!image.isNull()) { + p.drawImage(rect, image); + } + + const auto paintMarker = [&](const style::icon &icon) { + icon.paint( + p, + rect.x() + ((rect.width() - icon.width()) / 2), + rect.y() + (rect.height() / 2) - icon.height(), + width()); + }; + paintMarker(st::historyMapPoint); + paintMarker(st::historyMapPointInner); + }, map->lifetime()); + + controller()->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + map->update(); + }, map->lifetime()); + + map->setClickedCallback([=] { + chooseOnMap(); + }); + showFinishes() | rpl::start_with_next([=] { address->setFocus(); }, address->lifetime()); -#endif - - if (!mapSupported()) { - AddDividerTextWithLottie(content, { - .lottie = u"phone"_q, - .lottieSize = st::settingsCloudPasswordIconSize, - .lottieMargins = st::peerAppearanceIconPadding, - .showFinished = showFinishes(), - .about = tr::lng_location_fallback(Ui::Text::WithEntities), - .aboutMargins = st::peerAppearanceCoverLabelMargin, - .parts = RectPart::Top, - }); - } else { +} +void Location::chooseOnMap() { + if (const auto strong = _picker.get()) { + strong->activate(); + return; } + const auto callback = [=](Data::InputVenue venue) { + auto copy = _data.current(); + copy.point = Data::LocationPoint( + venue.lat, + venue.lon, + Data::LocationPoint::NoAccessHash); + copy.address = venue.address; + _data = std::move(copy); + }; + const auto session = &controller()->session(); + const auto current = _data.current().point; + const auto initial = current + ? Core::GeoLocation{ + .point = { current->lat(), current->lon() }, + .accuracy = Core::GeoLocationAccuracy::Exact, + } + : Core::GeoLocation(); + _picker = Ui::LocationPicker::Show({ + .parent = controller()->widget(), + .config = _config, + .chooseLabel = tr::lng_maps_point_set(), + .session = session, + .initial = initial, + .callback = crl::guard(this, callback), + .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, + .storageId = session->local().resolveStorageIdBots(), + .closeRequests = death(), + }); +} - Ui::ResizeFitChild(this, content); +void Location::setupUnsupported(not_null content) { + AddDividerTextWithLottie(content, { + .lottie = u"phone"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_location_fallback(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + .parts = RectPart::Top, + }); } void Location::save() { + const auto fail = [=](QString error) { + }; + auto value = _data.current(); + value.address = value.address.trimmed(); + controller()->session().data().businessInfo().saveLocation(value, fail); } bool Location::mapSupported() const { - return false; + return Ui::LocationPicker::Available(_config); } } // namespace diff --git a/Telegram/SourceFiles/settings/settings_common_session.cpp b/Telegram/SourceFiles/settings/settings_common_session.cpp index 94db16611..c7cd6f2cc 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.cpp +++ b/Telegram/SourceFiles/settings/settings_common_session.cpp @@ -7,30 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_common_session.h" -#include "api/api_cloud_password.h" -#include "apiwrap.h" -#include "core/application.h" -#include "core/core_cloud_password.h" -#include "lang/lang_keys.h" -#include "main/main_domain.h" -#include "main/main_session.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" -#include "settings/settings_advanced.h" -#include "settings/settings_calls.h" #include "settings/settings_chat.h" -#include "settings/settings_experimental.h" -#include "settings/settings_folders.h" -#include "settings/settings_information.h" #include "settings/settings_main.h" -#include "settings/settings_notifications.h" -#include "settings/settings_privacy_security.h" -#include "ui/widgets/menu/menu_add_action_callback.h" -#include "window/themes/window_theme_editor_box.h" -#include "window/window_controller.h" -#include "window/window_session_controller.h" -#include "styles/style_menu_icons.h" - -#include // AyuGram includes #include "ayu/ui/settings/settings_ayu.h" diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 0890f2703..0d7d12490 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits.h" #include "api/api_credits.h" +#include "boxes/gift_credits_box.h" #include "boxes/gift_premium_box.h" #include "core/click_handler_types.h" #include "data/data_file_origin.h" @@ -128,6 +129,7 @@ void Credits::setupHistory(not_null container) { container, object_ptr(container))); const auto content = history->entity(); + const auto self = _controller->session().user(); Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); @@ -231,7 +233,7 @@ void Credits::setupHistory(not_null container) { fullSlice, fullWrap->entity(), entryClicked, - premiumBot, + self, &_star, true, true); @@ -240,7 +242,7 @@ void Credits::setupHistory(not_null container) { inSlice, inWrap->entity(), entryClicked, - premiumBot, + self, &_star, true, false); @@ -249,7 +251,7 @@ void Credits::setupHistory(not_null container) { outSlice, outWrap->entity(), std::move(entryClicked), - premiumBot, + self, &_star, false, true); @@ -263,7 +265,6 @@ void Credits::setupHistory(not_null container) { const auto apiLifetime = content->lifetime().make_state(); { using Api = Api::CreditsHistory; - const auto self = _controller->session().user(); const auto apiFull = apiLifetime->make_state(self, true, true); const auto apiIn = apiLifetime->make_state(self, true, false); const auto apiOut = apiLifetime->make_state(self, false, true); @@ -289,7 +290,21 @@ void Credits::setupContent() { Ui::StartFireworks(_parent); } }; - FillCreditOptions(_controller, content, 0, paid); + const auto self = _controller->session().user(); + FillCreditOptions(_controller->uiShow(), content, self, 0, paid); + { + Ui::AddSkip(content); + const auto giftButton = AddButtonWithIcon( + content, + tr::lng_credits_gift_button(), + st::settingsButtonLightNoIcon); + Ui::AddSkip(content); + Ui::AddDivider(content); + giftButton->setClickedCallback([=] { + Ui::ShowGiftCreditsBox(_controller, paid); + }); + } + setupHistory(content); Ui::ResizeFitChild(this, content); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 648a64900..a8c63b5f8 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -12,20 +12,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/gift_premium_box.h" +#include "chat_helpers/stickers_gift_box_pack.h" +#include "chat_helpers/stickers_lottie.h" #include "core/click_handler_types.h" +#include "core/click_handler_types.h" // UrlClickHandler #include "core/ui_integration.h" #include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_file_origin.h" -#include "core/click_handler_types.h" // UrlClickHandler #include "data/data_photo_media.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 "history/history_item_components.h" // HistoryServicePaymentRefund. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" +#include "lottie/lottie_single_player.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" @@ -224,8 +229,9 @@ void AddViewMediaHandler( } // namespace void FillCreditOptions( - not_null controller, + std::shared_ptr show, not_null container, + not_null peer, int minimumCredits, Fn paid) { const auto options = container->add( @@ -301,13 +307,14 @@ void FillCreditOptions( }, button->lifetime()); button->setClickedCallback([=] { const auto invoice = Payments::InvoiceCredits{ - .session = &controller->session(), + .session = &show->session(), .randomId = UniqueIdFromOption(option), .credits = option.credits, .product = option.product, .currency = option.currency, .amount = option.amount, .extended = option.extended, + .giftPeerId = PeerId(option.giftBarePeerId), }; const auto weak = Ui::MakeWeak(button); @@ -346,19 +353,18 @@ void FillCreditOptions( }; using ApiOptions = Api::CreditsTopupOptions; - const auto apiCredits = content->lifetime().make_state( - controller->session().user()); + const auto apiCredits = content->lifetime().make_state(peer); - if (controller->session().premiumPossible()) { + if (show->session().premiumPossible()) { apiCredits->request( ) | rpl::start_with_error_done([=](const QString &error) { - controller->showToast(error); + show->showToast(error); }, [=] { fill(apiCredits->options()); }, content->lifetime()); } - controller->session().premiumPossibleValue( + show->session().premiumPossibleValue( ) | rpl::start_with_next([=](bool premiumPossible) { if (!premiumPossible) { fill({}); @@ -447,7 +453,7 @@ void ReceiptCreditsBox( const auto &stUser = st::boostReplaceUserpic; const auto session = &controller->session(); const auto peer = (e.peerType == Type::PremiumBot) - ? premiumBot + ? nullptr : e.barePeerId ? session->data().peer(PeerId(e.barePeerId)).get() : nullptr; @@ -456,10 +462,66 @@ void ReceiptCreditsBox( content, GenericEntryPhoto(content, callback, stUser.photoSize))); AddViewMediaHandler(thumb->entity(), controller, e); - } else if (peer) { + } else if (peer && !e.gift) { content->add(object_ptr>( content, object_ptr(content, peer, stUser))); + } else if (e.gift) { + struct State final { + DocumentData *sticker = nullptr; + std::shared_ptr media; + std::unique_ptr lottie; + rpl::lifetime downloadLifetime; + }; + Ui::AddSkip( + content, + st::creditsHistoryEntryGiftStickerSpace); + const auto icon = Ui::CreateChild(content); + icon->resize(Size(st::creditsHistoryEntryGiftStickerSize)); + const auto state = icon->lifetime().make_state(); + auto &packs = session->giftBoxStickersPacks(); + const auto document = packs.lookup(packs.monthsForStars(e.credits)); + if (document && document->sticker()) { + state->sticker = document; + state->media = document->createMediaView(); + state->media->thumbnailWanted(packs.origin()); + state->media->automaticLoad(packs.origin(), nullptr); + rpl::single() | rpl::then( + session->downloaderTaskFinished() + ) | rpl::filter([=] { + return state->media->loaded(); + }) | rpl::start_with_next([=] { + state->lottie = ChatHelpers::LottiePlayerFromDocument( + state->media.get(), + ChatHelpers::StickerLottieSize::MessageHistory, + icon->size(), + Lottie::Quality::High); + state->lottie->updates() | rpl::start_with_next([=] { + icon->update(); + }, icon->lifetime()); + state->downloadLifetime.destroy(); + }, state->downloadLifetime); + } + icon->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(icon); + const auto &lottie = state->lottie; + const auto frame = (lottie && lottie->ready()) + ? lottie->frameInfo({ .box = icon->size() }) + : Lottie::Animation::FrameInfo(); + if (!frame.image.isNull()) { + p.drawImage(0, 0, frame.image); + if (lottie->frameIndex() < lottie->framesCount() - 1) { + lottie->markFrameShown(); + } + } + }, icon->lifetime()); + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + icon->move( + (size.width() - icon->width()) / 2, + st::creditsHistoryEntryGiftStickerSkip); + }, icon->lifetime()); } else { const auto widget = content->add( object_ptr>( @@ -479,7 +541,6 @@ void ReceiptCreditsBox( Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( box, object_ptr( @@ -487,6 +548,8 @@ void ReceiptCreditsBox( rpl::single( !e.title.isEmpty() ? e.title + : e.gift + ? tr::lng_credits_box_history_entry_gift_name(tr::now) : peer ? peer->name() : Ui::GenerateEntryName(e).text), @@ -499,7 +562,7 @@ void ReceiptCreditsBox( auto &lifetime = content->lifetime(); const auto text = lifetime.make_state( st::semiboldTextStyle, - (e.in ? QChar('+') : kMinus) + (e.in ? u"+"_q : e.gift ? QString() : QString(kMinus)) + Lang::FormatCountDecimal(std::abs(int64(e.credits)))); const auto roundedText = e.refunded ? tr::lng_channel_earn_history_return(tr::now) @@ -539,6 +602,8 @@ void ReceiptCreditsBox( ? st::creditsStroke : e.in ? st::boxTextFgGood + : e.gift + ? st::windowBoldFg : st::menuIconAttentionColor); const auto x = (amount->width() - fullWidth) / 2; text->draw(p, Ui::Text::PaintContext{ @@ -592,16 +657,47 @@ void ReceiptCreditsBox( object_ptr( box, rpl::single(e.description), - st::defaultFlatLabel))); + st::creditsBoxAbout))); + } + if (e.gift) { + Ui::AddSkip(content); + const auto arrow = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::topicButtonArrow, + st::channelEarnLearnArrowMargins, + false)); + auto link = tr::lng_credits_box_history_entry_gift_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_credits_box_history_entry_gift_about_url(tr::now)); + }); + box->addRow(object_ptr>( + box, + Ui::CreateLabelWithCustomEmoji( + box, + (!e.in && peer) + ? tr::lng_credits_box_history_entry_gift_out_about( + lt_user, + rpl::single(TextWithEntities{ peer->shortName() }), + lt_link, + std::move(link), + Ui::Text::RichLangValue) + : tr::lng_credits_box_history_entry_gift_in_about( + lt_link, + std::move(link), + Ui::Text::RichLangValue), + { .session = session }, + st::creditsBoxAbout))); } Ui::AddSkip(content); Ui::AddSkip(content); - AddCreditsHistoryEntryTable( - controller, - box->verticalLayout(), - e); + AddCreditsHistoryEntryTable(controller, content, e); Ui::AddSkip(content); @@ -612,15 +708,41 @@ void ReceiptCreditsBox( tr::lng_credits_box_out_about( lt_link, tr::lng_payments_terms_link( - ) | rpl::map([](const QString &t) { - using namespace Ui::Text; - return Link(t, u"https://telegram.org/tos"_q); - }), + ) | Ui::Text::ToLink( + tr::lng_credits_box_out_about_link(tr::now) + ), Ui::Text::WithEntities), st::creditsBoxAboutDivider))); Ui::AddSkip(content); + if (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) { + const auto widget = Ui::CreateChild(content); + using ColoredMiniStars = Ui::Premium::ColoredMiniStars; + const auto stars = widget->lifetime().make_state( + widget, + false, + Ui::Premium::MiniStars::Type::BiStars); + stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); + widget->resize( + st::boxWidth - stUser.photoSize, + stUser.photoSize * 2); + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + widget->moveToLeft(stUser.photoSize / 2, 0); + const auto starsRect = Rect(widget->size()); + stars->setPosition(starsRect.topLeft()); + stars->setSize(starsRect.size()); + widget->lower(); + }, widget->lifetime()); + widget->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(widget); + p.fillRect(r, Qt::transparent); + stars->paint(p); + }, widget->lifetime()); + } + const auto button = box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); @@ -633,6 +755,59 @@ void ReceiptCreditsBox( }, button->lifetime()); } +void GiftedCreditsBox( + not_null box, + not_null controller, + not_null from, + not_null to, + int count, + TimeId date) { + const auto received = to->isSelf(); + const auto anonymous = from->isServiceUser(); + const auto peer = received ? from : to; + using PeerType = Data::CreditsHistoryEntry::PeerType; + Settings::ReceiptCreditsBox(box, controller, nullptr, { + .id = QString(), + .title = (received + ? tr::lng_credits_box_history_entry_gift_name + : tr::lng_credits_box_history_entry_gift_sent)(tr::now), + .date = base::unixtime::parse(date), + .credits = uint64(count), + .bareMsgId = uint64(), + .barePeerId = (anonymous ? uint64() : peer->id.value), + .peerType = (anonymous ? PeerType::Fragment : PeerType::Peer), + .in = received, + .gift = true, + }); +} + +void ShowRefundInfoBox( + not_null controller, + FullMsgId refundItemId) { + const auto owner = &controller->session().data(); + const auto item = owner->message(refundItemId); + const auto refund = item + ? item->Get() + : nullptr; + if (!refund) { + return; + } + Assert(refund->peer != nullptr); + auto info = Data::CreditsHistoryEntry(); + info.id = refund->transactionId; + info.date = base::unixtime::parse(item->date()); + info.credits = refund->amount; + info.barePeerId = refund->peer->id.value; + info.peerType = Data::CreditsHistoryEntry::PeerType::Peer; + info.refunded = true; + info.in = true; + controller->show(Box( + ::Settings::ReceiptCreditsBox, + controller, + nullptr, // premiumBot + info)); +} + object_ptr GenericEntryPhoto( not_null parent, Fn(Fn)> callback, @@ -684,7 +859,7 @@ object_ptr PaidMediaThumbnail( void SmallBalanceBox( not_null box, - not_null controller, + std::shared_ptr show, int creditsNeeded, UserId botId, Fn paid) { @@ -695,21 +870,13 @@ void SmallBalanceBox( paid(); }; - const auto bot = controller->session().data().user(botId).get(); + const auto bot = show->session().data().user(botId).get(); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { - const auto weak = base::make_weak(controller); - const auto clickContextOther = [=] { - return QVariant::fromValue(ClickHandlerContext{ - .sessionWindow = weak, - .botStartAutoSubmit = true, - }); - }; return box->setPinnedToTopContent(object_ptr( box, st::creditsLowBalancePremiumCover, Ui::Premium::TopBarDescriptor{ - .clickContextOther = clickContextOther, .title = tr::lng_credits_small_balance_title( lt_count, rpl::single(creditsNeeded) | tr::to_count()), @@ -722,7 +889,12 @@ void SmallBalanceBox( })); }(); - FillCreditOptions(controller, box->verticalLayout(), creditsNeeded, done); + FillCreditOptions( + show, + box->verticalLayout(), + show->session().user(), + creditsNeeded, + done); content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight); content->setMinimumHeight(st::infoLayerTopBarHeight); @@ -741,12 +913,12 @@ void SmallBalanceBox( { const auto balance = AddBalanceWidget( content, - controller->session().creditsValue(), + show->session().creditsValue(), true); const auto api = balance->lifetime().make_state( - controller->session().user()); + show->session().user()); api->request({}, [=](Data::CreditsStatusSlice slice) { - controller->session().setCredits(slice.balance); + show->session().setCredits(slice.balance); }); rpl::combine( balance->sizeValue(), diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index efea501b9..e07262d6d 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -16,6 +16,10 @@ namespace Data { struct CreditsHistoryEntry; } // namespace Data +namespace Main { +class SessionShow; +} // namespace Main + namespace Window { class SessionController; } // namespace Window @@ -29,8 +33,9 @@ class VerticalLayout; namespace Settings { void FillCreditOptions( - not_null controller, + std::shared_ptr show, not_null container, + not_null peer, int minCredits, Fn paid); @@ -54,6 +59,16 @@ void ReceiptCreditsBox( not_null controller, PeerData *premiumBot, const Data::CreditsHistoryEntry &e); +void GiftedCreditsBox( + not_null box, + not_null controller, + not_null from, + not_null to, + int count, + TimeId date); +void ShowRefundInfoBox( + not_null controller, + FullMsgId refundItemId); [[nodiscard]] object_ptr GenericEntryPhoto( not_null parent, @@ -74,7 +89,7 @@ void ReceiptCreditsBox( void SmallBalanceBox( not_null box, - not_null controller, + std::shared_ptr show, int creditsNeeded, UserId botId, Fn paid); diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index f578bce82..c17eda5b8 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -149,6 +149,7 @@ void SetupExperimental( addToggle(Media::Player::kOptionDisableAutoplayNext); addToggle(kOptionSendLargePhotos); addToggle(Webview::kOptionWebviewDebugEnabled); + addToggle(Webview::kOptionWebviewLegacyEdge); addToggle(kOptionAutoScrollInactiveChat); addToggle(Window::Notifications::kOptionGNotification); addToggle(Core::kOptionFreeType); diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 690c3b913..5eb9a5ccf 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -517,7 +517,7 @@ void SetupPremium( AddPremiumStar( AddButtonWithLabel( wrap->entity(), - tr::lng_credits_summary_title(), + tr::lng_settings_credits(), controller->session().creditsValue( ) | rpl::map([=](uint64 c) { return c ? Lang::FormatCountToShort(c).string : QString{}; diff --git a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp index 9baca321c..695f8f58a 100644 --- a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp +++ b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp @@ -553,7 +553,7 @@ bool ReadSetting( const auto proxy = readProxy(); if (proxy) { list.push_back(proxy); - } else if (index < -list.size()) { + } else if (index < -int64(list.size())) { ++index; } else if (index > list.size()) { --index; diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index cca4ee92b..38fc7f4ee 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -845,7 +845,8 @@ void FileLoadTask::process(Args &&args) { MTP_double(realSeconds), MTP_int(coverWidth), MTP_int(coverHeight), - MTPint())); // preload_prefix_size + MTPint(), // preload_prefix_size + MTPdouble())); // video_start_ts if (args.generateGoodThumbnail) { goodThumbnail = video->thumbnail; diff --git a/Telegram/SourceFiles/storage/serialize_document.cpp b/Telegram/SourceFiles/storage/serialize_document.cpp index dd87b0c5f..efb527506 100644 --- a/Telegram/SourceFiles/storage/serialize_document.cpp +++ b/Telegram/SourceFiles/storage/serialize_document.cpp @@ -207,7 +207,8 @@ DocumentData *Document::readFromStreamHelper( MTP_double(duration / 1000.), MTP_int(width), MTP_int(height), - MTPint())); // preload_prefix_size + MTPint(), // preload_prefix_size + MTPdouble())); // video_start_ts } else { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(width), diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 83d8e52f4..bfac71560 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -3198,4 +3198,18 @@ bool Account::decrypt( return true; } +Webview::StorageId TonSiteStorageId() { + auto result = Webview::StorageId{ + .path = BaseGlobalPath() + u"webview-tonsite"_q, + .token = Core::App().settings().tonsiteStorageToken(), + }; + if (result.token.isEmpty()) { + result.token = QByteArray::fromStdString( + Webview::GenerateStorageToken()); + Core::App().settings().setTonsiteStorageToken(result.token); + Core::App().saveSettingsDelayed(); + } + return result; +} + } // namespace Storage diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index d1314cde2..84533f93d 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -327,4 +327,6 @@ private: }; +[[nodiscard]] Webview::StorageId TonSiteStorageId(); + } // namespace Storage diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 691d84f16..bb99e43a8 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -175,7 +175,7 @@ void AddFeaturesList( st::boostFeatureIconPosition); }; const auto proj = &Ui::Text::RichLangValue; - const auto max = std::max({ + const auto lowMax = std::max({ features.linkLogoLevel, features.transcribeLevel, features.emojiPackLevel, @@ -189,9 +189,13 @@ void AddFeaturesList( ? 0 : features.linkStylesByLevel.back().first), }); + const auto highMax = std::max(lowMax, features.sponsoredLevel); auto nameColors = 0; auto linkStyles = 0; - for (auto i = std::max(startFromLevel, 1); i <= max; ++i) { + for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) { + if ((i > lowMax) && (i < highMax)) { + continue; + } const auto unlocks = (i == startFromLevel); container->add( MakeFeaturesBadge( @@ -202,6 +206,9 @@ void AddFeaturesList( lt_count, rpl::single(float64(i)))), st::boostLevelBadgePadding); + if (i >= features.sponsoredLevel) { + add(tr::lng_channel_earn_off(proj), st::boostFeatureOffSponsored); + } if (i >= features.customWallpaperLevel) { add( (group diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index ecac121b9..6c129512d 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -40,6 +40,7 @@ struct BoostFeatures { int wallpaperLevel = 0; int wallpapersCount = 0; int customWallpaperLevel = 0; + int sponsoredLevel = 0; }; struct BoostBoxData { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 246b0a3e9..aa27118b9 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webview/webview_interface.h" #include "base/debug_log.h" #include "base/invoke_queued.h" +#include "base/qt_signal_producer.h" #include "styles/style_payments.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -34,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include // AyuGram includes #include "ayu/ayu_settings.h" @@ -183,6 +185,8 @@ void Panel::Button::updateFg(QColor fg) { void Panel::Button::updateArgs(MainButtonArgs &&args) { _textFull = std::move(args.text); setDisabled(!args.isActive); + setPointerCursor(false); + setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor); setVisible(args.isVisible); toggleProgress(args.isProgressVisible); update(); @@ -386,6 +390,13 @@ Panel::~Panel() { void Panel::requestActivate() { _widget->showAndActivate(); + if (const auto widget = _webview ? _webview->window.widget() : nullptr) { + InvokeQueued(widget, [=] { + if (widget->isVisible()) { + _webview->window.focus(); + } + }); + } } void Panel::toggleProgress(bool shown) { @@ -540,9 +551,15 @@ bool Panel::showWebview( _webview->window.navigate(url); } }, &st::menuIconRestore); - callback(tr::lng_bot_terms(tr::now), [=] { - File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); - }, &st::menuIconGroupLog); + if (_menuButtons & MenuButton::ShareGame) { + callback(tr::lng_iv_share(tr::now), [=] { + _delegate->botShareGameScore(); + }, &st::menuIconShare); + } else { + callback(tr::lng_bot_terms(tr::now), [=] { + File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); + }, &st::menuIconGroupLog); + } const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu); if (main || (_menuButtons & MenuButton::RemoveFromMenu)) { const auto handler = [=] { @@ -692,6 +709,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { openInvoice(arguments); } else if (command == "web_app_open_popup") { openPopup(arguments); + } else if (command == "web_app_open_scan_qr_popup") { + openScanQrPopup(arguments); } else if (command == "web_app_request_write_access") { requestWriteAccess(); } else if (command == "web_app_request_phone") { @@ -704,6 +723,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { requestClipboardText(arguments); } else if (command == "web_app_set_header_color") { processHeaderColor(arguments); + } else if (command == "share_score") { + _delegate->botShareGameScore(); } }); @@ -735,6 +756,20 @@ postEvent: function(eventType, eventData) { setupProgressGeometry(); + base::qt_signal_producer( + qApp, + &QGuiApplication::focusWindowChanged + ) | rpl::filter([=](QWindow *focused) { + const auto handle = _widget->window()->windowHandle(); + const auto widget = _webview ? _webview->window.widget() : nullptr; + return widget + && !widget->isHidden() + && handle + && (focused == handle); + }) | rpl::start_with_next([=] { + _webview->window.focus(); + }, _webview->lifetime); + return true; } @@ -900,6 +935,19 @@ void Panel::openPopup(const QJsonObject &args) { } } +void Panel::openScanQrPopup(const QJsonObject &args) { + const auto widget = _webview->window.widget(); + [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .text = tr::lng_bot_no_scan_qr(tr::now), + .buttons = { { + .id = "ok", + .text = tr::lng_box_ok(tr::now), + .type = Webview::PopupArgs::Button::Type::Ok, + }}, + }); +} + void Panel::requestWriteAccess() { if (_inBlockingRequest) { replyRequestWriteAccess(false); @@ -1220,6 +1268,13 @@ void Panel::updateFooterHeight() { } void Panel::showBox(object_ptr box) { + showBox(std::move(box), LayerOption::KeepOther, anim::type::normal); +} + +void Panel::showBox( + object_ptr box, + LayerOptions options, + anim::type animated) { if (const auto widget = _webview ? _webview->window.widget() : nullptr) { const auto hideNow = !widget->isHidden(); if (hideNow || _webview->lastHidingBox) { @@ -1233,13 +1288,36 @@ void Panel::showBox(object_ptr box) { && widget->isHidden() && _webview->lastHidingBox == raw) { widget->show(); + _webviewBottom->show(); } }, _webview->lifetime); if (hideNow) { widget->hide(); + _webviewBottom->hide(); } } } + const auto raw = box.data(); + + InvokeQueued(raw, [=] { + if (raw->window()->isActiveWindow()) { + // In case focus is somewhat in a native child window, + // like a webview, Qt glitches here with input fields showing + // focused state, but not receiving any keyboard input: + // + // window()->windowHandle()->isActive() == false. + // + // Steps were: SeparatePanel with a WebView2 child, + // some interaction with mouse inside the WebView2, + // so that WebView2 gets focus and active window state, + // then we call setSearchAllowed() and after animation + // is finished try typing -> nothing happens. + // + // With this workaround it works fine. + _widget->activateWindow(); + } + }); + _widget->showBox( std::move(box), LayerOption::KeepOther, @@ -1250,6 +1328,14 @@ void Panel::showToast(TextWithEntities &&text) { _widget->showToast(std::move(text)); } +not_null Panel::toastParent() const { + return _widget->uiShow()->toastParent(); +} + +void Panel::hideLayer(anim::type animated) { + _widget->hideLayer(animated); +} + void Panel::showCriticalError(const TextWithEntities &text) { _progress = nullptr; _webviewProgress = false; @@ -1294,8 +1380,10 @@ void Panel::invoiceClosed(const QString &slug, const QString &status) { { u"slug"_q, slug }, { u"status"_q, status }, }); - _widget->showAndActivate(); - _hiddenForPayment = false; + if (_hiddenForPayment) { + _hiddenForPayment = false; + _widget->showAndActivate(); + } } void Panel::hideForPayment() { @@ -1328,32 +1416,34 @@ if (window.TelegramGameProxy) { )"); } -void Panel::showWebviewError( - const QString &text, - const Webview::Available &information) { - using Error = Webview::Available::Error; - Expects(information.error != Error::None); +TextWithEntities ErrorText(const Webview::Available &info) { + Expects(info.error != Webview::Available::Error::None); - auto rich = TextWithEntities{ text }; - rich.append("\n\n"); - switch (information.error) { - case Error::NoWebview2: { - rich.append(tr::lng_payments_webview_install_edge( + using Error = Webview::Available::Error; + switch (info.error) { + case Error::NoWebview2: + return tr::lng_payments_webview_install_edge( tr::now, lt_link, Text::Link( "Microsoft Edge WebView2 Runtime", "https://go.microsoft.com/fwlink/p/?LinkId=2124703"), - Ui::Text::WithEntities)); - } break; + Ui::Text::WithEntities); case Error::NoWebKitGTK: - rich.append(tr::lng_payments_webview_install_webkit(tr::now)); - break; + return { tr::lng_payments_webview_install_webkit(tr::now) }; + case Error::OldWindows: + return { tr::lng_payments_webview_update_windows(tr::now) }; default: - rich.append(QString::fromStdString(information.details)); - break; + return { QString::fromStdString(info.details) }; } - showCriticalError(rich); +} + +void Panel::showWebviewError( + const QString &text, + const Webview::Available &information) { + showCriticalError(TextWithEntities{ text }.append( + "\n\n" + ).append(ErrorText(information))); } rpl::lifetime &Panel::lifetime() { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 91e423279..f687c24e6 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -20,6 +20,8 @@ namespace Ui { class BoxContent; class RpWidget; class SeparatePanel; +enum class LayerOption; +using LayerOptions = base::flags; } // namespace Ui namespace Webview { @@ -28,6 +30,8 @@ struct Available; namespace Ui::BotWebView { +[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info); + struct MainButtonArgs { bool isActive = false; bool isVisible = false; @@ -40,6 +44,7 @@ enum class MenuButton { OpenBot = 0x01, RemoveFromMenu = 0x02, RemoveFromMainMenu = 0x04, + ShareGame = 0x08, }; inline constexpr bool is_flag_type(MenuButton) { return true; } using MenuButtons = base::flags; @@ -67,6 +72,7 @@ public: virtual void botAllowWriteAccess(Fn callback) = 0; virtual void botSharePhone(Fn callback) = 0; virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0; + virtual void botShareGameScore() = 0; virtual void botClose() = 0; }; @@ -89,7 +95,13 @@ public: rpl::producer bottomText); void showBox(object_ptr box); + void showBox( + object_ptr box, + LayerOptions options, + anim::type animated); + void hideLayer(anim::type animated); void showToast(TextWithEntities &&text); + not_null toastParent() const; void showCriticalError(const TextWithEntities &text); void showWebviewError( const QString &text, @@ -122,6 +134,7 @@ private: void openExternalLink(const QJsonObject &args); void openInvoice(const QJsonObject &args); void openPopup(const QJsonObject &args); + void openScanQrPopup(const QJsonObject &args); void requestWriteAccess(); void replyRequestWriteAccess(bool allowed); void requestPhone(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index e3f90912f..505fa455b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -82,6 +82,10 @@ bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) { Unexpected("AlbumType in CanBeInAlbumType."); } +bool InsertTextOnImageCancel(const QString &text) { + return !text.isEmpty() && !text.startsWith(u"data:image"_q); +} + PreparedList PreparedList::Reordered( PreparedList &&list, std::vector order) { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 49fd71fa1..c1fb3a379 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -88,6 +88,7 @@ struct PreparedFile { }; [[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album); +[[nodiscard]] bool InsertTextOnImageCancel(const QString &text); struct PreparedList { enum class Error { diff --git a/Telegram/SourceFiles/ui/color_int_conversion.cpp b/Telegram/SourceFiles/ui/color_int_conversion.cpp index a1b0bcb45..5c9c57f07 100644 --- a/Telegram/SourceFiles/ui/color_int_conversion.cpp +++ b/Telegram/SourceFiles/ui/color_int_conversion.cpp @@ -22,4 +22,12 @@ std::optional MaybeColorFromSerialized(quint32 serialized) { : std::make_optional(ColorFromSerialized(serialized)); } +QColor Color32FromSerialized(quint32 serialized) { + return QColor( + int((serialized >> 24) & 0xFFU), + int((serialized >> 16) & 0xFFU), + int((serialized >> 8) & 0xFFU), + int(serialized & 0xFFU)); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/color_int_conversion.h b/Telegram/SourceFiles/ui/color_int_conversion.h index ed2bb6a18..1102863b2 100644 --- a/Telegram/SourceFiles/ui/color_int_conversion.h +++ b/Telegram/SourceFiles/ui/color_int_conversion.h @@ -12,5 +12,6 @@ namespace Ui { [[nodiscard]] QColor ColorFromSerialized(quint32 serialized); [[nodiscard]] std::optional MaybeColorFromSerialized( quint32 serialized); +[[nodiscard]] QColor Color32FromSerialized(quint32 serialized); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp new file mode 100644 index 000000000..945e5cf8f --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp @@ -0,0 +1,1329 @@ +/* +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 "ui/controls/location_picker.h" + +#include "apiwrap.h" +#include "base/platform/base_platform_info.h" +#include "boxes/peer_list_box.h" +#include "core/current_geo_location.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_location.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "dialogs/ui/chat_search_empty.h" // Dialogs::SearchEmpty. +#include "lang/lang_instance.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/session/session_show.h" +#include "main/main_session.h" +#include "mtproto/mtproto_config.h" +#include "ui/effects/radial_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/separate_panel.h" +#include "ui/widgets/shadow.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "ui/webview_helpers.h" +#include "webview/webview_data_stream_memory.h" +#include "webview/webview_embed.h" +#include "webview/webview_interface.h" +#include "window/themes/window_theme.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_dialogs.h" +#include "styles/style_window.h" +#include "styles/style_settings.h" // settingsCloudPasswordIconSize +#include "styles/style_layers.h" // boxDividerHeight + +#include +#include +#include +#include +#include +#include + +namespace Ui { +namespace { + +constexpr auto kResolveAddressDelay = 3 * crl::time(1000); +constexpr auto kSearchDebounceDelay = crl::time(900); + +#ifdef Q_OS_MAC +const auto kProtocolOverride = "mapboxapihelper"; +#else // Q_OS_MAC +const auto kProtocolOverride = ""; +#endif // Q_OS_MAC + +Core::GeoLocation LastExactLocation; + +using VenueData = Data::InputVenue; + +class VenueRowDelegate { +public: + virtual void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + const QString &type) = 0; +}; + +class VenueRow final : public PeerListRow { +public: + VenueRow(not_null delegate, const VenueData &data); + + void update(const VenueData &data); + + [[nodiscard]] VenueData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + +private: + const not_null _delegate; + VenueData _data; + +}; + +VenueRow::VenueRow( + not_null delegate, + const VenueData &data) +: PeerListRow(UniqueRowIdFromString(data.id)) +, _delegate(delegate) +, _data(data) { + setCustomStatus(data.address); +} + +void VenueRow::update(const VenueData &data) { + _data = data; + setCustomStatus(data.address); + refreshName(st::pickLocationVenueItem); +} + +VenueData VenueRow::data() const { + return _data; +} + +QString VenueRow::generateName() { + return _data.title; +} + +QString VenueRow::generateShortName() { + return generateName(); +} + +PaintRoundImageCallback VenueRow::generatePaintUserpicCallback( + bool forceRound) { + return [=]( + QPainter &p, + int x, + int y, + int outerWidth, + int size) { + _delegate->rowPaintIcon(p, x, y, size, _data.venueType); + }; +} + +class VenuesController final + : public PeerListController + , public VenueRowDelegate + , public base::has_weak_ptr { +public: + VenuesController( + not_null session, + rpl::producer> content, + Fn callback); + + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + Main::Session &session() const override; + + void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + const QString &type) override; + +private: + struct VenueIcon { + not_null document; + std::shared_ptr media; + uint32 paletteVersion : 31 = 0; + uint32 iconLoaded : 1 = 0; + QImage image; + QImage icon; + }; + + void appendRow(const VenueData &data); + + void rebuild(const std::vector &rows); + + const not_null _session; + const Fn _callback; + rpl::variable> _rows; + + base::flat_map _icons; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] QString NormalizeVenuesQuery(QString query) { + return query.trimmed().toLower(); +} + +[[nodiscard]] object_ptr MakeFoursquarePromo() { + auto result = object_ptr((QWidget*)nullptr); + const auto skip = st::defaultVerticalListSkip; + const auto raw = result.data(); + raw->resize(0, skip + st::pickLocationPromoHeight); + const auto shadow = CreateChild(raw); + raw->widthValue() | rpl::start_with_next([=](int width) { + shadow->setGeometry(0, skip, width, st::lineWidth); + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(raw); + p.fillRect(clip, st::windowBg); + p.setPen(st::windowSubTextFg); + p.setFont(st::normalFont); + p.drawText( + raw->rect().marginsRemoved({ 0, skip, 0, 0 }), + tr::lng_maps_venues_source(tr::now), + style::al_center); + }, raw->lifetime()); + return result; +} + +VenuesController::VenuesController( + not_null session, + rpl::producer> content, + Fn callback) +: _session(session) +, _callback(std::move(callback)) +, _rows(std::move(content)) { +} + +void VenuesController::prepare() { + _rows.value( + ) | rpl::start_with_next([=](const std::vector &rows) { + rebuild(rows); + }, _lifetime); +} + +void VenuesController::rebuild(const std::vector &rows) { + auto i = 0; + auto count = delegate()->peerListFullRowsCount(); + while (i < rows.size()) { + if (i < count) { + const auto row = delegate()->peerListRowAt(i); + static_cast(row.get())->update(rows[i]); + } else { + appendRow(rows[i]); + } + ++i; + } + while (i < count) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); + --count; + } + if (i > 0) { + delegate()->peerListSetBelowWidget(MakeFoursquarePromo()); + } else { + delegate()->peerListSetBelowWidget({ nullptr }); + } + delegate()->peerListRefreshRows(); +} + +void VenuesController::rowClicked(not_null row) { + _callback(static_cast(row.get())->data()); +} + +void VenuesController::rowRightActionClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +Main::Session &VenuesController::session() const { + return *_session; +} + +void VenuesController::appendRow(const VenueData &data) { + delegate()->peerListAppendRow(std::make_unique(this, data)); +} + +void VenuesController::rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + const QString &icon) { + auto i = _icons.find(icon); + if (i == end(_icons)) { + i = _icons.emplace(icon, VenueIcon{ + .document = _session->data().venueIconDocument(icon), + }).first; + i->second.media = i->second.document->createMediaView(); + i->second.document->forceToCache(true); + i->second.document->save({}, QString(), LoadFromCloudOrLocal, true); + } + auto &data = i->second; + const auto version = uint32(style::PaletteVersion()); + const auto loaded = (!data.media || data.media->loaded()) ? 1 : 0; + const auto prepare = data.image.isNull() + || (data.iconLoaded != loaded) + || (data.paletteVersion != version); + if (prepare) { + const auto skip = st::pickLocationIconSkip; + const auto inner = size - skip * 2; + const auto ratio = style::DevicePixelRatio(); + + if (loaded && data.media) { + const auto bytes = base::take(data.media)->bytes(); + data.icon = Images::Read({ .content = bytes }).image; + if (!data.icon.isNull()) { + data.icon = data.icon.scaled( + QSize(inner, inner) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + if (!data.icon.isNull()) { + data.icon = data.icon.convertToFormat( + QImage::Format_ARGB32_Premultiplied); + } + } + } + + const auto full = QSize(size, size) * ratio; + auto image = (data.image.size() == full) + ? base::take(data.image) + : QImage(full, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + image.setDevicePixelRatio(ratio); + + const auto bg = EmptyUserpic::UserpicColor( + EmptyUserpic::ColorIndex(UniqueRowIdFromString(icon))); + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + { + auto gradient = QLinearGradient(0, 0, 0, size); + gradient.setStops({ + { 0., bg.color1->c }, + { 1., bg.color2->c } + }); + p.setBrush(gradient); + } + p.setPen(Qt::NoPen); + p.drawEllipse(QRect(0, 0, size, size)); + if (!data.icon.isNull()) { + p.drawImage( + QRect(skip, skip, inner, inner), + style::colorizeImage(data.icon, st::historyPeerUserpicFg)); + } + p.end(); + + data.paletteVersion = version; + data.iconLoaded = loaded; + data.image = std::move(image); + } + p.drawImage(x, y, data.image); +} + +[[nodiscard]] QByteArray DefaultCenter(Core::GeoLocation initial) { + const auto &use = initial.exact() ? initial : LastExactLocation; + if (!use) { + return "null"; + } + return "["_q + + QByteArray::number(use.point.x()) + + ","_q + + QByteArray::number(use.point.y()) + + "]"_q; +} + +[[nodiscard]] QByteArray DefaultBounds() { + const auto country = Core::ResolveCurrentCountryLocation(); + if (!country) { + return "null"; + } + return "[["_q + + QByteArray::number(country.bounds.x()) + + ","_q + + QByteArray::number(country.bounds.y()) + + "],["_q + + QByteArray::number(country.bounds.x() + country.bounds.width()) + + ","_q + + QByteArray::number(country.bounds.y() + country.bounds.height()) + + "]]"_q; +} + +[[nodiscard]] QByteArray ComputeStyles() { + static const auto map = base::flat_map{ + { "window-bg", &st::windowBg }, + { "window-bg-over", &st::windowBgOver }, + { "window-bg-ripple", &st::windowBgRipple }, + { "window-active-text-fg", &st::windowActiveTextFg }, + { "history-to-down-shadow", &st::historyToDownShadow }, + }; + static const auto phrases = base::flat_map>{ + { "maps-places-in-area", tr::lng_maps_places_in_area }, + }; + return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode()); +} + +[[nodiscard]] QByteArray ReadResource(const QString &name) { + auto file = QFile(u":/picker/"_q + name); + return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray(); +} + +[[nodiscard]] QByteArray PickerContent() { + return R"( + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + + +
+
+ + + + +
+
+
+ + + +)"_q; +} + +[[nodiscard]] object_ptr MakeChooseLocationButton( + QWidget *parent, + rpl::producer label, + rpl::producer address) { + auto result = object_ptr( + parent, + QString(), + st::pickLocationButton); + const auto raw = result.data(); + + const auto st = &st::pickLocationVenueItem; + const auto icon = CreateChild(raw); + icon->setGeometry( + st->photoPosition.x(), + st->photoPosition.y(), + st->photoSize, + st->photoSize); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + p.drawEllipse(icon->rect()); + st::pickLocationSendIcon.paintInCenter(p, icon->rect()); + }, icon->lifetime()); + icon->show(); + + const auto hadAddress = std::make_shared(false); + auto statusText = std::move( + address + ) | rpl::map([=](const QString &text) { + if (!text.isEmpty()) { + *hadAddress = true; + return text; + } + return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString(); + }); + const auto name = CreateChild( + raw, + std::move(label), + st::pickLocationButtonText); + name->show(); + const auto status = CreateChild( + raw, + rpl::duplicate(statusText), + st::pickLocationButtonStatus); + status->showOn(rpl::duplicate( + statusText + ) | rpl::map([](const QString &text) { + return !text.isEmpty(); + }) | rpl::distinct_until_changed()); + rpl::combine( + result->widthValue(), + std::move(statusText) + ) | rpl::start_with_next([=](int width, const QString &statusText) { + const auto available = width + - st->namePosition.x() + - st->button.padding.right(); + const auto namePosition = st->namePosition; + const auto statusPosition = st->statusPosition; + name->resizeToWidth(available); + const auto nameTop = statusText.isEmpty() + ? ((st->height - name->height()) / 2) + : namePosition.y(); + name->moveToLeft(namePosition.x(), nameTop, width); + status->resizeToNaturalWidth(available); + status->moveToLeft(statusPosition.x(), statusPosition.y(), width); + }, name->lifetime()); + + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + name->setAttribute(Qt::WA_TransparentForMouseEvents); + status->setAttribute(Qt::WA_TransparentForMouseEvents); + + return result; +} + +void SetupLoadingView(not_null container) { + class Loading final : public RpWidget { + public: + explicit Loading(QWidget *parent) + : RpWidget(parent) + , animation( + [=] { if (!anim::Disabled()) update(); }, + st::pickLocationLoading) { + animation.start(st::pickLocationLoading.sineDuration); + } + + private: + void paintEvent(QPaintEvent *e) override { + auto p = QPainter(this); + const auto size = st::pickLocationLoading.size; + const auto inner = QRect(QPoint(), size); + const auto positioned = style::centerrect(rect(), inner); + animation.draw(p, positioned.topLeft(), size, width()); + } + + InfiniteRadialAnimation animation; + + }; + + const auto view = CreateChild(container); + view->resize(container->width(), st::recentPeersEmptyHeightMin); + view->show(); + + ResizeFitChild(container, view); +} + +void SetupEmptyView( + not_null container, + std::optional query) { + using Icon = Dialogs::SearchEmptyIcon; + const auto view = CreateChild( + container, + (query ? Icon::NoResults : Icon::Search), + (query + ? tr::lng_maps_no_places + : tr::lng_maps_choose_to_search)(Text::WithEntities)); + view->setMinimalHeight(st::recentPeersEmptyHeightMin); + view->show(); + + ResizeFitChild(container, view); + + InvokeQueued(view, [=] { view->animate(); }); +} + +void SetupVenues( + not_null container, + std::shared_ptr show, + rpl::producer value, + Fn callback) { + const auto otherWrap = container->add(object_ptr>( + container, + object_ptr(container))); + const auto other = otherWrap->entity(); + rpl::duplicate( + value + ) | rpl::start_with_next([=](const PickerVenueState &state) { + while (!other->children().isEmpty()) { + delete other->children()[0]; + } + if (v::is(state)) { + otherWrap->hide(anim::type::instant); + return; + } else if (v::is(state)) { + SetupLoadingView(other); + } else { + const auto n = std::get_if(&state); + SetupEmptyView(other, n ? n->query : std::optional()); + } + otherWrap->show(anim::type::instant); + }, otherWrap->lifetime()); + + auto &lifetime = container->lifetime(); + auto venuesList = rpl::duplicate( + value + ) | rpl::map([=](PickerVenueState &&state) { + return v::is(state) + ? std::move(v::get(state).list) + : std::vector(); + }); + const auto delegate = lifetime.make_state( + show); + const auto controller = lifetime.make_state( + &show->session(), + std::move(venuesList), + std::move(callback)); + controller->setStyleOverrides(&st::pickLocationVenueList); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); + + show->session().downloaderTaskFinished() | rpl::start_with_next([=] { + content->update(); + }, content->lifetime()); +} + +[[nodiscard]] PickerVenueList ParseVenues( + not_null session, + const MTPmessages_BotResults &venues) { + const auto &data = venues.data(); + session->data().processUsers(data.vusers()); + + auto &list = data.vresults().v; + auto result = PickerVenueList(); + result.list.reserve(list.size()); + for (const auto &found : list) { + found.match([&](const auto &data) { + data.vsend_message().match([&]( + const MTPDbotInlineMessageMediaVenue &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { + result.list.push_back({ + .lat = geo.vlat().v, + .lon = geo.vlong().v, + .title = qs(data.vtitle()), + .address = qs(data.vaddress()), + .provider = qs(data.vprovider()), + .id = qs(data.vvenue_id()), + .venueType = qs(data.vvenue_type()), + }); + }, [](const auto &) {}); + }, [](const auto &) {}); + }); + } + return result; +} + +not_null SetupMapPlaceholder( + not_null parent, + int minHeight, + int maxHeight, + Fn choose) { + const auto result = CreateChild(parent); + + const auto top = CreateChild(result); + const auto bottom = CreateChild(result); + + const auto icon = CreateChild(result); + const auto iconSize = st::settingsCloudPasswordIconSize; + auto ownedLottie = Lottie::MakeIcon({ + .name = u"location"_q, + .sizeOverride = { iconSize, iconSize }, + .limitFps = true, + }); + const auto lottie = ownedLottie.get(); + icon->lifetime().add([kept = std::move(ownedLottie)] {}); + + icon->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(icon); + const auto left = (icon->width() - iconSize) / 2; + const auto scale = icon->height() / float64(iconSize); + auto hq = std::optional(); + if (scale < 1.) { + const auto center = QPointF( + icon->width() / 2., + icon->height() / 2.); + hq.emplace(p); + p.translate(center); + p.scale(scale, scale); + p.translate(-center); + p.setOpacity(scale); + } + lottie->paint(p, left, 0); + }, icon->lifetime()); + + InvokeQueued(icon, [=] { + const auto till = lottie->framesCount() - 1; + lottie->animate([=] { icon->update(); }, 0, till); + }); + + const auto button = CreateChild( + result, + tr::lng_maps_select_on_map(), + st::pickLocationChooseOnMap); + button->setFullRadius(true); + button->setTextTransform(RoundButton::TextTransform::NoTransform); + button->setClickedCallback(choose); + + parent->sizeValue() | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect(QPoint(), size)); + + const auto width = size.width(); + top->setGeometry(0, 0, width, top->height()); + bottom->setGeometry(QRect( + QPoint(0, size.height() - bottom->height()), + QSize(width, bottom->height()))); + const auto dividers = top->height() + bottom->height(); + + const auto ratio = (size.height() - minHeight) + / float64(maxHeight - minHeight); + const auto iconHeight = int(base::SafeRound(ratio * iconSize)); + + const auto available = size.height() - dividers; + const auto maxDelta = (maxHeight + - dividers + - iconSize + - button->height()) / 2; + const auto minDelta = (minHeight - dividers - button->height()) / 2; + + const auto delta = anim::interpolate(minDelta, maxDelta, ratio); + button->move( + (width - button->width()) / 2, + size.height() - bottom->height() - delta - button->height()); + const auto wide = available - delta - button->height(); + const auto skip = (wide - iconHeight) / 2; + icon->setGeometry(0, top->height() + skip, width, iconHeight); + }, result->lifetime()); + + top->show(); + icon->show(); + bottom->show(); + result->show(); + + return result; +} + +} // namespace + +LocationPicker::LocationPicker(Descriptor &&descriptor) +: _config(std::move(descriptor.config)) +, _callback(std::move(descriptor.callback)) +, _quit(std::move(descriptor.quit)) +, _window(std::make_unique()) +, _body((_window->setInnerSize(st::pickLocationWindow) + , _window->showInner(base::make_unique_q(_window.get())) + , _window->inner())) +, _chooseButtonLabel(std::move(descriptor.chooseLabel)) +, _webviewStorageId(descriptor.storageId) +, _updateStyles([=] { + const auto str = EscapeForScriptString(ComputeStyles()); + if (_webview) { + _webview->eval("LocationPicker.updateStyles('" + str + "');"); + } +}) +, _geocoderResolveTimer([=] { resolveAddressByTimer(); }) +, _venueState(PickerVenueLoading()) +, _session(descriptor.session) +, _venuesSearchDebounceTimer([=] { + Expects(_venuesSearchLocation.has_value()); + Expects(_venuesSearchQuery.has_value()); + + venuesRequest(*_venuesSearchLocation, *_venuesSearchQuery); +}) +, _api(&_session->mtp()) +, _venueRecipient(descriptor.recipient) { + std::move( + descriptor.closeRequests + ) | rpl::start_with_next([=] { + _window = nullptr; + delete this; + }, _lifetime); + + setup(descriptor); +} + +std::shared_ptr LocationPicker::uiShow() { + return Main::MakeSessionShow(nullptr, _session); +} + +bool LocationPicker::Available(const LocationPickerConfig &config) { + static const auto Supported = Webview::NavigateToDataSupported(); + return Supported && !config.mapsToken.isEmpty(); +} + +void LocationPicker::setup(const Descriptor &descriptor) { + setupWindow(descriptor); + + _initialProvided = descriptor.initial; + const auto initial = _initialProvided.exact() + ? _initialProvided + : LastExactLocation; + if (initial) { + venuesRequest(initial); + resolveAddress(initial); + venuesSearchEnableAt(initial); + } + if (!_initialProvided) { + resolveCurrentLocation(); + } +} + +void LocationPicker::setupWindow(const Descriptor &descriptor) { + const auto window = _window.get(); + + window->setWindowFlag(Qt::WindowStaysOnTopHint, false); + window->closeRequests() | rpl::start_with_next([=] { + close(); + }, _lifetime); + + const auto parent = descriptor.parent + ? descriptor.parent->window()->geometry() + : QGuiApplication::primaryScreen()->availableGeometry(); + window->setTitle(tr::lng_maps_point()); + window->move( + parent.x() + (parent.width() - window->width()) / 2, + parent.y() + (parent.height() - window->height()) / 2); + + _container = CreateChild(_body.get()); + _mapPlaceholderAdded = st::pickLocationButtonSkip + + st::pickLocationButton.height + + st::pickLocationButtonSkip + + st::boxDividerHeight; + const auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded; + const auto max = st::pickLocationMapHeight + _mapPlaceholderAdded; + _mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] { + setupWebview(); + }); + _scroll = CreateChild(_body.get()); + const auto controls = _scroll->setOwnedWidget( + object_ptr(_scroll)); + + _mapControlsWrap = controls->add( + object_ptr>( + controls, + object_ptr(controls))); + _mapControlsWrap->show(anim::type::instant); + const auto mapControls = _mapControlsWrap->entity(); + + const auto toppad = mapControls->add(object_ptr(controls)); + + AddSkip(mapControls); + AddSubsectionTitle(mapControls, tr::lng_maps_or_choose()); + + auto state = _venueState.value(); + SetupVenues(controls, uiShow(), std::move(state), [=](VenueData info) { + _callback(std::move(info)); + close(); + }); + + rpl::combine( + _body->sizeValue(), + _scroll->scrollTopValue(), + _venuesSearchShown.value() + ) | rpl::start_with_next([=](QSize size, int scrollTop, bool search) { + const auto width = size.width(); + const auto height = size.height(); + const auto sub = std::min( + (st::pickLocationMapHeight - st::pickLocationCollapsedHeight), + scrollTop); + const auto mapHeight = st::pickLocationMapHeight + - sub + + (_mapPlaceholder ? _mapPlaceholderAdded : 0); + _container->setGeometry(0, 0, width, mapHeight); + const auto scrollWidgetTop = search ? 0 : mapHeight; + const auto scrollHeight = height - scrollWidgetTop; + _scroll->setGeometry(0, scrollWidgetTop, width, scrollHeight); + controls->resizeToWidth(width); + toppad->resize(width, sub); + }, _container->lifetime()); + + _container->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(_container).fillRect(clip, st::windowBg); + }, _container->lifetime()); + + _container->show(); + _scroll->show(); + controls->show(); + window->show(); +} + +void LocationPicker::setupWebview() { + Expects(!_webview); + + delete base::take(_mapPlaceholder); + + const auto mapControls = _mapControlsWrap->entity(); + mapControls->insert( + 1, + object_ptr(mapControls) + )->show(); + + _mapButton = mapControls->insert( + 1, + MakeChooseLocationButton( + mapControls, + _chooseButtonLabel.value(), + _geocoderAddress.value()), + { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip }); + _mapButton->setClickedCallback([=] { + _webview->eval("LocationPicker.send();"); + }); + _mapButton->hide(); + + _scroll->scrollToY(0); + _venuesSearchShown.force_assign(_venuesSearchShown.current()); + + _mapLoading = CreateChild(_body.get()); + + _container->geometryValue() | rpl::start_with_next([=](QRect rect) { + _mapLoading->setGeometry(rect); + }, _mapLoading->lifetime()); + + SetupLoadingView(_mapLoading); + _mapLoading->show(); + + const auto window = _window.get(); + _webview = std::make_unique( + _container, + Webview::WindowConfig{ + .opaqueBg = st::windowBg->c, + .storageId = _webviewStorageId, + .dataProtocolOverride = kProtocolOverride, + }); + const auto raw = _webview.get(); + + window->lifetime().add([=] { + _webview = nullptr; + }); + + window->events( + ) | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Close) { + close(); + } else if (e->type() == QEvent::KeyPress) { + const auto event = static_cast(e.get()); + if (event->key() == Qt::Key_Escape && !_venuesSearchQuery) { + close(); + } + } + }, window->lifetime()); + raw->widget()->show(); + + _container->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + raw->widget()->setGeometry(QRect(QPoint(), size)); + }, _container->lifetime()); + + raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { + return true; + }); + raw->setNavigationDoneHandler([=](bool success) { + }); + raw->setMessageHandler([=](const QJsonDocument &message) { + crl::on_main(_window.get(), [=] { + const auto object = message.object(); + const auto event = object.value("event").toString(); + if (event == u"ready"_q) { + mapReady(); + } else if (event == u"keydown"_q) { + const auto key = object.value("key").toString(); + const auto modifier = object.value("modifier").toString(); + processKey(key, modifier); + } else if (event == u"send"_q) { + const auto lat = object.value("latitude").toDouble(); + const auto lon = object.value("longitude").toDouble(); + _callback({ + .lat = lat, + .lon = lon, + .address = _geocoderAddress.current(), + }); + close(); + } else if (event == u"move_start"_q) { + if (const auto now = _geocoderAddress.current() + ; !now.isEmpty()) { + _geocoderSavedAddress = now; + _geocoderAddress = QString(); + } + base::take(_geocoderResolvePostponed); + _geocoderResolveTimer.cancel(); + } else if (event == u"move_end"_q) { + const auto lat = object.value("latitude").toDouble(); + const auto lon = object.value("longitude").toDouble(); + const auto location = Core::GeoLocation{ + .point = { lat, lon }, + .accuracy = Core::GeoLocationAccuracy::Exact, + }; + if (AreTheSame(_geocoderResolvingFor, location) + && !_geocoderSavedAddress.isEmpty()) { + _geocoderAddress = base::take(_geocoderSavedAddress); + _geocoderResolveTimer.cancel(); + } else { + _geocoderResolvePostponed = location; + _geocoderResolveTimer.callOnce(kResolveAddressDelay); + } + if (!AreTheSame(_venuesRequestLocation, location)) { + _webview->eval( + "LocationPicker.toggleSearchVenues(true);"); + } + venuesSearchEnableAt(location); + } else if (event == u"search_venues"_q) { + const auto lat = object.value("latitude").toDouble(); + const auto lon = object.value("longitude").toDouble(); + venuesRequest({ + .point = { lat, lon }, + .accuracy = Core::GeoLocationAccuracy::Exact, + }); + } + }); + }); + raw->setDataRequestHandler([=](Webview::DataRequest request) { + const auto pos = request.id.find('#'); + if (pos != request.id.npos) { + request.id = request.id.substr(0, pos); + } + if (!request.id.starts_with("location/")) { + return Webview::DataResult::Failed; + } + const auto finishWith = [&](QByteArray data, std::string mime) { + request.done({ + .stream = std::make_unique( + std::move(data), + std::move(mime)), + }); + return Webview::DataResult::Done; + }; + if (!_subscribedToColors) { + _subscribedToColors = true; + + rpl::merge( + Lang::Updated(), + style::PaletteChanged() + ) | rpl::start_with_next([=] { + _updateStyles.call(); + }, _webview->lifetime()); + } + const auto id = std::string_view(request.id).substr(9); + if (id == "picker.html") { + return finishWith(PickerContent(), "text/html; charset=utf-8"); + } + const auto css = id.ends_with(".css"); + const auto js = !css && id.ends_with(".js"); + if (!css && !js) { + return Webview::DataResult::Failed; + } + const auto qstring = QString::fromUtf8(id.data(), id.size()); + const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q; + if (QRegularExpression(pattern).match(qstring).hasMatch()) { + const auto bytes = ReadResource(qstring); + if (!bytes.isEmpty()) { + const auto mime = css ? "text/css" : "text/javascript"; + return finishWith(bytes, mime); + } + } + return Webview::DataResult::Failed; + }); + + raw->init(R"()"); + raw->navigateToData("location/picker.html"); +} + +void LocationPicker::resolveAddressByTimer() { + if (const auto location = base::take(_geocoderResolvePostponed)) { + resolveAddress(location); + } +} + +void LocationPicker::resolveAddress(Core::GeoLocation location) { + if (AreTheSame(_geocoderResolvingFor, location)) { + return; + } + _geocoderResolvingFor = location; + const auto done = [=](Core::GeoAddress address) { + if (!AreTheSame(_geocoderResolvingFor, location)) { + return; + } else if (address) { + _geocoderAddress = address.name; + } else { + _geocoderAddress = u"(%1, %2)"_q + .arg(location.point.x(), 0, 'f') + .arg(location.point.y(), 0, 'f'); + } + }; + const auto baseLangId = Lang::GetInstance().baseId(); + const auto langId = baseLangId.isEmpty() + ? Lang::GetInstance().id() + : baseLangId; + const auto nonEmptyId = langId.isEmpty() ? u"en"_q : langId; + Core::ResolveLocationAddress( + location, + langId, + _config.geoToken, + crl::guard(this, done)); +} + +void LocationPicker::mapReady() { + Expects(_scroll != nullptr); + + delete base::take(_mapLoading); + + const auto token = _config.mapsToken.toUtf8(); + const auto center = DefaultCenter(_initialProvided); + const auto bounds = DefaultBounds(); + const auto protocol = *kProtocolOverride + ? "'"_q + kProtocolOverride + "'" + : "null"; + const auto params = "token: '" + token + "'" + + ", center: " + center + + ", bounds: " + bounds + + ", protocol: " + protocol; + _webview->eval("LocationPicker.init({ " + params + " });"); + + const auto handle = _window->window()->windowHandle(); + if (handle && QGuiApplication::focusWindow() == handle) { + _webview->focus(); + } + _mapButton->show(); +} + +bool LocationPicker::venuesFromCache( + Core::GeoLocation location, + QString query) { + const auto normalized = NormalizeVenuesQuery(query); + auto &cache = _venuesCache[normalized]; + const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) { + return AreTheSame(v.location, location); + }); + if (i == end(cache)) { + return false; + } + _venuesRequestLocation = location; + _venuesRequestQuery = normalized; + _venuesInitialQuery = query; + venuesApplyResults(i->result); + return true; +} + +void LocationPicker::venuesRequest( + Core::GeoLocation location, + QString query) { + const auto normalized = NormalizeVenuesQuery(query); + if (AreTheSame(_venuesRequestLocation, location) + && _venuesRequestQuery == normalized) { + return; + } else if (const auto oldRequestId = base::take(_venuesRequestId)) { + _api.request(oldRequestId).cancel(); + } + _venueState = PickerVenueLoading(); + _venuesRequestLocation = location; + _venuesRequestQuery = normalized; + _venuesInitialQuery = query; + if (_venuesBot) { + venuesSendRequest(); + } else if (_venuesBotRequestId) { + return; + } + const auto username = _session->serverConfig().venueSearchUsername; + _venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername( + MTP_string(username) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + auto &data = result.data(); + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + const auto peer = _session->data().peerLoaded( + peerFromMTP(data.vpeer())); + const auto user = peer ? peer->asUser() : nullptr; + if (user && user->isBotInlineGeo()) { + _venuesBot = user; + venuesSendRequest(); + } else { + LOG(("API Error: Bad peer returned by: %1").arg(username)); + } + }).fail([=] { + LOG(("API Error: Error returned on lookup: %1").arg(username)); + }).send(); +} + +void LocationPicker::venuesSendRequest() { + Expects(_venuesBot != nullptr); + + if (_venuesRequestId || !_venuesRequestLocation) { + return; + } + _venuesRequestId = _api.request(MTPmessages_GetInlineBotResults( + MTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point), + _venuesBot->inputUser, + (_venueRecipient ? _venueRecipient->input : MTP_inputPeerEmpty()), + MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(_venuesRequestLocation.point.x()), + MTP_double(_venuesRequestLocation.point.y()), + MTP_int(0)), // accuracy_radius + MTP_string(_venuesRequestQuery), + MTP_string() // offset + )).done([=](const MTPmessages_BotResults &result) { + auto parsed = ParseVenues(_session, result); + _venuesCache[_venuesRequestQuery].push_back({ + .location = _venuesRequestLocation, + .result = parsed, + }); + venuesApplyResults(std::move(parsed)); + }).fail([=] { + venuesApplyResults({}); + }).send(); +} + +void LocationPicker::venuesApplyResults(PickerVenueList venues) { + _venuesRequestId = 0; + if (venues.list.empty()) { + _venueState = PickerVenueNothingFound{ _venuesInitialQuery }; + } else { + _venueState = std::move(venues); + } +} + +void LocationPicker::venuesSearchEnableAt(Core::GeoLocation location) { + if (!_venuesSearchLocation) { + _window->setSearchAllowed( + tr::lng_dlg_filter(), + [=](std::optional query) { + venuesSearchChanged(query); + }); + } + _venuesSearchLocation = location; +} + +void LocationPicker::venuesSearchChanged( + const std::optional &query) { + _venuesSearchQuery = query; + + const auto shown = query && !query->trimmed().isEmpty(); + _venuesSearchShown = shown; + if (_container->isHidden() != shown) { + _container->setVisible(!shown); + _mapControlsWrap->toggle(!shown, anim::type::instant); + if (shown) { + _venuesNoSearchLocation = _venuesRequestLocation; + } else if (_venuesNoSearchLocation) { + if (!venuesFromCache(_venuesNoSearchLocation)) { + venuesRequest(_venuesNoSearchLocation); + } + } + } + + if (shown + && !venuesFromCache( + *_venuesSearchLocation, + *_venuesSearchQuery)) { + _venueState = PickerVenueLoading(); + _venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay); + } else { + _venuesSearchDebounceTimer.cancel(); + } +} + +void LocationPicker::resolveCurrentLocation() { + using namespace Core; + const auto window = _window.get(); + ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) { + const auto changed = !AreTheSame(LastExactLocation, location); + if (location.accuracy != GeoLocationAccuracy::Exact || !changed) { + if (!_venuesSearchLocation) { + _venueState = PickerVenueWaitingForLocation(); + } + return; + } + LastExactLocation = location; + if (location) { + if (_venuesSearchQuery.value_or(QString()).isEmpty()) { + venuesRequest(location); + } + resolveAddress(location); + } + if (_webview) { + const auto point = QByteArray::number(location.point.x()) + + ","_q + + QByteArray::number(location.point.y()); + _webview->eval("LocationPicker.narrowTo([" + point + "]);"); + } + })); +} + +void LocationPicker::processKey( + const QString &key, + const QString &modifier) { + const auto ctrl = ::Platform::IsMac() ? u"cmd"_q : u"ctrl"_q; + if (key == u"escape"_q) { + if (!_window->closeSearch()) { + close(); + } + } else if (key == u"w"_q && modifier == ctrl) { + close(); + } else if (key == u"m"_q && modifier == ctrl) { + minimize(); + } else if (key == u"q"_q && modifier == ctrl) { + quit(); + } +} + +void LocationPicker::activate() { + if (_window) { + _window->activateWindow(); + } +} + +void LocationPicker::close() { + crl::on_main(this, [=] { + _window = nullptr; + delete this; + }); +} + +void LocationPicker::minimize() { + if (_window) { + _window->setWindowState(_window->windowState() + | Qt::WindowMinimized); + } +} + +void LocationPicker::quit() { + if (const auto onstack = _quit) { + onstack(); + } +} + +not_null LocationPicker::Show(Descriptor &&descriptor) { + return new LocationPicker(std::move(descriptor)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h new file mode 100644 index 000000000..943e7ac75 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/location_picker.h @@ -0,0 +1,175 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/invoke_queued.h" +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "core/current_geo_location.h" +#include "mtproto/sender.h" +#include "webview/webview_common.h" + +namespace Data { +struct InputVenue; +} // namespace Data + +namespace Main { +class Session; +class SessionShow; +} // namespace Main + +namespace Webview { +class Window; +} // namespace Webview + +namespace Ui { + +class AbstractButton; +class SeparatePanel; +class RpWidget; +class ScrollArea; +class VerticalLayout; +template +class SlideWrap; + +struct PickerVenueLoading { + friend inline bool operator==( + PickerVenueLoading, + PickerVenueLoading) = default; +}; + +struct PickerVenueNothingFound { + QString query; + + friend inline bool operator==( + const PickerVenueNothingFound&, + const PickerVenueNothingFound&) = default; +}; + +struct PickerVenueWaitingForLocation { + friend inline bool operator==( + PickerVenueWaitingForLocation, + PickerVenueWaitingForLocation) = default; +}; + +struct PickerVenueList { + std::vector list; + + friend inline bool operator==( + const PickerVenueList&, + const PickerVenueList&) = default; +}; + +using PickerVenueState = std::variant< + PickerVenueLoading, + PickerVenueNothingFound, + PickerVenueWaitingForLocation, + PickerVenueList>; + +struct LocationPickerConfig { + QString mapsToken; + QString geoToken; +}; + +class LocationPicker final : public base::has_weak_ptr { +public: + struct Descriptor { + RpWidget *parent = nullptr; + LocationPickerConfig config; + rpl::producer chooseLabel; + PeerData *recipient = nullptr; + not_null session; + Core::GeoLocation initial; + Fn callback; + Fn quit; + Webview::StorageId storageId; + rpl::producer<> closeRequests; + }; + + [[nodiscard]] static bool Available(const LocationPickerConfig &config); + static not_null Show(Descriptor &&descriptor); + + void activate(); + void close(); + void minimize(); + void quit(); + +private: + struct VenuesCacheEntry { + Core::GeoLocation location; + PickerVenueList result; + }; + + explicit LocationPicker(Descriptor &&descriptor); + + [[nodiscard]] std::shared_ptr uiShow(); + + void setup(const Descriptor &descriptor); + void setupWindow(const Descriptor &descriptor); + void setupWebview(); + void processKey(const QString &key, const QString &modifier); + void resolveCurrentLocation(); + void resolveAddressByTimer(); + void resolveAddress(Core::GeoLocation location); + void mapReady(); + + bool venuesFromCache(Core::GeoLocation location, QString query = {}); + void venuesRequest(Core::GeoLocation location, QString query = {}); + void venuesSendRequest(); + void venuesApplyResults(PickerVenueList venues); + void venuesSearchEnableAt(Core::GeoLocation location); + void venuesSearchChanged(const std::optional &query); + + LocationPickerConfig _config; + Fn _callback; + Fn _quit; + std::unique_ptr _window; + not_null _body; + RpWidget *_container = nullptr; + RpWidget *_mapPlaceholder = nullptr; + RpWidget *_mapLoading = nullptr; + AbstractButton *_mapButton = nullptr; + SlideWrap *_mapControlsWrap = nullptr; + rpl::variable _chooseButtonLabel; + ScrollArea *_scroll = nullptr; + Webview::StorageId _webviewStorageId; + std::unique_ptr _webview; + SingleQueuedInvokation _updateStyles; + Core::GeoLocation _initialProvided; + int _mapPlaceholderAdded = 0; + bool _subscribedToColors = false; + + base::Timer _geocoderResolveTimer; + Core::GeoLocation _geocoderResolvePostponed; + Core::GeoLocation _geocoderResolvingFor; + QString _geocoderSavedAddress; + rpl::variable _geocoderAddress; + + rpl::variable _venueState; + + const not_null _session; + std::optional _venuesSearchLocation; + std::optional _venuesSearchQuery; + base::Timer _venuesSearchDebounceTimer; + MTP::Sender _api; + PeerData *_venueRecipient = nullptr; + UserData *_venuesBot = nullptr; + mtpRequestId _venuesBotRequestId = 0; + mtpRequestId _venuesRequestId = 0; + Core::GeoLocation _venuesRequestLocation; + QString _venuesRequestQuery; + QString _venuesInitialQuery; + base::flat_map> _venuesCache; + Core::GeoLocation _venuesNoSearchLocation; + rpl::variable _venuesSearchShown = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index e78642348..b982687d6 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -47,3 +47,13 @@ creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) { starIconSmall: icon{{ "payments/small_star", windowFg }}; starIconSmallPadding: margins(0px, -2px, 0px, 0px); + +creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }}; + +creditsHistoryEntryGiftStickerSkip: -20px; +creditsHistoryEntryGiftStickerSize: 150px; +creditsHistoryEntryGiftStickerSpace: 105px; + +creditsGiftBox: Box(defaultBox) { + shadowIgnoreTopSkip: true; +} diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index 9f48191f0..018188cb3 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -193,6 +193,21 @@ not_null AddInputFieldForCredits( PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const Data::CreditsHistoryEntry &entry) { + using PeerType = Data::CreditsHistoryEntry::PeerType; + if (entry.peerType == PeerType::PremiumBot) { + const auto svg = std::make_shared(Ui::Premium::Svg()); + return [=](Painter &p, int x, int y, int, int size) mutable { + const auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + { + auto gradient = QLinearGradient(x + size, y + size, x, y); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setBrush(gradient); + } + p.drawEllipse(x, y, size, size); + svg->render(&p, QRectF(x, y, size, size) - Margins(size / 5.)); + }; + } const auto bg = [&]() -> EmptyUserpic::BgColors { switch (entry.peerType) { case Data::CreditsHistoryEntry::PeerType::Peer: @@ -202,9 +217,11 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( case Data::CreditsHistoryEntry::PeerType::PlayMarket: return { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 }; case Data::CreditsHistoryEntry::PeerType::Fragment: - return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 }; + return { st::windowSubTextFg, st::imageBg }; case Data::CreditsHistoryEntry::PeerType::PremiumBot: return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 }; + case Data::CreditsHistoryEntry::PeerType::Ads: + return { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 }; case Data::CreditsHistoryEntry::PeerType::Unsupported: return { st::historyPeerArchiveUserpicBg, @@ -216,10 +233,6 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const auto userpic = std::make_shared(bg, QString()); return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { userpic->paintCircle(p, x, y, outerWidth, size); - using PeerType = Data::CreditsHistoryEntry::PeerType; - if (entry.peerType == PeerType::PremiumBot) { - return; - } const auto rect = QRect(x, y, size, size); ((entry.peerType == PeerType::AppStore) ? st::sessionIconiPhone @@ -227,6 +240,8 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( ? st::sessionIconAndroid : (entry.peerType == PeerType::Fragment) ? st::introFragmentIcon + : (entry.peerType == PeerType::Ads) + ? st::creditsHistoryEntryTypeAds : st::dialogsInaccessibleUserpic).paintInCenter(p, rect); }; } @@ -440,8 +455,14 @@ Fn)> PaintPreviewCallback( } TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) { - return ((entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment) - ? tr::lng_bot_username_description1_link + return (entry.gift + ? tr::lng_credits_box_history_entry_gift_name + : (entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment) + ? tr::lng_credits_box_history_entry_fragment + : (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) + ? tr::lng_credits_box_history_entry_premium_bot + : (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads) + ? tr::lng_credits_box_history_entry_ads : tr::lng_credits_summary_history_entry_inner_in)( tr::now, TextWithEntities::Simple); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index bf7379e41..28d4ea077 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -364,3 +364,4 @@ boostFeatureLink: icon{{ "settings/premium/features/feature_links", windowBgActi boostFeatureName: icon{{ "settings/premium/features/feature_color_names", windowBgActive }}; boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowBgActive }}; boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }}; +boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }}; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index d95062313..7ebd961f3 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -146,11 +146,11 @@ QString FillAmountAndCurrency( // std::abs doesn't work on that one :/ Expects(amount != std::numeric_limits::min()); - const auto rule = LookupCurrencyRule(currency); if (currency == kCreditsCurrency) { return QChar(0x2B50) + Lang::FormatCountDecimal(std::abs(amount)); } + const auto rule = LookupCurrencyRule(currency); const auto prefix = (amount < 0) ? QString::fromUtf8("\xe2\x88\x92") : QString(); diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp index 11347aa61..6666077dd 100644 --- a/Telegram/SourceFiles/ui/vertical_list.cpp +++ b/Telegram/SourceFiles/ui/vertical_list.cpp @@ -28,31 +28,34 @@ void AddDivider(not_null container) { container->add(object_ptr(container)); } -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins, RectParts parts) { - AddDividerText( + return AddDividerText( container, std::move(text) | Ui::Text::ToWithEntities(), margins, parts); } -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins, RectParts parts) { + auto label = object_ptr( + container, + std::move(text), + st::boxDividerLabel); + const auto result = label.data(); container->add(object_ptr( container, - object_ptr( - container, - std::move(text), - st::boxDividerLabel), + std::move(label), margins, parts)); + return result; } not_null AddSubsectionTitle( diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h index 7ab743bd3..82934367f 100644 --- a/Telegram/SourceFiles/ui/vertical_list.h +++ b/Telegram/SourceFiles/ui/vertical_list.h @@ -25,12 +25,12 @@ class VerticalLayout; void AddSkip(not_null container); void AddSkip(not_null container, int skip); void AddDivider(not_null container); -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins = st::defaultBoxDividerLabelPadding, RectParts parts = RectPart::Top | RectPart::Bottom); -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins = st::defaultBoxDividerLabelPadding, diff --git a/Telegram/SourceFiles/ui/webview_helpers.cpp b/Telegram/SourceFiles/ui/webview_helpers.cpp new file mode 100644 index 000000000..3be7a6359 --- /dev/null +++ b/Telegram/SourceFiles/ui/webview_helpers.cpp @@ -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 "ui/webview_helpers.h" + +#include "lang/lang_keys.h" + +namespace Ui { +namespace { + +[[nodiscard]] QByteArray Serialize(const QColor &qt) { + if (qt.alpha() == 255) { + return '#' + + QByteArray::number(qt.red(), 16).right(2) + + QByteArray::number(qt.green(), 16).right(2) + + QByteArray::number(qt.blue(), 16).right(2); + } + return "rgba(" + + QByteArray::number(qt.red()) + "," + + QByteArray::number(qt.green()) + "," + + QByteArray::number(qt.blue()) + "," + + QByteArray::number(qt.alpha() / 255.) + ")"; +} + +} // namespace + +QByteArray ComputeStyles( + const base::flat_map &colors, + const base::flat_map> &phrases, + bool nightTheme) { + static const auto serialize = [](const style::color *color) { + return Serialize((*color)->c); + }; + static const auto escape = [](tr::phrase<> phrase) { + const auto text = phrase(tr::now); + + auto result = QByteArray(); + for (auto i = 0; i != text.size(); ++i) { + uint ucs4 = text[i].unicode(); + if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) { + ushort low = text[i + 1].unicode(); + if (QChar::isLowSurrogate(low)) { + ucs4 = QChar::surrogateToUcs4(ucs4, low); + ++i; + } + } + if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') { + result.append('\\').append(char(ucs4)); + } else if (ucs4 < 32 || ucs4 > 127) { + result.append('\\' + QByteArray::number(ucs4, 16) + ' '); + } else { + result.append(char(ucs4)); + } + } + return result; + }; + auto result = QByteArray(); + for (const auto &[name, phrase] : phrases) { + result += "--td-lng-"_q + name + ":'"_q + escape(phrase) + "'; "_q; + } + for (const auto &[name, color] : colors) { + result += "--td-"_q + name + ':' + serialize(color) + ';'; + } + result += "--td-night:"_q + (nightTheme ? "1" : "0") + ';'; + return result; +} + +QByteArray ComputeSemiTransparentOverStyle( + const QByteArray &name, + const style::color &over, + const style::color &bg) { + const auto result = [&](const QColor &c) { + return "--td-"_q + name + ':' + Serialize(c) + ';'; + }; + if (over->c.alpha() < 255) { + return result(over->c); + } + // The most transparent color that will still give the same result. + const auto r0 = bg->c.red(); + const auto g0 = bg->c.green(); + const auto b0 = bg->c.blue(); + const auto r1 = over->c.red(); + const auto g1 = over->c.green(); + const auto b1 = over->c.blue(); + const auto mina = [](int c0, int c1) { + return (c0 == c1) + ? 0 + : (c0 > c1) + ? (((c0 - c1) * 255) / c0) + : (((c1 - c0) * 255) / (255 - c0)); + }; + const auto rmina = mina(r0, r1); + const auto gmina = mina(g0, g1); + const auto bmina = mina(b0, b1); + const auto a = std::max({ rmina, gmina, bmina }); + const auto r = (r1 * 255 - r0 * (255 - a)) / a; + const auto g = (g1 * 255 - g0 * (255 - a)) / a; + const auto b = (b1 * 255 - b0 * (255 - a)) / a; + return result(QColor(r, g, b, a)); +} + +QByteArray EscapeForAttribute(QByteArray value) { + return value + .replace('&', "&") + .replace('"', """) + .replace('\'', "'") + .replace('<', "<") + .replace('>', ">"); +} + +QByteArray EscapeForScriptString(QByteArray value) { + return value + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\'', "\\\'"); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/webview_helpers.h b/Telegram/SourceFiles/ui/webview_helpers.h new file mode 100644 index 000000000..518096479 --- /dev/null +++ b/Telegram/SourceFiles/ui/webview_helpers.h @@ -0,0 +1,31 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/flat_map.h" + +namespace tr { +template +struct phrase; +} // namespace tr + +namespace Ui { + +[[nodiscard]] QByteArray ComputeStyles( + const base::flat_map &colors, + const base::flat_map> &phrases, + bool nightTheme = false); +[[nodiscard]] QByteArray ComputeSemiTransparentOverStyle( + const QByteArray &name, + const style::color &over, + const style::color &bg); + +[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value); +[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value); + +} // namespace Ui diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index 02c4e528f..f3ddf7ef3 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/section_widget.h" #include "mainwidget.h" +#include "mainwindow.h" #include "ui/ui_utility.h" #include "ui/chat/chat_theme.h" #include "ui/painter.h" @@ -460,7 +461,11 @@ void SectionWidget::showFinished() { showChildren(); showFinishedHook(); - setInnerFocus(); + if (isAncestorOf(window()->focusWidget())) { + setInnerFocus(); + } else { + controller()->widget()->setInnerFocus(); + } } rpl::producer SectionWidget::desiredHeight() const { diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 8ef614d21..ec9fa548e 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -499,6 +499,15 @@ void Controller::invokeForSessionController( if (separateSession) { return callback(separateSession); } + const auto accountWindow = account + ? Core::App().separateWindowFor(not_null(account)) + : nullptr; + const auto accountSession = accountWindow + ? accountWindow->sessionController() + : nullptr; + if (accountSession) { + return callback(accountSession); + } _id.account->domain().activate(std::move(account)); if (_sessionController) { callback(_sessionController.get()); diff --git a/Telegram/SourceFiles/window/window_main_menu_helpers.cpp b/Telegram/SourceFiles/window/window_main_menu_helpers.cpp index ba42403d4..5803b851e 100644 --- a/Telegram/SourceFiles/window/window_main_menu_helpers.cpp +++ b/Telegram/SourceFiles/window/window_main_menu_helpers.cpp @@ -367,12 +367,15 @@ void SetupMenuBots( (height - icon->height()) / 2); }, button->lifetime()); const auto weak = Ui::MakeWeak(container); + const auto show = controller->uiShow(); button->setAcceptBoth(true); button->clicks( ) | rpl::start_with_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { - bots->requestSimple(controller, user, { - .fromMainMenu = true, + bots->open({ + .bot = user, + .context = { .controller = controller }, + .source = InlineBots::WebViewSourceMainMenu(), }); if (weak) { controller->window().hideSettingsAndLayer(); @@ -384,7 +387,7 @@ void SetupMenuBots( st::popupMenuWithIcons); (*menu)->addAction( tr::lng_bot_remove_from_menu(tr::now), - [=] { bots->removeFromMenu(user); }, + [=] { bots->removeFromMenu(show, user); }, &st::menuIconDelete); (*menu)->popup(QCursor::pos()); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 9398d33a1..c6bea0d52 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1169,6 +1169,8 @@ void Filler::addViewStatistics() { using Flag = ChannelDataFlag; const auto canGetStats = (channel->flags() & Flag::CanGetStatistics); const auto canViewEarn = (channel->flags() & Flag::CanViewRevenue); + const auto canViewCreditsEarn + = (channel->flags() & Flag::CanViewCreditsRevenue); if (canGetStats) { _addAction(tr::lng_stats_title(tr::now), [=] { if (const auto strong = weak.get()) { @@ -1186,7 +1188,7 @@ void Filler::addViewStatistics() { } }, &st::menuIconBoosts); } - if (canViewEarn) { + if (canViewEarn || canViewCreditsEarn) { _addAction(tr::lng_channel_earn_title(tr::now), [=] { if (const auto strong = weak.get()) { controller->showSection(Info::ChannelEarn::Make(peer)); @@ -1553,7 +1555,8 @@ void Filler::fillArchiveActions() { const auto controller = _controller; const auto hidden = controller->session().settings().archiveCollapsed(); - { + const auto inmenu = controller->session().settings().archiveInMainMenu(); + if (!inmenu) { const auto text = hidden ? tr::lng_context_archive_expand(tr::now) : tr::lng_context_archive_collapse(tr::now); @@ -1562,7 +1565,6 @@ void Filler::fillArchiveActions() { controller->session().saveSettingsDelayed(); }, hidden ? &st::menuIconExpand : &st::menuIconCollapse); } - const auto inmenu = controller->session().settings().archiveInMainMenu(); { const auto text = inmenu ? tr::lng_context_archive_to_list(tr::now) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 34c7f9c0b..337495e10 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -581,6 +581,8 @@ void SessionNavigation::showPeerByLinkResolved( info.messageId, commentId->id, params); + } else if (resolveType == ResolveType::Profile) { + showPeerInfo(peer, params); } else if (peer->isForum() && resolveType != ResolveType::Boost) { const auto itemId = info.messageId; if (!itemId) { @@ -618,17 +620,23 @@ void SessionNavigation::showPeerByLinkResolved( const auto contextPeer = item ? item->history()->peer : bot; - const auto action = bot->session().attachWebView().lookupLastAction( - info.clickFromAttachBotWebviewUrl - ).value_or(Api::SendAction(bot->owner().history(contextPeer))); + const auto action = info.clickFromBotWebviewContext + ? info.clickFromBotWebviewContext->action + : Api::SendAction(bot->owner().history(contextPeer)); crl::on_main(this, [=] { - bot->session().attachWebView().requestApp( - parentController(), - action, - bot, - info.botAppName, - info.startToken, - info.botAppForceConfirmation); + bot->session().attachWebView().open({ + .bot = bot, + .context = { + .controller = parentController(), + .action = action, + .maySkipConfirmation = !info.botAppForceConfirmation, + }, + .button = { .startCommand = info.startToken }, + .source = InlineBots::WebViewSourceLinkApp{ + .appname = info.botAppName, + .token = info.startToken, + }, + }); }); } else if (bot && resolveType == ResolveType::ShareGame) { Window::ShowShareGameBox(parentController(), bot, info.startToken); @@ -676,20 +684,25 @@ void SessionNavigation::showPeerByLinkResolved( crl::on_main(this, [=] { const auto history = peer->owner().history(peer); showPeerHistory(history, params, msgId); - peer->session().attachWebView().request( + + peer->session().attachWebView().openByUsername( parentController(), Api::SendAction(history), attachBotUsername, info.attachBotToggleCommand.value_or(QString())); }); - } else if (bot && info.attachBotMenuOpen) { + } else if (bot && info.attachBotMainOpen) { const auto startCommand = info.attachBotToggleCommand.value_or( QString()); - bot->session().attachWebView().requestAddToMenu( - bot, - InlineBots::AddToMenuOpenMenu{ startCommand }, - parentController(), - std::optional()); + bot->session().attachWebView().open({ + .bot = bot, + .context = { .controller = parentController() }, + .button = { .startCommand = startCommand }, + .source = InlineBots::WebViewSourceLinkBotProfile{ + .token = startCommand, + .compact = info.attachBotMainCompact, + }, + }); } else if (bot && info.attachBotToggleCommand) { const auto itemId = info.clickFromMessageId; const auto item = _session->data().message(itemId); @@ -699,17 +712,21 @@ void SessionNavigation::showPeerByLinkResolved( const auto contextUser = contextPeer ? contextPeer->asUser() : nullptr; - bot->session().attachWebView().requestAddToMenu( - bot, - InlineBots::AddToMenuOpenAttach{ - .startCommand = *info.attachBotToggleCommand, - .chooseTypes = info.attachBotChooseTypes, + bot->session().attachWebView().open({ + .bot = bot, + .context = { + .controller = parentController(), + .action = (contextUser + ? Api::SendAction( + contextUser->owner().history(contextUser)) + : std::optional()), }, - parentController(), - (contextUser - ? Api::SendAction( - contextUser->owner().history(contextUser)) - : std::optional())); + .button = { .startCommand = *info.attachBotToggleCommand }, + .source = InlineBots::WebViewSourceLinkAttachMenu{ + .choose = info.attachBotChooseTypes, + .token = *info.attachBotToggleCommand, + }, + }); } else { const auto draft = info.text; crl::on_main(this, [=] { diff --git a/Telegram/SourceFiles/window/window_session_controller_link_info.h b/Telegram/SourceFiles/window/window_session_controller_link_info.h index 2c7457122..d64d82c53 100644 --- a/Telegram/SourceFiles/window/window_session_controller_link_info.h +++ b/Telegram/SourceFiles/window/window_session_controller_link_info.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace InlineBots { +struct WebViewContext; +} // namespace InlineBots + namespace Window { enum class ResolveType { @@ -18,6 +22,7 @@ enum class ResolveType { ShareGame, Mention, Boost, + Profile, }; struct CommentId { @@ -45,11 +50,12 @@ struct PeerByLinkInfo { bool botAppForceConfirmation = false; QString attachBotUsername; std::optional attachBotToggleCommand; - bool attachBotMenuOpen = false; + bool attachBotMainOpen = false; + bool attachBotMainCompact = false; InlineBots::PeerTypes attachBotChooseTypes; std::optional voicechatHash; FullMsgId clickFromMessageId; - QString clickFromAttachBotWebviewUrl; + std::shared_ptr clickFromBotWebviewContext; }; } // namespace Window diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist index 53b8daf8b..72913e85e 100644 --- a/Telegram/Telegram.plist +++ b/Telegram/Telegram.plist @@ -12,12 +12,20 @@ Icon.icns CFBundleIdentifier @bundle_identifier_plist@ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + @output_name@ CFBundlePackageType APPL CFBundleShortVersionString @desktop_app_version_string@ CFBundleSignature ???? + CFBundleSupportedPlatforms + + MacOSX + CFBundleURLTypes @@ -30,6 +38,7 @@ CFBundleURLSchemes tg + tonsite @@ -39,16 +48,18 @@ LSApplicationCategoryType public.app-category.social-networking - LSMinimumSystemVersion - @CMAKE_OSX_DEPLOYMENT_TARGET@ LSFileQuarantineEnabled + LSMinimumSystemVersion + @CMAKE_OSX_DEPLOYMENT_TARGET@ NOTE NSMicrophoneUsageDescription We need access to your microphone so that you can record voice messages and make calls. NSCameraUsageDescription We need access to your camera so that you can record video messages and make video calls. + NSLocationUsageDescription + We need access to your location so that you can send your current locations. NSPrincipalClass NSApplication NSSupportsAutomaticGraphicsSwitching diff --git a/Telegram/Telegram/Telegram Lite.entitlements b/Telegram/Telegram/Telegram Lite.entitlements index 46355b637..050eea08f 100644 --- a/Telegram/Telegram/Telegram Lite.entitlements +++ b/Telegram/Telegram/Telegram Lite.entitlements @@ -18,5 +18,7 @@ com.apple.security.device.camera + com.apple.security.personal-information.location + diff --git a/Telegram/Telegram/Telegram.entitlements b/Telegram/Telegram/Telegram.entitlements index 97c1f6d58..af2883220 100644 --- a/Telegram/Telegram/Telegram.entitlements +++ b/Telegram/Telegram/Telegram.entitlements @@ -6,5 +6,7 @@ com.apple.security.device.camera + com.apple.security.personal-information.location + diff --git a/Telegram/ThirdParty/libtgvoip b/Telegram/ThirdParty/libtgvoip index 25facad34..2d2592860 160000 --- a/Telegram/ThirdParty/libtgvoip +++ b/Telegram/ThirdParty/libtgvoip @@ -1 +1 @@ -Subproject commit 25facad342c3280315f9ef553906f46c3eeba1e4 +Subproject commit 2d2592860478e60d972b96e67ee034b8a71bb57a diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index b9fa8b84d..9bf4065ea 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit b9fa8b84d8abe741183f157218ac038c596a54a5 +Subproject commit 9bf4065ea00cbed5e63cec348457ed13143459d0 diff --git a/Telegram/build/build.bat b/Telegram/build/build.bat index 3f3b65caa..6dc539c61 100644 --- a/Telegram/build/build.bat +++ b/Telegram/build/build.bat @@ -14,42 +14,54 @@ if not exist "%FullScriptPath%..\..\..\DesktopPrivate" ( FOR /F "tokens=1* delims= " %%i in (%FullScriptPath%target) do set "BuildTarget=%%i" -if "%BuildTarget%" equ "uwp" ( - set "BuildUWP=1" -) else if "%BuildTarget%" equ "uwp64" ( - set "BuildUWP=1" -) else ( - set "BuildUWP=0" -) - +set "Build64=0" +set "BuildARM=0" +set "BuildUWP=0" if "%BuildTarget%" equ "win64" ( set "Build64=1" +) else if "%BuildTarget%" equ "winarm" ( + set "BuildARM=1" +) else if "%BuildTarget%" equ "uwp" ( + set "BuildUWP=1" ) else if "%BuildTarget%" equ "uwp64" ( set "Build64=1" -) else ( - set "Build64=0" + set "BuildUWP=1" +) else if "%BuildTarget%" equ "uwparm" ( + set "BuildARM=1" + set "BuildUWP=1" ) if %Build64% neq 0 ( if "%Platform%" neq "x64" ( - echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'. exit /b ) else if "%VSCMD_ARG_HOST_ARCH%" neq "x64" ( - echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'. exit /b ) else if "%VSCMD_ARG_TGT_ARCH%" neq "x64" ( - echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'. + exit /b + ) +) else if %BuildARM% neq 0 ( + if "%Platform%" neq "arm64" ( + echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'. + exit /b + ) else if "%VSCMD_ARG_HOST_ARCH%" neq "arm64" ( + echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'. + exit /b + ) else if "%VSCMD_ARG_TGT_ARCH%" neq "arm64" ( + echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'. exit /b ) ) else ( if "%Platform%" neq "x86" ( - echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'. exit /b ) else if "%VSCMD_ARG_HOST_ARCH%" neq "x86" ( - echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'. exit /b ) else if "%VSCMD_ARG_TGT_ARCH%" neq "x86" ( - echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'. + echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'. exit /b ) ) @@ -76,12 +88,16 @@ echo. if %BuildUWP% neq 0 ( if %Build64% neq 0 ( echo Building version %AppVersionStrFull% for UWP 64 bit.. + ) else if %BuildARM% neq 0 ( + echo Building version %AppVersionStrFull% for UWP ARM.. ) else ( echo Building version %AppVersionStrFull% for UWP.. ) ) else ( if %Build64% neq 0 ( echo Building version %AppVersionStrFull% for Windows 64 bit.. + ) else if %BuildARM% neq 0 ( + echo Building version %AppVersionStrFull% for Windows on ARM.. ) else ( echo Building version %AppVersionStrFull% for Windows.. ) @@ -96,6 +112,11 @@ if %Build64% neq 0 ( set "SetupFile=tsetup-x64.%AppVersionStrFull%.exe" set "PortableFile=tportable-x64.%AppVersionStrFull%.zip" set "DumpSymsPath=%SolutionPath%\..\..\Libraries\win64\breakpad\src\tools\windows\dump_syms\Release\dump_syms.exe" +) else if %BuildARM% neq 0 ( + set "UpdateFile=tarm64upd%AppVersion%" + set "SetupFile=tsetup-arm64.%AppVersionStrFull%.exe" + set "PortableFile=tportable-arm64.%AppVersionStrFull%.zip" + set "DumpSymsPath=%SolutionPath%\..\..\Libraries\breakpad\src\tools\windows\dump_syms\Release\dump_syms.exe" ) else ( set "UpdateFile=tupdate%AppVersion%" set "SetupFile=tsetup.%AppVersionStrFull%.exe" @@ -210,7 +231,11 @@ if %BuildUWP% equ 0 ( if not exist "%SetupFile%" goto error ) - call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -path "modules\%Platform%\d3d\d3dcompiler_47.dll" -target %BuildTarget% %AlphaBetaParam% + if %BuildARM% neq 0 ( + call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -target %BuildTarget% %AlphaBetaParam% + ) else ( + call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -path "modules\%Platform%\d3d\d3dcompiler_47.dll" -target %BuildTarget% %AlphaBetaParam% + ) if %errorlevel% neq 0 goto error if %AlphaVersion% neq 0 ( @@ -309,10 +334,12 @@ if %BuildUWP% neq 0 ( if %errorlevel% neq 0 goto error ) -if %Build64% equ 0 ( - set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tsetup" -) else ( +if %Build64% neq 0 ( set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tx64" +) else if %BuildARM% neq 0 ( + set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tarm64" +) else ( + set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tsetup" ) if %BuildUWP% equ 0 ( diff --git a/Telegram/build/build.sh b/Telegram/build/build.sh index 08bd287ad..aa2dcc1ec 100755 --- a/Telegram/build/build.sh +++ b/Telegram/build/build.sh @@ -327,7 +327,7 @@ if [ "$BuildTarget" == "mac" ] || [ "$BuildTarget" == "macstore" ]; then echo "Signing the application.." if [ "$BuildTarget" == "mac" ]; then - codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: John Preston" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram.entitlements" + codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram.entitlements" elif [ "$BuildTarget" == "macstore" ]; then codesign --force --timestamp --options runtime --sign "3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName/Contents/Frameworks/Breakpad.framework/Versions/A/Resources/breakpadUtilities.dylib" --entitlements "$HomePath/Telegram/Breakpad.entitlements" codesign --force --deep --timestamp --options runtime --sign "3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram Lite.entitlements" diff --git a/Telegram/build/deploy.sh b/Telegram/build/deploy.sh index 23e8143e3..e3217ca40 100755 --- a/Telegram/build/deploy.sh +++ b/Telegram/build/deploy.sh @@ -49,6 +49,7 @@ HomePath="$FullScriptPath/.." DeployMac="0" DeployWin="0" DeployWin64="0" +DeployWinArm="0" DeployLinux="0" if [ "$DeployTarget" == "mac" ]; then DeployMac="1" @@ -59,6 +60,9 @@ elif [ "$DeployTarget" == "win" ]; then elif [ "$DeployTarget" == "win64" ]; then DeployWin64="1" echo "Deploying version $AppVersionStrFull for Windows 64 bit.." +elif [ "$DeployTarget" == "winarm" ]; then + DeployWinArm="1" + echo "Deploying version $AppVersionStrFull for Windows on ARM.." elif [ "$DeployTarget" == "linux" ]; then DeployLinux="1" echo "Deploying version $AppVersionStrFull for Linux 64 bit.." @@ -66,6 +70,7 @@ else DeployMac="1" DeployWin="1" DeployWin64="1" + DeployWinArm="0" DeployLinux="1" echo "Deploying four versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, macOS and Linux 64 bit.." fi @@ -94,6 +99,11 @@ Win64UpdateFile="tx64upd$AppVersion" Win64SetupFile="tsetup-x64.$AppVersionStrFull.exe" Win64PortableFile="tportable-x64.$AppVersionStrFull.zip" Win64RemoteFolder="tx64" +WinArmDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tarm64" +WinArmUpdateFile="tarm64upd$AppVersion" +WinArmSetupFile="tsetup-arm64.$AppVersionStrFull.exe" +WinArmPortablefile="tportable-arm64.$AppVersionStrFull.zip" +WinArmRemoteFolder="tarm64" LinuxDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tlinux" LinuxUpdateFile="tlinuxupd$AppVersion" LinuxSetupFile="tsetup.$AppVersionStrFull.tar.xz" @@ -105,6 +115,8 @@ if [ "$AlphaVersion" != "0" ]; then AlphaFilePath="$WinDeployPath/$AlphaKeyFile" elif [ "$DeployTarget" == "win64" ]; then AlphaFilePath="$Win64DeployPath/$AlphaKeyFile" + elif [ "$DeployTarget" == "winarm" ]; then + AlphaFilePath="$WinArmDeployPath/$AlphaKeyFile" elif [ "$DeployTarget" == "linux" ]; then AlphaFilePath="$LinuxDeployPath/$AlphaKeyFile" else @@ -125,6 +137,8 @@ if [ "$AlphaVersion" != "0" ]; then WinPortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip" Win64UpdateFile="${Win64UpdateFile}_${AlphaSignature}" Win64PortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip" + WinArmUpdateFile="${WinArmUpdateFile}_${AlphaSignature}" + WinArmPortablefile="talpha${AlphaVersion}_${AlphaSignature}.zip" LinuxUpdateFile="${LinuxUpdateFile}_${AlphaSignature}" LinuxSetupFile="talpha${AlphaVersion}_${AlphaSignature}.tar.xz" fi @@ -166,6 +180,19 @@ if [ "$DeployWin64" == "1" ]; then Error "$Win64PortableFile not found!" fi fi +if [ "$DeployWinArm" == "1" ]; then + if [ ! -f "$WinArmDeployPath/$WinArmUpdateFile" ]; then + Error "$WinArmUpdateFile not found!" + fi + if [ "$AlphaVersion" == "0" ]; then + if [ ! -f "$WinArmDeployPath/$WinArmSetupFile" ]; then + Error "$WinArmSetupFile not found!" + fi + fi + if [ ! -f "$WinArmDeployPath/$WinArmPortableFile" ]; then + Error "$WinArmPortableFile not found!" + fi +fi if [ "$DeployLinux" == "1" ]; then if [ ! -f "$LinuxDeployPath/$LinuxUpdateFile" ]; then Error "$LinuxDeployPath/$LinuxUpdateFile not found!" @@ -193,6 +220,12 @@ if [ "$DeployWin64" == "1" ]; then Files+=("tx64/$Win64SetupFile") fi fi +if [ "$DeployWinArm" == "1" ]; then + Files+=("tarm64/$WinArmUpdateFile" "tarm64/$WinArmPortableFile") + if [ "$AlphaVersion" == "0" ]; then + Files+=("tarm64/$WinArmSetupFile") + fi +fi if [ "$DeployLinux" == "1" ]; then Files+=("tlinux/$LinuxUpdateFile" "tlinux/$LinuxSetupFile") fi diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 2b0c82ed0..82f184cb8 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,16 +1,14 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} +{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git config -f .gitmodules submodule.m4.shallow true && git submodule init && git submodule update" -%} {%- set QT = "6.7.2" -%} {%- set QT_TAG = "v" ~ QT -%} -{%- set CMAKE_VER = "3.27.6" -%} -{%- set CMAKE_FILE = "cmake-" ~ CMAKE_VER ~ "-Linux-x86_64.sh" -%} -{%- set CFLAGS_DEBUG = "-g -pipe -fPIC -fstack-protector-all -fstack-clash-protection -fcf-protection -D_GLIBCXX_ASSERTIONS" -%} -{%- set CFLAGS_LTO = "-flto=auto -ffat-lto-objects" -%} +{%- set CFLAGS_DEBUG = "$CFLAGS -O0 -fno-lto -U_FORTIFY_SOURCE" -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} # syntax=docker/dockerfile:1 -FROM rockylinux:8 AS builder-base +FROM rockylinux:8 AS builder ENV LANG C.UTF-8 ENV LIBRARY_PATH /usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib ENV LD_LIBRARY_PATH $LIBRARY_PATH @@ -18,7 +16,7 @@ ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/loc RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ - && dnf -y install autoconf automake libtool pkgconfig make patch git \ + && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang lld nasm yasm \ file which perl-open perl-XML-Parser perl-IPC-Cmd xorg-x11-util-macros \ gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils \ @@ -34,24 +32,17 @@ WORKDIR {{ LibrariesPath }} RUN python3 -m pip install meson ninja -RUN mkdir /opt/cmake \ - && curl -sSLo {{ CMAKE_FILE }} {{ GIT }}/Kitware/CMake/releases/download/v{{ CMAKE_VER }}/{{ CMAKE_FILE }} \ - && sh {{ CMAKE_FILE }} --prefix=/opt/cmake --skip-license \ - && ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake \ - && rm {{ CMAKE_FILE }} - -FROM builder-base AS builder ENV AR gcc-ar ENV RANLIB gcc-ranlib ENV NM gcc-nm -ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}{{ CFLAGS_LTO }}{% endif %} -pipe -fPIC -fno-omit-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -DNDEBUG -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS +ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS ENV CXXFLAGS $CFLAGS FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 20a7c5ffd8265fc6e45203ea2536f7b1965be19a \ + && git fetch --depth=1 origin 85a1c4ec327ed390a27e85f2162c31525220a50d \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -68,7 +59,7 @@ RUN git init zlib \ && rm -rf zlib FROM builder AS xz -RUN git clone -b v5.4.4 --depth=1 https://git.tukaani.org/xz.git \ +RUN git clone -b v5.4.4 --depth=1 {{ GIT }}/tukaani-project/xz.git \ && cd xz \ && cmake -B build . -DCMAKE_BUILD_TYPE=None \ && cmake --build build -j$(nproc) \ @@ -155,6 +146,17 @@ RUN git clone -b 1.4.1 --depth=1 {{ GIT }}/videolan/dav1d.git \ && cd .. \ && rm -rf dav1d +FROM builder AS openh264 +RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ + && cd openh264 \ + && meson build \ + --buildtype=plain \ + --default-library=both \ + && meson compile -C build \ + && DESTDIR="{{ LibrariesPath }}/openh264-cache" meson install -C build \ + && cd .. \ + && rm -rf openh264 + FROM builder AS libde265 RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ && cd libde265 \ @@ -250,7 +252,7 @@ COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -RUN git clone -b v0.10.2 --depth=1 {{ GIT }}/libjxl/libjxl.git \ +RUN git clone -b v0.10.3 --depth=1 {{ GIT }}/libjxl/libjxl.git \ && cd libjxl \ && git apply ../patches/libjxl.patch \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ @@ -310,8 +312,9 @@ RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \ && rm -rf libxcb FROM builder AS xcb-wm -RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ +RUN git clone -b xcb-util-wm-0.4.2 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ && cd libxcb-wm \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \ @@ -319,8 +322,9 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-util.git \ && cd libxcb-util \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \ @@ -330,8 +334,9 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GI FROM builder AS xcb-image COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-image.git \ && cd libxcb-image \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \ @@ -339,8 +344,9 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ +RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ && cd libxcb-keysyms \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \ @@ -348,8 +354,9 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ +RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ && cd libxcb-render-util \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \ @@ -361,8 +368,9 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ +RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ && cd libxcb-cursor \ + && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \ @@ -610,12 +618,13 @@ RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \ && rm -rf ffmpeg FROM builder AS pipewire -RUN git clone -b 0.3.25 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ +RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ && cd pipewire \ && meson build \ --buildtype=plain \ -Dtests=disabled \ -Dexamples=disabled \ + -Dsession-managers=media-session \ -Dspa-plugins=disabled \ && meson compile -C build \ && DESTDIR="{{ LibrariesPath }}/pipewire-cache" meson install -C build \ @@ -750,6 +759,7 @@ RUN git clone -b v2023.06.01 --depth=1 https://chromium.googlesource.com/breakpa FROM builder AS webrtc COPY --link --from=opus {{ LibrariesPath }}/opus-cache / +COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / @@ -761,7 +771,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / RUN git init tg_owt \ && cd tg_owt \ && git remote add origin {{ GIT }}/desktop-app/tg_owt.git \ - && git fetch --depth=1 origin c9cc4390ab951f2cbc103ff783a11f398b27660b \ + && git fetch --depth=1 origin 4a60ce1ab9fdb962004c6a959f682ace3db50cbd \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ && rm -rf .git \ @@ -775,6 +785,7 @@ RUN git init tg_owt \ -DTG_OWT_OPENSSL_INCLUDE_PATH=/usr/local/include \ -DTG_OWT_OPUS_INCLUDE_PATH=/usr/local/include/opus \ -DTG_OWT_LIBVPX_INCLUDE_PATH=/usr/local/include \ + -DTG_OWT_OPENH264_INCLUDE_PATH=/usr/local/include \ -DTG_OWT_FFMPEG_INCLUDE_PATH=/usr/local/include WORKDIR tg_owt @@ -790,7 +801,19 @@ RUN cmake --build out --config Debug --parallel \ && find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \; {%- endif %} -FROM builder-base +FROM builder AS ada +RUN git clone -b v2.9.0 --depth=1 {{ GIT }}/ada-url/ada.git \ + && cd ada \ + && cmake -GNinja -B build . \ + -D CMAKE_BUILD_TYPE=None \ + -D ADA_TESTING=OFF \ + -D ADA_TOOLS=OFF \ + && cmake --build build --parallel \ + && DESTDIR="{{ LibrariesPath }}/ada-cache" cmake --install build \ + && cd .. \ + && rm -rf ada + +FROM builder COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / COPY --link --from=xz {{ LibrariesPath }}/xz-cache / COPY --link --from=protobuf {{ LibrariesPath }}/protobuf-cache / @@ -799,6 +822,7 @@ COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / COPY --link --from=opus {{ LibrariesPath }}/opus-cache / COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / +COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / COPY --link --from=libavif {{ LibrariesPath }}/libavif-cache / @@ -832,6 +856,7 @@ COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache / COPY --link --from=webrtc {{ LibrariesPath }}/tg_owt tg_owt COPY --link --from=webrtc_release {{ LibrariesPath }}/tg_owt/out/Release tg_owt/out/Release COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache / +COPY --link --from=ada {{ LibrariesPath }}/ada-cache / {%- if DEBUG %} COPY --link --from=webrtc_debug {{ LibrariesPath }}/tg_owt/out/Debug tg_owt/out/Debug diff --git a/Telegram/build/docker/centos_env/build.sh b/Telegram/build/docker/centos_env/build.sh index 0bf1aae04..e7a34e6ae 100755 --- a/Telegram/build/docker/centos_env/build.sh +++ b/Telegram/build/docker/centos_env/build.sh @@ -3,4 +3,4 @@ set -e cd Telegram ./configure.sh "$@" -cmake --build ../out --config "${CONFIG:-RelWithDebInfo}" --parallel +cmake --build ../out --config "${CONFIG:-Release}" --parallel diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 60d60ea47..67e767003 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1,4 +1,4 @@ -import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob +import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob, tempfile executePath = os.getcwd() sys.dont_write_bytecode = True @@ -26,7 +26,7 @@ if win and not 'Platform' in os.environ: win32 = win and (os.environ['Platform'] == 'x86') win64 = win and (os.environ['Platform'] == 'x64') -winarm = win and (os.environ['Platform'] == 'arm') +winarm = win and (os.environ['Platform'] == 'arm64') arch = '' if win32: @@ -114,6 +114,12 @@ elif (win64): 'X8664': 'x64', 'WIN32X64': 'x64', }) +elif (winarm): + environment.update({ + 'SPECIAL_TARGET': 'winarm', + 'X8664': 'ARM64', + 'WIN32X64': 'ARM64', + }) elif (mac): environment.update({ 'SPECIAL_TARGET': 'mac', @@ -239,6 +245,8 @@ def filterByPlatform(commands): inscope = True if win64 and 'win64' in scopes: inscope = True + if winarm and 'winarm' in scopes: + inscope = True if mac and 'mac' in scopes: inscope = True # if linux and 'linux' in scopes: @@ -435,8 +443,12 @@ if customRunCommand: modifiedEnv['PROMPT'] = '(prepare) $P$G' subprocess.run("cmd.exe", shell=True, env=modifiedEnv) else: - modifiedEnv['PS1'] = '(prepare) \\w \\$ ' - subprocess.run("bash --noprofile --norc", env=modifiedEnv) + prompt = '(prepare) %~ %# ' + with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_zshrc: + tmp_zshrc.write(f'export PS1="{prompt}"\n') + tmp_zshrc_path = tmp_zshrc.name + subprocess.run(['zsh', '--rcs', tmp_zshrc_path], env=modifiedEnv) + os.remove(tmp_zshrc_path) elif not run(command): print('FAILED :(') finish(1) @@ -445,7 +457,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 20a7c5ffd8 + git checkout 85a1c4ec32 """) stage('msys64', """ @@ -456,12 +468,12 @@ win: SET CHERE_INVOKING=enabled_from_arguments SET MSYS2_PATH_TYPE=inherit - powershell -Command "iwr -OutFile ./msys64.exe https://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20221028.sfx.exe" + powershell -Command "iwr -OutFile ./msys64.exe https://github.com/msys2/msys2-installer/releases/download/2024-05-07/msys2-base-x86_64-20240507.sfx.exe" msys64.exe del msys64.exe bash -c "pacman-key --init; pacman-key --populate; pacman -Syu --noconfirm" - pacman -Syu --noconfirm mingw-w64-x86_64-perl mingw-w64-x86_64-nasm mingw-w64-x86_64-yasm mingw-w64-x86_64-ninja + pacman -Syu --noconfirm mingw-w64-x86_64-perl mingw-w64-x86_64-nasm mingw-w64-x86_64-yasm mingw-w64-x86_64-ninja msys/make diffutils pkg-config SET PATH=%PATH_BACKUP_% """, 'ThirdParty') @@ -498,9 +510,9 @@ mac: if not mac or 'build-stackwalk' in options: stage('gyp', """ win: - git clone https://chromium.googlesource.com/external/gyp + git clone https://github.com/desktop-app/gyp.git cd gyp - git checkout 9d09418933 + git checkout 618958fdbe mac: python3 -m pip install \\ --ignore-installed \\ @@ -528,7 +540,7 @@ release: stage('xz', """ !win: - git clone -b v5.4.5 https://git.tukaani.org/xz.git + git clone -b v5.4.5 https://github.com/tukaani-project/xz.git cd xz sed -i '' '\\@check_symbol_exists(futimens "sys/types.h;sys/stat.h" HAVE_FUTIMENS)@d' CMakeLists.txt CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\ @@ -606,8 +618,10 @@ win32: perl Configure no-shared no-tests debug-VC-WIN32 /FS win64: perl Configure no-shared no-tests debug-VC-WIN64A /FS +winarm: + perl Configure no-shared no-tests debug-VC-WIN64-ARM /FS win: - jom -j%NUMBER_OF_PROCESSORS% + jom -j%NUMBER_OF_PROCESSORS% build_libs mkdir out.dbg move libcrypto.lib out.dbg move libssl.lib out.dbg @@ -620,8 +634,10 @@ win32_release: perl Configure no-shared no-tests VC-WIN32 /FS win64_release: perl Configure no-shared no-tests VC-WIN64A /FS +winarm_release: + perl Configure no-shared no-tests VC-WIN64-ARM /FS win_release: - jom -j%NUMBER_OF_PROCESSORS% + jom -j%NUMBER_OF_PROCESSORS% build_libs mkdir out move libcrypto.lib out move libssl.lib out @@ -716,18 +732,32 @@ mac: make install """) +stage('gas-preprocessor', """ +win: + git clone https://github.com/FFmpeg/gas-preprocessor + cd gas-preprocessor + echo @echo off > cpp.bat + echo cl %%%%%%** >> cpp.bat +""") + # Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos. stage('dav1d', """ git clone -b 1.4.1 https://code.videolan.org/videolan/dav1d.git cd dav1d +win32: + SET "TARGET=x86" + SET "DAV1D_ASM_DISABLE=-Denable_asm=false" +win64: + SET "TARGET=x86_64" + SET "DAV1D_ASM_DISABLE=" +winarm: + SET "TARGET=aarch64" + SET "DAV1D_ASM_DISABLE=" + SET "PATH_BACKUP_=%PATH%" + SET "PATH=%LIBS_DIR%\\gas-preprocessor;%PATH%" + echo armasm64 fails with 'syntax error in expression: tbnz x14, #4, 8f' as if this instruction is unknown/unsupported. + git revert --no-edit d503bb0ccaf104b2f13da0f092e09cc9411b3297 win: - if "%X8664%" equ "x64" ( - SET "TARGET=x86_64" - SET "DAV1D_ASM_DISABLE=" - ) else ( - SET "TARGET=x86" - SET "DAV1D_ASM_DISABLE=-Denable_asm=false" - ) set FILE=cross-file.txt echo [binaries] > %FILE% echo c = 'cl' >> %FILE% @@ -752,6 +782,8 @@ release: win: copy %LIBS_DIR%\\local\\lib\\libdav1d.a %LIBS_DIR%\\local\\lib\\dav1d.lib deactivate +winarm: + SET "PATH=%PATH_BACKUP_%" mac: buildOneArch() { arch=$1 @@ -777,6 +809,67 @@ mac: lipo -create build.arm64/libdav1d.a build/libdav1d.a -output ${USED_PREFIX}/lib/libdav1d.a """) +stage('openh264', """ + git clone -b v2.4.1 https://github.com/cisco/openh264.git + cd openh264 +win32: + SET "TARGET=x86" +win64: + SET "TARGET=x86_64" +winarm: + SET "TARGET=aarch64" + SET "PATH_BACKUP_=%PATH%" + SET "PATH=%LIBS_DIR%\\gas-preprocessor;%PATH%" +win: + set FILE=cross-file.txt + echo [binaries] > %FILE% + echo c = 'cl' >> %FILE% + echo cpp = 'cl' >> %FILE% + echo ar = 'lib' >> %FILE% + echo windres = 'rc' >> %FILE% + echo [host_machine] >> %FILE% + echo system = 'windows' >> %FILE% + echo cpu_family = '%TARGET%' >> %FILE% + echo cpu = '%TARGET%' >> %FILE% + echo endian = 'little' >> %FILE% + +depends:python/Scripts/activate.bat + %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat + meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=debug -Db_vscrt=mtd builddir-debug + meson compile -C builddir-debug + meson install -C builddir-debug +release: + meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=release -Db_vscrt=mt builddir-release + meson compile -C builddir-release + meson install -C builddir-release +win: + copy %LIBS_DIR%\\local\\lib\\libopenh264.a %LIBS_DIR%\\local\\lib\\openh264.lib + deactivate +winarm: + SET "PATH=%PATH_BACKUP_%" +mac: + buildOneArch() { + arch=$1 + folder=`pwd`/$2 + + meson setup \ + --cross-file ../patches/macos_meson_${arch}.txt \ + --prefix ${USED_PREFIX} \ + --default-library=static \ + --buildtype=minsize \ + ${folder} + meson compile -C ${folder} + meson install -C ${folder} + + mv ${USED_PREFIX}/lib/libopenh264.a ${folder}/libopenh264.a + } + + buildOneArch aarch64 build.aarch64 + buildOneArch x86_64 build.x86_64 + + lipo -create build.aarch64/libopenh264.a build.x86_64/libopenh264.a -output ${USED_PREFIX}/lib/libopenh264.a +""") + stage('libavif', """ git clone -b v1.0.4 https://github.com/AOMediaCodec/libavif.git cd libavif @@ -846,7 +939,7 @@ mac: """) stage('libwebp', """ - git clone -b v1.3.2 https://github.com/webmproject/libwebp.git + git clone -b v1.4.0 https://github.com/webmproject/libwebp.git cd libwebp win: nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all @@ -938,7 +1031,7 @@ mac: """) stage('libjxl', """ - git clone -b v0.8.2 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git + git clone -b v0.10.3 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl """ + setVar("cmake_defines", """ -DBUILD_SHARED_LIBS=OFF @@ -954,12 +1047,10 @@ stage('libjxl', """ -DJPEGXL_ENABLE_SJPEG=OFF -DJPEGXL_ENABLE_OPENEXR=OFF -DJPEGXL_ENABLE_SKCMS=ON - -DJPEGXL_BUNDLE_SKCMS=ON -DJPEGXL_ENABLE_VIEWERS=OFF -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_PLUGINS=OFF -DJPEGXL_ENABLE_COVERAGE=OFF - -DJPEGXL_ENABLE_PROFILER=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF """) + """ win: @@ -967,8 +1058,8 @@ win: -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ - -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ - -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ + -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^ + -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ @@ -1003,12 +1094,13 @@ win: SET CHERE_INVOKING=enabled_from_arguments SET MSYS2_PATH_TYPE=inherit - if "%X8664%" equ "x64" ( - SET "TOOLCHAIN=x86_64-win64-vs17" - ) else ( - SET "TOOLCHAIN=x86-win32-vs17" - ) - +win32: + SET "TOOLCHAIN=x86-win32-vs17" +win64: + SET "TOOLCHAIN=x86_64-win64-vs17" +winarm: + SET "TOOLCHAIN=arm64-win64-vs17" +win: depends:patches/build_libvpx_win.sh bash --login ../patches/build_libvpx_win.sh @@ -1095,12 +1187,19 @@ stage('ffmpeg', """ git clone -b n6.1.1 https://github.com/FFmpeg/FFmpeg.git ffmpeg cd ffmpeg win: +depends:patches/ffmpeg.patch + git apply ../patches/ffmpeg.patch + SET PATH_BACKUP_=%PATH% SET PATH=%ROOT_DIR%\\ThirdParty\\msys64\\usr\\bin;%PATH% SET CHERE_INVOKING=enabled_from_arguments SET MSYS2_PATH_TYPE=inherit + SET "ARCH_PARAM=" +winarm: + SET "ARCH_PARAM=--arch=aarch64" +win: depends:patches/build_ffmpeg_win.sh bash --login ../patches/build_ffmpeg_win.sh @@ -1317,11 +1416,12 @@ depends:patches/breakpad.diff git clone -b release-1.11.0 https://github.com/google/googletest src/testing win: SET "PYTHONUTF8=1" - if "%X8664%" equ "x64" ( - SET "FolderPostfix=_x64" - ) else ( - SET "FolderPostfix=" - ) + SET "FolderPostfix=" +win64: + SET "FolderPostfix=_x64" +winarm: + SET "FolderPostfix=_ARM64" +win: depends:python/Scripts/activate.bat %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat cd src\\client\\windows @@ -1618,7 +1718,7 @@ win: stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git cd tg_owt - git checkout afd9d5d317 + git checkout 4a60ce1ab9 git submodule init git submodule update win: @@ -1626,6 +1726,7 @@ win: SET OPUS_PATH=$USED_PREFIX/include/opus SET OPENSSL_PATH=$LIBS_DIR/openssl3/include SET LIBVPX_PATH=$USED_PREFIX/include + SET OPENH264_PATH=$USED_PREFIX/include SET FFMPEG_PATH=$LIBS_DIR/ffmpeg mkdir out cd out @@ -1639,6 +1740,7 @@ win: -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PATH \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja release: @@ -1653,12 +1755,14 @@ release: -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PATH \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja mac: MOZJPEG_PATH=$USED_PREFIX/include OPUS_PATH=$USED_PREFIX/include/opus LIBVPX_PATH=$USED_PREFIX/include + OPENH264_PATH=$USED_PREFIX/include FFMPEG_PATH=$USED_PREFIX/include mkdir out cd out @@ -1673,6 +1777,7 @@ mac: -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja cd .. @@ -1687,6 +1792,7 @@ mac: -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja cd .. @@ -1703,6 +1809,7 @@ release: -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja cd .. @@ -1716,6 +1823,7 @@ release: -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \ -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \ -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \ + -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \ -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../.. ninja cd .. @@ -1723,6 +1831,30 @@ release: lipo -create Release.arm64/libtg_owt.a Release.x86_64/libtg_owt.a -output Release/libtg_owt.a """) +stage('ada', """ + git clone -b v2.9.0 https://github.com/ada-url/ada.git + cd ada +win: + cmake -B out . ^ + -A %WIN32X64% ^ + -D ADA_TESTING=OFF ^ + -D ADA_TOOLS=OFF ^ + -D CMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ + -D CMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ + -D CMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" + cmake --build out --config Debug --parallel + cmake --build out --config Release --parallel +mac: + CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\ + -D ADA_TESTING=OFF \\ + -D ADA_TOOLS=OFF \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX + cmake --build build $MAKE_THREADS_CNT + cmake --install build +""") + stage('protobuf', """ win: git clone --recursive -b v21.9 https://github.com/protocolbuffers/protobuf diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index 1703e15d0..2bc778741 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -2,7 +2,7 @@ import sys, os def resolve(arch): if sys.platform == 'darwin': - os.environ['QT'] = '6.2.8' + os.environ['QT'] = '6.2.9' elif sys.platform == 'win32': if arch == 'arm' or 'qt6' in sys.argv: print('Choosing Qt 6.') diff --git a/Telegram/build/release.py b/Telegram/build/release.py index 4bcf93fe9..4e6f38352 100644 --- a/Telegram/build/release.py +++ b/Telegram/build/release.py @@ -220,6 +220,20 @@ files.append({ 'mime': 'application/zip', 'label': 'Windows 64 bit: Portable', }) +files.append({ + 'local': 'tsetup-arm64.' + version_full + '.exe', + 'remote': 'tsetup-arm64.' + version_full + '.exe', + 'backup_folder': 'tarm64', + 'mime': 'application/octet-stream', + 'label': 'Windows on ARM: Installer', +}) +files.append({ + 'local': 'tportable-arm64.' + version_full + '.zip', + 'remote': 'tportable-arm64.' + version_full + '.zip', + 'backup_folder': 'tarm64', + 'mime': 'application/zip', + 'label': 'Windows on ARM: Portable', +}) files.append({ 'local': 'tsetup.' + version_full + '.dmg', 'remote': 'tsetup.' + version_full + '.dmg', diff --git a/Telegram/build/setup.iss b/Telegram/build/setup.iss index e52e7d2c4..799da0931 100644 --- a/Telegram/build/setup.iss +++ b/Telegram/build/setup.iss @@ -36,7 +36,12 @@ DisableProgramGroupPage=no WizardStyle=modern SignTool=sha256 -#if MyBuildTarget == "win64" +#if MyBuildTarget == "winarm" + ArchitecturesAllowed="arm64" + OutputBaseFilename=tsetup-arm64.{#MyAppVersionFull} + #define ArchModulesFolder "arm64" + AppVerName={#MyAppName} {#MyAppVersion} arm64 +#elif MyBuildTarget == "win64" ArchitecturesAllowed="x64 arm64" ArchitecturesInstallIn64BitMode="x64 arm64" OutputBaseFilename=ayusetup-x64.{#MyAppVersionFull} @@ -68,7 +73,9 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip [Files] Source: "{#ReleasePath}\Telegram.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#ReleasePath}\Updater.exe"; DestDir: "{app}"; Flags: ignoreversion +#if MyBuildTarget != "winarm" Source: "{#ReleasePath}\{#ModulesFolder}\d3d\d3dcompiler_47.dll"; DestDir: "{app}\{#ModulesFolder}\d3d"; Flags: ignoreversion +#endif ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] diff --git a/Telegram/build/updates.py b/Telegram/build/updates.py index 7a05070e7..f6f701eda 100644 --- a/Telegram/build/updates.py +++ b/Telegram/build/updates.py @@ -81,7 +81,7 @@ if building: if result != 0: finish(1, 'While stripping Telegram.') - result = subprocess.call('codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: John Preston" Telegram.app --entitlements "../../Telegram/Telegram/Telegram.entitlements"', shell=True) + result = subprocess.call('codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)" Telegram.app --entitlements "../../Telegram/Telegram/Telegram.entitlements"', shell=True) if result != 0: finish(1, 'While signing Telegram.') diff --git a/Telegram/build/version b/Telegram/build/version index 553191613..37a420e7a 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5002002 -AppVersionStrMajor 5.2 -AppVersionStrSmall 5.2.2 -AppVersionStr 5.2.2 +AppVersion 5003002 +AppVersionStrMajor 5.3 +AppVersionStrSmall 5.3.2 +AppVersionStr 5.3.2 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.2.2 +AppVersionOriginal 5.3.2 diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake index 881ad9952..44539b54b 100644 --- a/Telegram/cmake/lib_tgcalls.cmake +++ b/Telegram/cmake/lib_tgcalls.cmake @@ -88,10 +88,6 @@ PRIVATE v2/SignalingEncryption.h v2/SignalingSctpConnection.cpp v2/SignalingSctpConnection.h - v2_4_0_0/InstanceV2_4_0_0Impl.cpp - v2_4_0_0/InstanceV2_4_0_0Impl.h - v2_4_0_0/Signaling_4_0_0.cpp - v2_4_0_0/Signaling_4_0_0.h # Desktop capturer desktop_capturer/DesktopCaptureSource.h @@ -152,10 +148,17 @@ PRIVATE platform/darwin/GLVideoView.mm platform/darwin/GLVideoViewMac.h platform/darwin/GLVideoViewMac.mm + platform/darwin/h265_nalu_rewriter.cc + platform/darwin/h265_nalu_rewriter.h platform/darwin/objc_video_encoder_factory.h platform/darwin/objc_video_encoder_factory.mm platform/darwin/objc_video_decoder_factory.h platform/darwin/objc_video_decoder_factory.mm + platform/darwin/RTCCodecSpecificInfoH265+Private.h + platform/darwin/RTCCodecSpecificInfoH265.h + platform/darwin/RTCCodecSpecificInfoH265.mm + platform/darwin/RTCH265ProfileLevelId.h + platform/darwin/RTCH265ProfileLevelId.mm platform/darwin/TGCMIOCapturer.h platform/darwin/TGCMIOCapturer.m platform/darwin/TGCMIODevice.h @@ -218,6 +221,7 @@ PUBLIC TGCALLS_USE_STD_OPTIONAL PRIVATE WEBRTC_APP_TDESKTOP + RTC_ENABLE_H265 RTC_ENABLE_VP9 ) @@ -255,6 +259,7 @@ PRIVATE -Wno-ambiguous-reversed-operator -Wno-deprecated-declarations -Wno-unqualified-std-cast-call + -Wno-unused-function ) remove_target_sources(lib_tgcalls ${tgcalls_loc} diff --git a/Telegram/cmake/lib_tgvoip.cmake b/Telegram/cmake/lib_tgvoip.cmake index 886cd9935..fbae70966 100644 --- a/Telegram/cmake/lib_tgvoip.cmake +++ b/Telegram/cmake/lib_tgvoip.cmake @@ -134,20 +134,13 @@ PRIVATE ) if (WIN32) - if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - target_compile_options(lib_tgvoip_bundled - PRIVATE - /wd4005 - /wd4244 # conversion from 'int' to 'float', possible loss of data (several in webrtc) - /wd5055 # operator '>' deprecated between enumerations and floating-point types - ) - else() - target_compile_definitions(lib_tgvoip_bundled - PUBLIC - # Doesn't build with mingw for now - TGVOIP_NO_DSP - ) - endif() + target_compile_options_if_exists(lib_tgvoip_bundled + PRIVATE + /wd4005 # 'identifier' : macro redefinition + /wd4068 # unknown pragma + /wd4996 # deprecated + /wd5055 # operator '>' deprecated between enumerations and floating-point types + ) elseif (APPLE) target_compile_definitions(lib_tgvoip_bundled PUBLIC diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index 602abf41c..1d4edf9f5 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -38,6 +38,7 @@ PUBLIC tdesktop::td_scheme PRIVATE desktop-app::lib_webview + desktop-app::external_ada tdesktop::td_lang tdesktop::td_ui ) diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index dc16ff2cf..089dd1316 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -73,6 +73,8 @@ PRIVATE chat_helpers/stickers_emoji_image_loader.cpp chat_helpers/stickers_emoji_image_loader.h + core/current_geo_location.cpp + core/current_geo_location.h core/file_location.cpp core/file_location.h core/mime_type.cpp @@ -81,6 +83,8 @@ PRIVATE countries/countries_instance.cpp countries/countries_instance.h + data/raw/raw_countries_bounds.cpp + data/raw/raw_countries_bounds.h data/data_birthday.cpp data/data_birthday.h data/data_channel_earn.h @@ -197,9 +201,16 @@ PRIVATE payments/ui/payments_panel_data.h payments/ui/payments_panel_delegate.h + platform/linux/current_geo_location_linux.cpp + platform/linux/current_geo_location_linux.h platform/mac/file_bookmark_mac.h platform/mac/file_bookmark_mac.mm + platform/mac/current_geo_location_mac.h + platform/mac/current_geo_location_mac.mm + platform/win/current_geo_location_win.cpp + platform/win/current_geo_location_win.h platform/platform_file_bookmark.h + platform/platform_current_geo_location.h settings/settings_common.cpp settings/settings_common.h @@ -393,6 +404,11 @@ PRIVATE ui/text/text_options.cpp ui/text/text_options.h + ui/widgets/fields/special_fields.cpp + ui/widgets/fields/special_fields.h + ui/widgets/fields/time_part_input_with_placeholder.cpp + ui/widgets/fields/time_part_input_with_placeholder.h + ui/widgets/color_editor.cpp ui/widgets/color_editor.h ui/widgets/continuous_sliders.cpp @@ -431,10 +447,8 @@ PRIVATE ui/unread_badge_paint.h ui/userpic_view.cpp ui/userpic_view.h - ui/widgets/fields/special_fields.cpp - ui/widgets/fields/special_fields.h - ui/widgets/fields/time_part_input_with_placeholder.cpp - ui/widgets/fields/time_part_input_with_placeholder.h + ui/webview_helpers.cpp + ui/webview_helpers.h window/window_slide_animation.cpp window/window_slide_animation.h @@ -442,6 +456,12 @@ PRIVATE ui/ui_pch.h ) +nice_target_sources(td_ui ${res_loc} +PRIVATE + picker_html/picker.css + picker_html/picker.js +) + if (DESKTOP_APP_SPECIAL_TARGET) remove_target_sources(td_ui ${src_loc} ui/controls/window_outdated_bar_dummy.cpp diff --git a/Telegram/create.bat b/Telegram/create.bat index 85ec9439b..2307b0d2a 100644 --- a/Telegram/create.bat +++ b/Telegram/create.bat @@ -67,26 +67,30 @@ exit /b %errorlevel% set "CommandPath=%1" set "CommandPathUnix=!CommandPath:\=/!" set "CommandPathWin=!CommandPath:/=\!" - + if "!CommandPathUnix:~-4!" == "_mac" ( + set "CommandExt=mm" + ) else ( + set "CommandExt=cpp" + ) if "!CommandPathUnix!" == "" ( echo Provide source path. exit /b 1 - ) else if exist "SourceFiles\!CommandPathWin!.cpp" ( + ) else if exist "SourceFiles\!CommandPathWin!.!CommandExt!" ( echo This source already exists. exit /b 1 ) - echo Generating source !CommandPathUnix!.cpp.. - mkdir "SourceFiles\!CommandPathWin!.cpp" - rmdir "SourceFiles\!CommandPathWin!.cpp" + echo Generating source !CommandPathUnix!.!CommandExt!.. + mkdir "SourceFiles\!CommandPathWin!.!CommandExt!" + rmdir "SourceFiles\!CommandPathWin!.!CommandExt!" - call :write_comment !CommandPathWin!.cpp + call :write_comment !CommandPathWin!.!CommandExt! set "quote=""" set "quote=!quote:~0,1!" set "source1=#include !quote!!CommandPathUnix!.h!quote!" ( echo !source1! echo. - )>> "SourceFiles\!CommandPathWin!.cpp" + )>> "SourceFiles\!CommandPathWin!.!CommandExt!" exit /b ) diff --git a/Telegram/lib_base b/Telegram/lib_base index f30400147..ca4503b30 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit f30400147d997fedc787e214467d305db6c159e7 +Subproject commit ca4503b3075fcaed5719b6ff1f40e40d14d08d95 diff --git a/Telegram/lib_crl b/Telegram/lib_crl index 078006d29..c1d6b0273 160000 --- a/Telegram/lib_crl +++ b/Telegram/lib_crl @@ -1 +1 @@ -Subproject commit 078006d29af0002e6cd8c61a405cdeaf65b37142 +Subproject commit c1d6b0273653095b10b4d0f4f7c30b614b690fd5 diff --git a/Telegram/lib_storage b/Telegram/lib_storage index 0971b69ca..ccdc72548 160000 --- a/Telegram/lib_storage +++ b/Telegram/lib_storage @@ -1 +1 @@ -Subproject commit 0971b69ca90f1697ef81276d9820dcd6d26de4ac +Subproject commit ccdc72548a5065b5991b4e06e610d76bc4f6023e diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index f701713cd..8751e27d5 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit f701713cd798bd7d5f69d318fdefb125d101aa76 +Subproject commit 8751e27d50d2f26b5d20673e5ddba38e90953570 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 659b91812..c27c69953 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 659b9181240aae16c05ef8ab7e6c4dd527afcf8a +Subproject commit c27c69953db52cfcb56abc3d422764f0fb4c2152 diff --git a/changelog.txt b/changelog.txt index 6e398bcc6..e43e0dbe2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,41 @@ +5.3.2 (02.08.24) + +- Fix crash on launch in some Linux systems. + +5.3.1 (01.08.24) + +- Open normal links from tonsite-s in system browser. +- Fix unicode tonsite:// links. +- Fix crash on Linux X11. + +5.3 (31.07.24) + +- View recent and popular web apps in chats search. +- Open several web apps in different windows. +- Gift Telegram Stars to your friends. +- Send location marks and venues. +- Open tonsite:// links in webview. +- Edit order of stickers in your packs. + +5.2.6 beta (29.07.24) + +- Fix launching on X11. (Linux) + +5.2.5 beta (27.07.24) + +- Fix media viewer context menu. +- Fix blockquotes layout in messages. + +5.2.4 beta (24.07.24) + +- Allow opening several web apps. +- Send location marks and venues. + +5.2.3 (07.07.24) + +- Fix crash in bot star stats page. +- Bug fixes and other minor improvements. + 5.2.2 (02.07.24) - Fix topics search in topic groups. diff --git a/cmake b/cmake index 5742caae6..08de4f18f 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 5742caae65e4163e7faec238eb4e3e5c219ad09c +Subproject commit 08de4f18f5e4459689957b3aa115e10d8cbef9d6