Merge tag 'v4.15.0' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/dialogs/dialogs_row.cpp
#	Telegram/SourceFiles/dialogs/dialogs_widget.cpp
#	Telegram/SourceFiles/history/view/history_view_message.cpp
#	Telegram/SourceFiles/info/profile/info_profile_actions.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
ZavaruKitsu 2024-02-18 21:37:55 +03:00
commit 43b0df3f94
200 changed files with 3605 additions and 1270 deletions

View file

@ -85,7 +85,7 @@ jobs:
docker run --rm \
-v $PWD:/usr/src/tdesktop \
-e DEBUG=1 \
-e CONFIG=Debug \
tdesktop:centos_env \
/usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \
-D CMAKE_C_FLAGS_DEBUG="" \

View file

@ -40,7 +40,7 @@ jobs:
macos:
name: MacOS
runs-on: macos-12
runs-on: macos-13
strategy:
matrix:

View file

@ -744,8 +744,6 @@ PRIVATE
history/view/controls/history_view_ttl_button.h
history/view/controls/history_view_voice_record_bar.cpp
history/view/controls/history_view_voice_record_bar.h
history/view/controls/history_view_voice_record_button.cpp
history/view/controls/history_view_voice_record_button.h
history/view/controls/history_view_webpage_processor.cpp
history/view/controls/history_view_webpage_processor.h
history/view/media/history_view_call.cpp
@ -1487,6 +1485,8 @@ PRIVATE
ui/widgets/level_meter.h
ui/countryinput.cpp
ui/countryinput.h
ui/dynamic_thumbnails.cpp
ui/dynamic_thumbnails.h
ui/filter_icons.cpp
ui/filter_icons.h
ui/filter_icon_panel.cpp

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -867,6 +867,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_background_sure_delete" = "Are you sure you want to delete this background?";
"lng_background_other_info" = "{user} will be able to apply this wallpaper";
"lng_background_other_channel" = "All subscribers will see this wallpaper";
"lng_background_other_group" = "All members will see this wallpaper";
"lng_background_apply1" = "Apply the wallpaper in this chat.";
"lng_background_apply2" = "Enjoy the view.";
"lng_background_apply_button" = "Apply For This Chat";
@ -876,6 +877,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_background_apply_me" = "Apply for me";
"lng_background_apply_both" = "Apply for me and {user}";
"lng_background_apply_channel" = "Apply For Channel";
"lng_background_apply_group" = "Apply For Group";
"lng_download_path_ask" = "Ask download path for each file";
"lng_download_path" = "Download path";
@ -1736,11 +1738,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_story_mention_button" = "View Story";
"lng_action_story_mention_me_unavailable" = "The story where you mentioned {user} is no longer available.";
"lng_action_story_mention_unavailable" = "The story where {user} mentioned you is no longer available.";
"lng_action_giveaway_started_group" = "{from} just started a giveaway of Telegram Premium subscriptions to its members.";
"lng_action_giveaway_started" = "{from} just started a giveaway of Telegram Premium subscriptions to its followers.";
"lng_action_giveaway_results#one" = "{count} winner of the giveaway was randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results#other" = "{count} winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results_some" = "Some winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results_none" = "No winners of the giveaway could be selected.";
"lng_action_boost_apply#one" = "{from} boosted the group";
"lng_action_boost_apply#other" = "{from} boosted the group {count} times";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
@ -1983,6 +1988,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_stickers" = "Group stickers";
"lng_group_stickers_description" = "You can choose a sticker set which will be available for every member while in the group chat.";
"lng_group_stickers_add" = "Choose sticker set";
"lng_group_emoji" = "Group emoji pack";
"lng_group_emoji_description" = "Choose an emoji pack that will be available to all members within the group.";
"lng_premium" = "Premium";
"lng_premium_free" = "Free";
@ -2033,6 +2040,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_infinite_reactions" = "React with thousands of emoji — with multiple reactions per message.";
"lng_premium_summary_subtitle_tags_for_messages" = "Tags for Messages";
"lng_premium_summary_about_tags_for_messages" = "Organize your Saved Messages with tags for quicker access.";
"lng_premium_summary_subtitle_last_seen" = "Last Seen Times";
"lng_premium_summary_about_last_seen" = "View the last seen and read times of others even if you hide yours.";
"lng_premium_summary_subtitle_message_privacy" = "Message Privacy";
"lng_premium_summary_about_message_privacy" = "Restrict people you don't know from sending you messages.";
"lng_premium_summary_subtitle_premium_stickers" = "Premium Stickers";
"lng_premium_summary_about_premium_stickers" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
"lng_premium_summary_subtitle_animated_emoji" = "Animated Emoji";
@ -2144,28 +2155,48 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_gifts_terms_policy" = "Privacy Policy";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_group_button" = "Boost Group";
"lng_boost_again_button" = "Boost Again";
"lng_boost_group_about" = "Boost your group to unlock additional\nappearance settings.";
"lng_boost_level#one" = "Level {count}";
"lng_boost_level#other" = "Level {count}";
"lng_boost_level_unlocks#one" = "Level {count} Unlocks:";
"lng_boost_level_unlocks#other" = "Level {count} Unlocks:";
"lng_boost_channel_title_first" = "Enable stories for channel";
"lng_boost_channel_needs_first#one" = "{channel} needs **{count}** more boost to enable posting stories. Help make it possible!";
"lng_boost_channel_needs_first#other" = "{channel} needs **{count}** more boosts to enable posting stories. Help make it possible!";
"lng_boost_channel_title_first_group" = "Enable stories for group";
"lng_boost_channel_needs_unlock#one" = "{channel} needs **{count}** more boost to unlock new features.";
"lng_boost_channel_needs_unlock#other" = "{channel} needs **{count}** more boosts to unlock new features.";
//"lng_boost_channel_needs_first#one" = "{channel} needs **{count}** more boost to enable posting stories. Help make it possible!";
//"lng_boost_channel_needs_first#other" = "{channel} needs **{count}** more boosts to enable posting stories. Help make it possible!";
"lng_boost_channel_title_more" = "Help upgrade channel";
"lng_boost_channel_needs_more#one" = "{channel} needs **{count}** more boost to be able to {post}.";
"lng_boost_channel_needs_more#other" = "{channel} needs **{count}** more boosts to be able to {post}.";
"lng_boost_channel_title_more_group" = "Help upgrade group";
//"lng_boost_channel_needs_more#one" = "{channel} needs **{count}** more boost to be able to {post}.";
//"lng_boost_channel_needs_more#other" = "{channel} needs **{count}** more boosts to be able to {post}.";
"lng_boost_channel_title_max" = "Maximum level reached";
"lng_boost_channel_you_title" = "You boosted {channel}!";
"lng_boost_channel_you_first#one" = "This channel needs **{count}** more boost\nto enable stories.";
"lng_boost_channel_you_first#other" = "This channel needs **{count}** more boosts\nto enable stories.";
"lng_boost_channel_you_more#one" = "This channel needs **{count}** more boost\nto be able to {post}.";
"lng_boost_channel_you_more#other" = "This channel needs **{count}** more boosts\nto be able to {post}.";
//"lng_boost_channel_you_first#one" = "This channel needs **{count}** more boost\nto enable stories.";
//"lng_boost_channel_you_first#other" = "This channel needs **{count}** more boosts\nto enable stories.";
//"lng_boost_channel_you_more#one" = "This channel needs **{count}** more boost\nto be able to {post}.";
//"lng_boost_channel_you_more#other" = "This channel needs **{count}** more boosts\nto be able to {post}.";
"lng_boost_channel_reached_first" = "This channel reached **Level 1** and can now post stories.";
"lng_boost_channel_reached_more#one" = "This channel reached **Level {count}** and can now {post}.";
"lng_boost_channel_reached_more#other" = "This channel reached **Level {count}** and can now {post}.";
//"lng_boost_channel_you_first_group#one" = "This group needs **{count}** more boost\nto enable stories.";
//"lng_boost_channel_you_first_group#other" = "This group needs **{count}** more boosts\nto enable stories.";
//"lng_boost_channel_you_more_group#one" = "This group needs **{count}** more boost\nto be able to {post}.";
//"lng_boost_channel_you_more_group#other" = "This group needs **{count}** more boosts\nto be able to {post}.";
"lng_boost_channel_reached_first_group" = "This group reached **Level 1** and can now post stories.";
"lng_boost_channel_reached_more_group#one" = "This group reached **Level {count}** and can now {post}.";
"lng_boost_channel_reached_more_group#other" = "This group reached **Level {count}** and can now {post}.";
"lng_boost_channel_post_stories#one" = "post **{count} story** per day";
"lng_boost_channel_post_stories#other" = "post **{count} stories** per day";
"lng_boost_channel_features" = "Your boosts will help {channel} to unlock new features.";
"lng_boost_group_lift_restrictions" = "Boost the group to remove messaging restrictions.";
"lng_boost_group_lift_restrictions_many#one" = "Boost the group **{count} times** to remove messaging restrictions.";
"lng_boost_group_lift_restrictions_many#other" = "Boost the group **{count} times** to remove messaging restrictions.";
"lng_boost_error_gifted_title" = "Can't boost with gifted Premium!";
"lng_boost_error_gifted_text" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels.";
"lng_boost_error_gifted_text_group" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost groups.";
"lng_boost_need_more" = "More boosts needed";
"lng_boost_need_more_text#one" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts.";
"lng_boost_need_more_text#other" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts.";
@ -2173,10 +2204,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_need_more_again#other" = "To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boosts.";
"lng_boost_error_already_title" = "Already Boosted!";
"lng_boost_error_already_text" = "You are already boosting this channel.";
"lng_boost_error_already_text_group" = "You are already boosting this group.";
"lng_boost_error_premium_title" = "Premium needed!";
"lng_boost_error_premium_text_group" = "Only **Telegram Premium** subscribers can boost groups. Do you want to subscribe to **Telegram Premium**?";
"lng_boost_error_premium_text" = "Only **Telegram Premium** subscribers can boost channels. Do you want to subscribe to **Telegram Premium**?";
"lng_boost_error_premium_yes" = "Yes";
"lng_boost_error_flood_title" = "Can't boost too often!";
"lng_boost_error_flood_text_group" = "You can change the group you boost only once a day. Next time you can boost is in {left}.";
"lng_boost_error_flood_text" = "You can change the channel you boost only once a day. Next time you can boost is in {left}.";
"lng_boost_now_instead" = "You currently boost {channel}. Do you want to boost {other} instead?";
"lng_boost_now_replace" = "Replace";
@ -2193,6 +2227,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_reassign_done#other" = "{count} boosts are reassigned from {channels}.";
"lng_boost_reassign_channels#one" = "{count} channel";
"lng_boost_reassign_channels#other" = "{count} channels";
"lng_boost_reassign_groups#one" = "{count} group";
"lng_boost_reassign_groups#other" = "{count} groups";
"lng_boost_reassign_mixed#one" = "{count} group or channel";
"lng_boost_reassign_mixed#other" = "{count} groups and channels";
"lng_boost_channel_title_color" = "Enable colors";
"lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color.";
@ -2201,23 +2239,51 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_channel_title_wallpaper" = "Enable wallpapers";
"lng_boost_channel_needs_level_wallpaper#one" = "Your channel needs to reach **Level {count}** to change channel wallpaper.";
"lng_boost_channel_needs_level_wallpaper#other" = "Your channel needs to reach **Level {count}** to change channel wallpaper.";
"lng_boost_group_needs_level_wallpaper#one" = "Your group needs to reach **Level {count}** to change group wallpaper.";
"lng_boost_group_needs_level_wallpaper#other" = "Your group needs to reach **Level {count}** to change group wallpaper.";
"lng_boost_channel_title_status" = "Enable emoji status";
"lng_boost_channel_needs_level_status#one" = "Your channel needs to reach **Level {count}** to set emoji status.";
"lng_boost_channel_needs_level_status#other" = "Your channel needs to reach **Level {count}** to set emoji status.";
"lng_boost_group_needs_level_status#one" = "Your group needs to reach **Level {count}** to set emoji status.";
"lng_boost_group_needs_level_status#other" = "Your group needs to reach **Level {count}** to set emoji status.";
"lng_boost_channel_title_reactions" = "Custom reactions";
"lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction.";
"lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions.";
"lng_boost_group_title_emoji" = "Enable emoji pack";
"lng_boost_group_needs_level_emoji#one" = "Your group needs to reach **Level {count}** to set emoji pack.";
"lng_boost_group_needs_level_emoji#other" = "Your group needs to reach **Level {count}** to set emoji pack.";
"lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:";
"lng_boost_channel_ask_button" = "Copy Link";
"lng_boost_channel_or" = "or";
"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}";
"lng_boost_channel_gifting_link" = "Get boosts >";
"lng_feature_stories#one" = "**{count}** Story Per Day";
"lng_feature_stories#other" = "**{count}** Stories Per Day";
"lng_feature_reactions#one" = "**{count}** Custom Reaction";
"lng_feature_reactions#other" = "**{count}** Custom Reactions";
"lng_feature_name_color_channel#one" = "**{count}** Channel Name Color";
"lng_feature_name_color_channel#other" = "**{count}** Channel Name Colors";
"lng_feature_link_style_channel#one" = "**{count}** Style for Links and Quotes";
"lng_feature_link_style_channel#other" = "**{count}** Styles for Links and Quotes";
"lng_feature_link_emoji" = "Custom Logo for Links and Quotes";
"lng_feature_emoji_status" = "**1000+** Emoji Statuses";
"lng_feature_backgrounds_channel#one" = "**{count}** Channel Background";
"lng_feature_backgrounds_channel#other" = "**{count}** Channel Backgrounds";
"lng_feature_custom_background_channel" = "Custom Channel Background";
"lng_feature_backgrounds_group#one" = "**{count}** Group Background";
"lng_feature_backgrounds_group#other" = "**{count}** Group Backgrounds";
"lng_feature_custom_background_group" = "Custom Group Background";
"lng_feature_custom_emoji_pack" = "Custom Emoji Pack";
"lng_feature_transcribe" = "Voice-to-Text Conversion";
"lng_giveaway_new_title" = "Boosts via Gifts";
"lng_giveaway_new_about" = "Get more boosts for your channel by gifting Premium to your subscribers.";
"lng_giveaway_new_about_group" = "Get more boosts for your group by gifting Premium to your subscribers.";
"lng_giveaway_create_option" = "Create Giveaway";
"lng_giveaway_create_subtitle" = "winners are chosen randomly";
"lng_giveaway_award_option" = "Award Specific Users";
@ -2231,30 +2297,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_channels_title" = "Channels included in the giveaway";
"lng_giveaway_channels_this#one" = "this channel will receive {count} boost";
"lng_giveaway_channels_this#other" = "this channel will receive {count} boosts";
"lng_giveaway_channels_this_group#one" = "this group will receive {count} boost";
"lng_giveaway_channels_this_group#other" = "this group will receive {count} boosts";
"lng_giveaway_channels_add" = "Add Channel";
"lng_giveaway_channels_about" = "Choose the channels the users need to join to take part in the giveaway.";
"lng_giveaway_users_title" = "Users eligible for the giveaway";
"lng_giveaway_users_all" = "All subscribers";
"lng_giveaway_users_all_group" = "All members";
"lng_giveaway_users_from_all_countries" = "from all countries";
"lng_giveaway_users_from_one_country" = "from {country}";
"lng_giveaway_users_from_countries#one" = "from {count} country";
"lng_giveaway_users_from_countries#other" = "from {count} countries";
"lng_giveaway_users_new" = "Only new subscribers";
"lng_giveaway_users_new_group" = "Only new members";
"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started or to users from specific countries.";
"lng_giveaway_users_about_group" = "Choose if you want to limit the giveaway only to those who joined the group after the giveaway started or to members from specific countries.";
"lng_giveaway_start" = "Start Giveaway";
"lng_giveaway_award" = "Gift Premium";
"lng_giveaway_start_sure" = "Are you sure you want to start this prepaid giveaway now? This action cannot be undone.";
"lng_giveaway_date_title" = "Date when giveaway ends";
"lng_giveaway_date" = "Date and Time";
"lng_giveaway_date_about#one" = "Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium.";
"lng_giveaway_date_about#one" = "Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium.";
"lng_giveaway_date_about#other" = "Choose when {count} subscribers of your channel will be randomly selected to receive Telegram Premium.";
"lng_giveaway_date_about_group#one" = "Choose when {count} members of your group will be randomly selected to receive Telegram Premium.";
"lng_giveaway_date_about_group#other" = "Choose when {count} members of your group will be randomly selected to receive Telegram Premium.";
"lng_giveaway_duration_title#one" = "Duration of Premium subscription";
"lng_giveaway_duration_title#other" = "Duration of Premium subscriptions";
"lng_giveaway_duration_price" = "{price} x {amount}";
"lng_giveaway_date_select" = "Select Date and Time";
"lng_giveaway_date_confirm" = "Confirm";
"lng_giveaway_channels_select#one" = "Select up to {count} channel";
"lng_giveaway_channels_select#other" = "Select up to {count} channels";
"lng_giveaway_recipients_save" = "Save Recipients";
"lng_giveaway_recipients_deselect" = "Deselect All";
"lng_giveaway_maximum_countries_error#one" = "You can select maximum {count} country.";
@ -2277,8 +2348,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_created_title" = "Giveaway created";
"lng_giveaway_created_body" = "Check your channels' {link} to see how this giveaway boosted your channel.";
"lng_giveaway_created_body_group" = "Check your groups' {link} to see how this giveaway boosted your group.";
"lng_giveaway_awarded_title" = "Premium subscriptions gifted";
"lng_giveaway_awarded_body" = "Check your channels' {link} to see how gifts boosted your channel.";
"lng_giveaway_awarded_body_group" = "Check your groups' {link} to see how gifts boosted your group.";
"lng_giveaway_created_link" = "Statistics";
"lng_prize_title" = "Congratulations!";
@ -2301,8 +2374,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_participants" = "Participants";
"lng_prizes_participants_all#one" = "All subscribers of the channel:";
"lng_prizes_participants_all#other" = "All subscribers of the channels:";
"lng_prizes_participants_all_group#one" = "All members of the group:";
"lng_prizes_participants_all_group#other" = "All members of the groups:";
"lng_prizes_participants_all_mixed#one" = "All members of the group:";
"lng_prizes_participants_all_mixed#other" = "All members of the groups and channels:";
"lng_prizes_participants_new#one" = "All users who joined the channel below after this date:";
"lng_prizes_participants_new#other" = "All users who joined the channels below after this date:";
"lng_prizes_participants_new_group#one" = "All users who joined the group below after this date:";
"lng_prizes_participants_new_group#other" = "All users who joined the groups below after this date:";
"lng_prizes_participants_new_mixed#one" = "All users who joined the group below after this date:";
"lng_prizes_participants_new_mixed#other" = "All users who joined the groups and channels below after this date:";
"lng_prizes_countries" = "from {countries}";
"lng_prizes_countries_and_one" = "{countries}, {country}";
"lng_prizes_countries_and_last" = "{countries} and {country}";
@ -2313,32 +2394,43 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_how_text" = "This giveaway is sponsored by {admins}.";
"lng_prizes_end_text" = "This giveaway was sponsored by {admins}.";
"lng_prizes_admins#one" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscription {duration} for its followers";
"lng_prizes_admins#other" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions {duration} for its followers.";
"lng_prizes_admins#other" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions {duration} for its followers";
"lng_prizes_admins_group#one" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscription {duration} for its members";
"lng_prizes_admins_group#other" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions {duration} for its members";
"lng_prizes_additional_added#one" = "{channel} also included **{count} {prize}** in the prize. Admins of the channel are responsible for delivering this prize.";
"lng_prizes_additional_added#other" = "{channel} also included **{count} {prize}** in the prizes. Admins of the channel are responsible for delivering these prizes.";
"lng_prizes_additional_added_group#one" = "{channel} also included **{count} {prize}** in the prize. Admins of the group are responsible for delivering this prize.";
"lng_prizes_additional_added_group#other" = "{channel} also included **{count} {prize}** in the prizes. Admins of the group are responsible for delivering these prizes.";
"lng_prizes_how_when_finish" = "On {date}, Telegram will automatically select {winners}.";
"lng_prizes_end_when_finish" = "On {date}, Telegram automatically selected {winners}.";
"lng_prizes_end_activated#one" = "**{count}** of the winners already used their gift link.";
"lng_prizes_end_activated#other" = "**{count}** of the winners already used their gift links.";
"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}";
"lng_prizes_winners_all_of_one#one" = "{count} random subscriber of {channel}";
"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}";
"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels";
"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels";
"lng_prizes_winners_all_of_one_group#one" = "{count} random member of {channel}";
"lng_prizes_winners_all_of_one_group#other" = "{count} random members of {channel}";
"lng_prizes_winners_all_of_many#one" = "{count} random subscriber of {channel} and other listed groups and channels";
"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed groups and channels";
"lng_prizes_winners_all_of_many_group#one" = "{count} random member of {channel} and other listed groups and channels";
"lng_prizes_winners_all_of_many_group#other" = "{count} random members of {channel} and other listed groups and channels";
"lng_prizes_winners_new_of_one#one" = "{count} random user that joined {channel} after {start_date}";
"lng_prizes_winners_new_of_one#other" = "{count} random users that joined {channel} after {start_date}";
"lng_prizes_winners_new_of_many#one" = "{count} random user that joined {channel} and other listed channels after {start_date}";
"lng_prizes_winners_new_of_many#other" = "{count} random users that joined {channel} and other listed channels after {start_date}";
"lng_prizes_how_participate_one" = "To take part in this giveaway please join channel {channel} before {date}.";
"lng_prizes_how_participate_many" = "To take part in this giveaway please join channel {channel} and other listed channels before {date}.";
"lng_prizes_winners_new_of_many#one" = "{count} random user that joined {channel} and other listed groups and channels after {start_date}";
"lng_prizes_winners_new_of_many#other" = "{count} random users that joined {channel} and other listed groups and channels after {start_date}";
"lng_prizes_how_participate_one" = "To take part in this giveaway please join {channel} before {date}.";
"lng_prizes_how_participate_many" = "To take part in this giveaway please join {channel} and other listed groups and channels before {date}.";
"lng_prizes_how_no_admin" = "You are not eligible to participate in this giveaway, because you are an admin of participating channel ({channel}).";
"lng_prizes_how_no_admin_group" = "You are not eligible to participate in this giveaway, because you are an admin of participating group ({channel}).";
"lng_prizes_how_no_joined" = "You are not eligible to participate in this giveaway, because you joined this channel on {date}, which is before the contest started.";
"lng_prizes_how_no_joined_group" = "You are not eligible to participate in this giveaway, because you joined this group on {date}, which is before the contest started.";
"lng_prizes_how_no_country" = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway.";
"lng_prizes_how_yes_joined_one" = "You are participating in this giveaway, because you have joined channel {channel}.";
"lng_prizes_how_yes_joined_many" = "You are participating in this giveaway, because you have joined channel {channel} (and other listed channels).";
"lng_prizes_how_yes_joined_one" = "You are participating in this giveaway, because you have joined {channel}.";
"lng_prizes_how_yes_joined_many" = "You are participating in this giveaway, because you have joined {channel} (and other listed groups and channels).";
"lng_prizes_you_won" = "You won a prize in this giveaway {cup}";
"lng_prizes_view_prize" = "View my prize";
"lng_prizes_you_didnt" = "You didn't win a prize in this giveaway.";
"lng_prizes_cancelled" = "The channel cancelled the prizes by reversing the payment for them.";
"lng_prizes_cancelled_group" = "The channel cancelled the prizes by reversing the payment for them.";
"lng_prizes_badge" = "x{amount}";
"lng_prizes_results_title" = "Winners Selected!";
@ -2441,6 +2533,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stickers_remove_pack_confirm" = "Remove";
"lng_stickers_archive_pack" = "Archive Stickers";
"lng_stickers_has_been_archived" = "Sticker pack has been archived.";
"lng_emoji_group_set" = "Group emoji set";
"lng_emoji_remove_group_set" = "Remove group emoji set?";
"lng_emoji_group_from_your" = "Choose from your emoji";
"lng_emoji_group_from_featured" = "Choose from trending emoji";
"lng_masks_archive_pack" = "Archive Masks";
"lng_masks_has_been_archived" = "Mask pack has been archived.";
"lng_masks_installed" = "Mask pack has been installed.";
@ -2520,6 +2616,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_broadcast_silent_ph" = "Silent broadcast...";
"lng_send_anonymous_ph" = "Send anonymously...";
"lng_story_reply_ph" = "Reply privately...";
"lng_story_comment_ph" = "Comment story...";
"lng_send_text_no" = "Text not allowed.";
"lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
"lng_send_text_type_and_last" = "{types} and {last}";
@ -2752,6 +2849,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_copy_email" = "Copy Email Address";
"lng_context_copy_hashtag" = "Copy Hashtag";
"lng_context_copy_mention" = "Copy Username";
"lng_context_copy_filename" = "Copy Filename";
"lng_context_save_image" = "Save Image As...";
"lng_context_copy_image" = "Copy Image";
"lng_context_cancel_download" = "Cancel Download";
@ -2768,6 +2866,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_save_gif" = "Save GIF";
"lng_context_delete_gif" = "Delete GIF";
"lng_context_open_channel" = "Open Channel";
"lng_context_open_group" = "Open Group";
"lng_context_attached_stickers" = "Attached Stickers";
"lng_context_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply";
@ -2950,8 +3049,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_channel_level_min" = "Level 1+";
"lng_edit_channel_wallpaper" = "Channel wallpaper";
"lng_edit_channel_wallpaper_about" = "Set a wallpaper that will be visible for everyone reading your channel.";
"lng_edit_channel_wallpaper_group" = "Group wallpaper";
"lng_edit_channel_wallpaper_about_group" = "Set a wallpaper that will be visible for everyone participating in your group.";
"lng_edit_channel_status" = "Channel emoji status";
"lng_edit_channel_status_about" = "Choose a status that will be shown next to the channel's name.";
"lng_edit_channel_status_group" = "Group emoji status";
"lng_edit_channel_status_about_group" = "Choose a status that will be shown next to the group's name.";
"lng_edit_self_title" = "Edit your name";
"lng_confirm_contact_data" = "New Contact";
"lng_add_contact" = "Create";
@ -3515,6 +3618,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_slowmode_interval_seconds#other" = "every {count} seconds";
"lng_rights_slowmode_interval_minutes#one" = "every {count} minute";
"lng_rights_slowmode_interval_minutes#other" = "every {count} minutes";
"lng_rights_boosts_no_restrict" = "Do not restrict boosters";
"lng_rights_boosts_about" = "Turn this on to always allow users who boosted your group to send messages and media.";
"lng_rights_boosts_about_on" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages.";
"lng_slowmode_enabled"= "Slow mode is enabled. You can send your next message in {left}.";
"lng_slowmode_no_many" = "Slow mode is enabled. You can't send more than one message at a time.";
@ -3610,6 +3716,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restricted_send_gifs" = "The admins of this group restricted you from posting GIFs here.";
"lng_restricted_send_inline" = "The admins of this group restricted you from posting inline content here.";
"lng_restricted_send_polls" = "The admins of this group restricted you from posting polls here.";
"lng_restricted_boost_group" = "Boost this group to send messages";
"lng_restricted_send_message_until" = "The admins of this group restricted you from writing here until {date}, {time}.";
"lng_restricted_send_photos_until" = "The admins of this group restricted you from posting photos here until {date}, {time}.";
@ -3747,6 +3854,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_changed_stickers_group" = "{from} changed the group's {sticker_set}";
"lng_admin_log_changed_stickers_set" = "sticker set";
"lng_admin_log_removed_stickers_group" = "{from} removed the group's sticker set";
"lng_admin_log_changed_emoji_group" = "{from} changed the group's {sticker_set}";
"lng_admin_log_changed_emoji_set" = "emoji set";
"lng_admin_log_removed_emoji_group" = "{from} removed the group's emoji set";
"lng_admin_log_changed_linked_chat" = "{from} changed the discussion group to «{chat}»";
"lng_admin_log_removed_linked_chat" = "{from} removed the discussion group";
"lng_admin_log_changed_linked_channel" = "{from} changed the linked channel to «{chat}»";
@ -4582,10 +4692,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boosts_level" = "Level";
"lng_boosts_existing" = "Existing boosts";
"lng_boosts_premium_audience" = "Premium subscribers";
"lng_boosts_premium_members" = "Premium members";
"lng_boosts_next_level" = "Boosts to level up";
"lng_boosts_list_title#one" = "{count} Boost";
"lng_boosts_list_title#other" = "{count} Boosts";
"lng_boosts_list_subtext" = "Your channel is currently boosted by these users.";
"lng_boosts_list_subtext_group" = "Your group is currently boosted by these users.";
"lng_boosts_show_more_boosts#one" = "Show {count} More Boosts";
"lng_boosts_show_more_boosts#other" = "Show {count} More Boosts";
"lng_boosts_show_more_gifts#one" = "Show {count} More Boosts";
@ -4593,8 +4705,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boosts_list_status" = "boost expires on {date}";
"lng_boosts_link_title" = "Link for boosting";
"lng_boosts_link_subtext" = "Share this link with your subscribers to get more boosts.";
"lng_boosts_link_subtext_group" = "Share this link with the members of your group to get more boosts.";
"lng_boosts_get_boosts" = "Get Boosts via Gifts";
"lng_boosts_get_boosts_subtext" = "Get more boosts for your channel by gifting Telegram Premium to your subscribers.";
"lng_boosts_get_boosts_subtext_group" = "Get more boosts for your group by gifting Telegram Premium to the members.";
"lng_boosts_list_unclaimed" = "Unclaimed";
"lng_boosts_list_pending" = "To be distributed";
"lng_boosts_list_pending_about" = "The recipient will be selected when the giveaway ends.";

View file

@ -13,5 +13,6 @@
<file alias="stats.tgs">../../animations/stats.tgs</file>
<file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file>
<file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file>
<file alias="palette.tgs">../../animations/palette.tgs</file>
</qresource>
</RCC>

View file

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

View file

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

View file

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

View file

@ -55,19 +55,42 @@ rpl::producer<std::vector<uint8>> PeerColors::suggestedValue() const {
auto PeerColors::indicesValue() const
-> rpl::producer<Ui::ColorIndicesCompressed> {
return rpl::single(_colorIndicesCurrent
? *_colorIndicesCurrent
: Ui::ColorIndicesCompressed()
return rpl::single(
indicesCurrent()
) | rpl::then(_colorIndicesChanged.events() | rpl::map([=] {
return *_colorIndicesCurrent;
return indicesCurrent();
}));
}
int PeerColors::requiredLevelFor(PeerId channel, uint8 index) const {
Ui::ColorIndicesCompressed PeerColors::indicesCurrent() const {
return _colorIndicesCurrent
? *_colorIndicesCurrent
: Ui::ColorIndicesCompressed();
}
const base::flat_map<uint8, int> &PeerColors::requiredLevelsGroup() const {
return _requiredLevelsGroup;
}
const base::flat_map<uint8, int> &PeerColors::requiredLevelsChannel() const {
return _requiredLevelsChannel;
}
int PeerColors::requiredGroupLevelFor(PeerId channel, uint8 index) const {
if (Data::DecideColorIndex(channel) == index) {
return 0;
} else if (const auto i = _requiredLevels.find(index)
; i != end(_requiredLevels)) {
} else if (const auto i = _requiredLevelsGroup.find(index)
; i != end(_requiredLevelsGroup)) {
return i->second;
}
return 1;
}
int PeerColors::requiredChannelLevelFor(PeerId channel, uint8 index) const {
if (Data::DecideColorIndex(channel) == index) {
return 0;
} else if (const auto i = _requiredLevelsChannel.find(index)
; i != end(_requiredLevelsChannel)) {
return i->second;
}
return 1;
@ -100,7 +123,8 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) {
};
const auto &list = data.vcolors().v;
_requiredLevels.clear();
_requiredLevelsGroup.clear();
_requiredLevelsChannel.clear();
suggested.reserve(list.size());
for (const auto &color : list) {
const auto &data = color.data();
@ -110,8 +134,11 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) {
continue;
}
const auto colorIndex = uint8(colorIndexBare);
if (const auto min = data.vgroup_min_level()) {
_requiredLevelsGroup[colorIndex] = min->v;
}
if (const auto min = data.vchannel_min_level()) {
_requiredLevels[colorIndex] = min->v;
_requiredLevelsChannel[colorIndex] = min->v;
}
if (!data.is_hidden()) {
suggested.push_back(colorIndex);

View file

@ -25,10 +25,19 @@ public:
[[nodiscard]] std::vector<uint8> suggested() const;
[[nodiscard]] rpl::producer<std::vector<uint8>> suggestedValue() const;
[[nodiscard]] Ui::ColorIndicesCompressed indicesCurrent() const;
[[nodiscard]] auto indicesValue() const
-> rpl::producer<Ui::ColorIndicesCompressed>;
[[nodiscard]] int requiredLevelFor(
[[nodiscard]] auto requiredLevelsGroup() const
-> const base::flat_map<uint8, int> &;
[[nodiscard]] auto requiredLevelsChannel() const
-> const base::flat_map<uint8, int> &;
[[nodiscard]] int requiredGroupLevelFor(
PeerId channel,
uint8 index) const;
[[nodiscard]] int requiredChannelLevelFor(
PeerId channel,
uint8 index) const;
@ -42,7 +51,8 @@ private:
mtpRequestId _requestId = 0;
base::Timer _timer;
rpl::variable<std::vector<uint8>> _suggested;
base::flat_map<uint8, int> _requiredLevels;
base::flat_map<uint8, int> _requiredLevelsGroup;
base::flat_map<uint8, int> _requiredLevelsChannel;
rpl::event_stream<> _colorIndicesChanged;
std::unique_ptr<Ui::ColorIndicesCompressed> _colorIndicesCurrent;

View file

@ -604,7 +604,7 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto channel = _peer->asChannel();
if (!channel || channel->isMegagroup()) {
if (!channel) {
return lifetime;
}
@ -628,6 +628,7 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
const auto slots = data.vmy_boost_slots();
_boostStatus.overview = Data::BoostsOverview{
.group = channel->isMegagroup(),
.mine = slots ? int(slots->v.size()) : 0,
.level = std::max(data.vlevel().v, 0),
.boostCount = std::max(

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_transcribes.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_session.h"
@ -25,6 +26,14 @@ Transcribes::Transcribes(not_null<ApiWrap*> api)
, _api(&api->instance()) {
}
bool Transcribes::freeFor(not_null<HistoryItem*> item) const {
if (const auto channel = item->history()->peer->asMegagroup()) {
const auto owner = &channel->owner();
return channel->levelHint() >= owner->groupFreeTranscribeLevel();
}
return false;
}
bool Transcribes::trialsSupport() {
if (!_trialsSupport) {
const auto count = _session->account().appConfig().get<int>(

View file

@ -36,6 +36,8 @@ public:
void apply(const MTPDupdateTranscribedAudio &update);
[[nodiscard]] bool freeFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool trialsSupport();
[[nodiscard]] TimeId trialsRefreshAt();
[[nodiscard]] int trialsCount();

View file

@ -1122,6 +1122,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
(d.is_out()
? peerToMTP(_session->userPeerId())
: MTP_peerUser(d.vuser_id())),
MTPint(), // from_boosts_applied
MTP_peerUser(d.vuser_id()),
MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
@ -1154,6 +1155,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTP_flags(flags),
d.vid(),
MTP_peerUser(d.vfrom_id()),
MTPint(), // from_boosts_applied
MTP_peerChat(d.vchat_id()),
MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),

View file

@ -2638,6 +2638,22 @@ void ApiWrap::setGroupStickerSet(
_session->data().stickers().notifyUpdated(Data::StickersType::Stickers);
}
void ApiWrap::setGroupEmojiSet(
not_null<ChannelData*> megagroup,
const StickerSetIdentifier &set) {
Expects(megagroup->mgInfo != nullptr);
megagroup->mgInfo->emojiSet = set;
request(MTPchannels_SetEmojiStickers(
megagroup->inputChannel,
Data::InputStickerSet(set)
)).send();
_session->changes().peerUpdated(
megagroup,
Data::PeerUpdate::Flag::EmojiSet);
_session->data().stickers().notifyUpdated(Data::StickersType::Emoji);
}
std::vector<not_null<DocumentData*>> *ApiWrap::stickersByEmoji(
const QString &key) {
const auto it = _stickersByEmoji.find(key);

View file

@ -246,6 +246,9 @@ public:
void setGroupStickerSet(
not_null<ChannelData*> megagroup,
const StickerSetIdentifier &set);
void setGroupEmojiSet(
not_null<ChannelData*> megagroup,
const StickerSetIdentifier &set);
[[nodiscard]] std::vector<not_null<DocumentData*>> *stickersByEmoji(
const QString &key);

View file

@ -467,6 +467,10 @@ bool BackgroundPreviewBox::forChannel() const {
return _forPeer && _forPeer->isChannel();
}
bool BackgroundPreviewBox::forGroup() const {
return forChannel() && _forPeer->isMegagroup();
}
void BackgroundPreviewBox::generateBackground() {
if (_paper.backgroundColors().empty()) {
return;
@ -492,7 +496,9 @@ void BackgroundPreviewBox::resetTitle() {
void BackgroundPreviewBox::rebuildButtons(bool dark) {
clearButtons();
addButton(forChannel()
addButton(forGroup()
? tr::lng_background_apply_group()
: forChannel()
? tr::lng_background_apply_channel()
: _forPeer
? tr::lng_background_apply_button()
@ -708,7 +714,7 @@ void BackgroundPreviewBox::checkLevelForChannel() {
return std::optional<Ui::AskBoostReason>();
}
return std::make_optional(Ui::AskBoostReason{
Ui::AskBoostWallpaper{ required }
Ui::AskBoostWallpaper{ required, _forPeer->isMegagroup()}
});
}, [=] { _forPeerLevelCheck = false; });
}
@ -1083,7 +1089,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector<QColor> &bg) {
_service = GenerateServiceItem(
delegate(),
_serviceHistory,
(forChannel()
(forGroup()
? tr::lng_background_other_group(tr::now)
: forChannel()
? tr::lng_background_other_channel(tr::now)
: (_forPeer && !_fromMessageId)
? tr::lng_background_other_info(

View file

@ -95,6 +95,7 @@ private:
[[nodiscard]] OverridenStyle prepareOverridenStyle(bool dark);
[[nodiscard]] bool forChannel() const;
[[nodiscard]] bool forGroup() const;
void checkLevelForChannel();
void recreate(bool dark);

View file

@ -791,6 +791,9 @@ slowmodeLabelsMargin: margins(0px, 5px, 0px, 0px);
slowmodeLabel: LabelSimple(defaultLabelSimple) {
textFg: windowSubTextFg;
}
boostsUnrestrictLabel: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
customBadgeField: InputField(defaultInputField) {
textMargins: margins(2px, 7px, 2px, 0px);

View file

@ -470,8 +470,8 @@ void EditCaptionBox::rebuildPreview() {
void EditCaptionBox::setupField() {
const auto peer = _historyItem->history()->peer;
const auto allow = [=](const auto&) {
return Data::AllowEmojiWithoutPremium(peer);
const auto allow = [=](not_null<DocumentData*> emoji) {
return Data::AllowEmojiWithoutPremium(peer, emoji);
};
InitMessageFieldHandlers(
_controller,

View file

@ -1363,20 +1363,26 @@ void GiveawayInfoBox(
? start->quantity
: (results->winnersCount + results->unclaimedCount);
const auto months = start ? start->months : results->months;
const auto group = results
? results->channel->isMegagroup()
: (!start->channels.empty()
&& start->channels.front()->isMegagroup());
text.append((finished
? tr::lng_prizes_end_text
: tr::lng_prizes_how_text)(
tr::now,
lt_admins,
tr::lng_prizes_admins(
tr::now,
lt_count,
quantity,
lt_channel,
Ui::Text::Bold(first),
lt_duration,
TextWithEntities{ GiftDuration(months) },
Ui::Text::RichLangValue),
(group
? tr::lng_prizes_admins_group
: tr::lng_prizes_admins)(
tr::now,
lt_count,
quantity,
lt_channel,
Ui::Text::Bold(first),
lt_duration,
TextWithEntities{ GiftDuration(months) },
Ui::Text::RichLangValue),
Ui::Text::RichLangValue));
const auto many = start
? (start->channels.size() > 1)
@ -1387,8 +1393,12 @@ void GiveawayInfoBox(
const auto all = start ? start->all : results->all;
auto winners = all
? (many
? tr::lng_prizes_winners_all_of_many
: tr::lng_prizes_winners_all_of_one)(
? (group
? tr::lng_prizes_winners_all_of_many_group
: tr::lng_prizes_winners_all_of_many)
: (group
? tr::lng_prizes_winners_all_of_one_group
: tr::lng_prizes_winners_all_of_one))(
tr::now,
lt_count,
count,
@ -1411,15 +1421,17 @@ void GiveawayInfoBox(
? results->additionalPrize
: start->additionalPrize;
if (!additionalPrize.isEmpty()) {
text.append("\n\n").append(tr::lng_prizes_additional_added(
tr::now,
lt_count,
count,
lt_channel,
Ui::Text::Bold(first),
lt_prize,
TextWithEntities{ additionalPrize },
Ui::Text::RichLangValue));
text.append("\n\n").append((group
? tr::lng_prizes_additional_added_group
: tr::lng_prizes_additional_added)(
tr::now,
lt_count,
count,
lt_channel,
Ui::Text::Bold(first),
lt_prize,
TextWithEntities{ additionalPrize },
Ui::Text::RichLangValue));
}
const auto untilDate = start
? start->untilDate
@ -1448,18 +1460,25 @@ void GiveawayInfoBox(
if (info.adminChannelId) {
const auto channel = controller->session().data().channel(
info.adminChannelId);
text.append("\n\n").append(tr::lng_prizes_how_no_admin(
tr::now,
lt_channel,
Ui::Text::Bold(channel->name()),
Ui::Text::RichLangValue));
text.append("\n\n").append((channel->isMegagroup()
? tr::lng_prizes_how_no_admin_group
: tr::lng_prizes_how_no_admin)(
tr::now,
lt_channel,
Ui::Text::Bold(channel->name()),
Ui::Text::RichLangValue));
} else if (info.tooEarlyDate) {
text.append("\n\n").append(tr::lng_prizes_how_no_joined(
tr::now,
lt_date,
Ui::Text::Bold(
langDateTime(base::unixtime::parse(info.tooEarlyDate))),
Ui::Text::RichLangValue));
const auto channel = controller->session().data().channel(
info.adminChannelId);
text.append("\n\n").append((channel->isMegagroup()
? tr::lng_prizes_how_no_joined_group
: tr::lng_prizes_how_no_joined)(
tr::now,
lt_date,
Ui::Text::Bold(
langDateTime(
base::unixtime::parse(info.tooEarlyDate))),
Ui::Text::RichLangValue));
} else if (!info.disallowedCountry.isEmpty()) {
text.append("\n\n").append(tr::lng_prizes_how_no_country(
tr::now,
@ -1499,7 +1518,9 @@ void GiveawayInfoBox(
box.get(),
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_prizes_cancelled(),
(group
? tr::lng_prizes_cancelled_group()
: tr::lng_prizes_cancelled()),
st::giveawayRefundedLabel),
st::giveawayRefundedPadding),
{ padding.left(), 0, padding.right(), padding.bottom() });

View file

@ -13,11 +13,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/background_box.h"
#include "boxes/stickers_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document_media.h"
#include "data/data_emoji_statuses.h"
#include "data/data_file_origin.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
@ -28,6 +32,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_emoji_status_panel.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "lottie/lottie_single_player.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
@ -478,6 +484,7 @@ void Set(
MTP_flags(Flag::f_color | Flag::f_background_emoji_id),
MTP_int(values.colorIndex),
MTP_long(values.backgroundEmojiId)));
} else if (peer->isMegagroup()) {
} else if (const auto channel = peer->asChannel()) {
using Flag = MTPchannels_UpdateColor::Flag;
send(MTPchannels_UpdateColor(
@ -527,9 +534,13 @@ void Apply(
} else {
CheckBoostLevel(show, peer, [=](int level) {
const auto peerColors = &peer->session().api().peerColors();
const auto colorRequired = peerColors->requiredLevelFor(
peer->id,
values.colorIndex);
const auto colorRequired = peer->isMegagroup()
? peerColors->requiredGroupLevelFor(
peer->id,
values.colorIndex)
: peerColors->requiredChannelLevelFor(
peer->id,
values.colorIndex);
const auto iconRequired = values.backgroundEmojiId
? session->account().appConfig().get<int>(
"channel_bg_icon_level_min",
@ -553,7 +564,10 @@ void Apply(
}
const auto reason = [&]() -> Ui::AskBoostReason {
if (level < statusRequired) {
return { Ui::AskBoostEmojiStatus{ statusRequired } };
return { Ui::AskBoostEmojiStatus{
statusRequired,
peer->isMegagroup()
} };
} else if (level < iconRequired) {
return { Ui::AskBoostChannelColor{ iconRequired } };
}
@ -670,6 +684,44 @@ int ColorSelector::resizeGetHeight(int newWidth) {
return (top - skip) + ((count % columns) ? (isize + skip) : 0);
}
[[nodiscard]] auto ButtonStyleWithAddedPadding(
not_null<Ui::RpWidget*> parent,
const style::SettingsButton &basicSt,
QMargins added) {
const auto st = parent->lifetime().make_state<style::SettingsButton>(
basicSt);
st->padding += added;
return st;
}
struct ButtonWithEmoji {
not_null<const style::SettingsButton*> st;
int emojiWidth = 0;
int noneWidth = 0;
int added = 0;
};
[[nodiscard]] ButtonWithEmoji ButtonStyleWithRightEmoji(
not_null<Ui::RpWidget*> parent) {
const auto ratio = style::DevicePixelRatio();
const auto emojiWidth = Data::FrameSizeFromTag({}) / ratio;
const auto noneWidth = st::normalFont->width(
tr::lng_settings_color_emoji_off(tr::now));
const auto added = st::normalFont->spacew;
const auto rightAdded = std::max(noneWidth, emojiWidth);
return {
.st = ButtonStyleWithAddedPadding(
parent,
st::peerAppearanceButton,
QMargins(0, 0, added + rightAdded, 0)),
.emojiWidth = emojiWidth,
.noneWidth = noneWidth,
.added = added,
};
}
[[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiIconButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
@ -677,22 +729,12 @@ int ColorSelector::resizeGetHeight(int newWidth) {
rpl::producer<uint8> colorIndexValue,
rpl::producer<DocumentId> emojiIdValue,
Fn<void(DocumentId)> emojiIdChosen) {
const auto &basicSt = st::settingsButtonNoIcon;
const auto ratio = style::DevicePixelRatio();
const auto added = st::normalFont->spacew;
const auto emojiSize = Data::FrameSizeFromTag({}) / ratio;
const auto noneWidth = added
+ st::normalFont->width(tr::lng_settings_color_emoji_off(tr::now));
const auto emojiWidth = added + emojiSize;
const auto rightPadding = std::max(noneWidth, emojiWidth)
+ basicSt.padding.right();
const auto st = parent->lifetime().make_state<style::SettingsButton>(
basicSt);
st->padding.setRight(rightPadding);
auto result = object_ptr<Ui::SettingsButton>(
const auto button = ButtonStyleWithRightEmoji(parent);
auto result = Settings::CreateButtonWithIcon(
parent,
tr::lng_settings_color_emoji(),
*st);
*button.st,
{ &st::menuBlueIconColorNames });
const auto raw = result.data();
const auto right = Ui::CreateChild<Ui::RpWidget>(raw);
@ -719,6 +761,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
}, right->lifetime());
const auto session = &show->session();
const auto added = st::normalFont->spacew;
std::move(emojiIdValue) | rpl::start_with_next([=](DocumentId emojiId) {
state->emojiId = emojiId;
state->emoji = emojiId
@ -727,7 +770,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
[=] { right->update(); })
: nullptr;
right->resize(
(emojiId ? emojiWidth : noneWidth) + added,
(emojiId ? button.emojiWidth : button.noneWidth) + button.added,
right->height());
right->update();
}, right->lifetime());
@ -738,7 +781,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
) | rpl::start_with_next([=](QSize outer, int width) {
right->resize(width, outer.height());
const auto skip = st::settingsButton.padding.right();
right->moveToRight(skip - added, 0, outer.width());
right->moveToRight(skip - button.added, 0, outer.width());
}, right->lifetime());
right->paintRequest(
@ -752,7 +795,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
const auto colors = style->coloredValues(false, state->index);
state->emoji->paint(p, {
.textColor = colors.name,
.position = QPoint(added, (height - emojiSize) / 2),
.position = QPoint(added, (height - button.emojiWidth) / 2),
.internal = {
.forceFirstFrame = true,
},
@ -791,23 +834,16 @@ int ColorSelector::resizeGetHeight(int newWidth) {
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<DocumentId> statusIdValue,
Fn<void(DocumentId,TimeId)> statusIdChosen) {
const auto &basicSt = st::settingsButtonNoIcon;
const auto ratio = style::DevicePixelRatio();
const auto added = st::normalFont->spacew;
const auto emojiSize = Data::FrameSizeFromTag({}) / ratio;
const auto noneWidth = added
+ st::normalFont->width(tr::lng_settings_color_emoji_off(tr::now));
const auto emojiWidth = added + emojiSize;
const auto rightPadding = std::max(noneWidth, emojiWidth)
+ basicSt.padding.right();
const auto st = parent->lifetime().make_state<style::SettingsButton>(
basicSt);
st->padding.setRight(rightPadding);
auto result = object_ptr<Ui::SettingsButton>(
Fn<void(DocumentId,TimeId)> statusIdChosen,
bool group) {
const auto button = ButtonStyleWithRightEmoji(parent);
auto result = Settings::CreateButtonWithIcon(
parent,
tr::lng_edit_channel_status(),
*st);
(group
? tr::lng_edit_channel_status_group()
: tr::lng_edit_channel_status()),
*button.st,
{ &st::menuBlueIconEmojiStatus });
const auto raw = result.data();
const auto right = Ui::CreateChild<Ui::RpWidget>(raw);
@ -834,7 +870,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
[=] { right->update(); })
: nullptr;
right->resize(
(id ? emojiWidth : noneWidth) + added,
(id ? button.emojiWidth : button.noneWidth) + button.added,
right->height());
right->update();
}, right->lifetime());
@ -845,7 +881,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
) | rpl::start_with_next([=](QSize outer, int width) {
right->resize(width, outer.height());
const auto skip = st::settingsButton.padding.right();
right->moveToRight(skip - added, 0, outer.width());
right->moveToRight(skip - button.added, 0, outer.width());
}, right->lifetime());
right->paintRequest(
@ -861,14 +897,18 @@ int ColorSelector::resizeGetHeight(int newWidth) {
st::stickerPanPremium1,
st::stickerPanPremium2,
0.5),
.position = QPoint(added, (height - emojiSize) / 2),
.position = QPoint(
button.added,
(height - button.emojiWidth) / 2),
});
} else {
const auto &font = st::normalFont;
p.setFont(font);
p.setPen(st::windowActiveTextFg);
p.drawText(
QPoint(added, (height - font->height) / 2 + font->ascent),
QPoint(
button.added,
(height - font->height) / 2 + font->ascent),
tr::lng_settings_color_emoji_off(tr::now));
}
}, right->lifetime());
@ -889,6 +929,116 @@ int ColorSelector::resizeGetHeight(int newWidth) {
return result;
}
[[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiPackButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> channel) {
Expects(channel->mgInfo != nullptr);
const auto button = ButtonStyleWithRightEmoji(parent);
auto result = Settings::CreateButtonWithIcon(
parent,
tr::lng_group_emoji(),
*button.st,
{ &st::menuBlueIconEmojiPack });
const auto raw = result.data();
struct State {
DocumentData *icon = nullptr;
std::unique_ptr<Ui::Text::CustomEmoji> custom;
QImage cache;
};
const auto state = parent->lifetime().make_state<State>();
const auto right = Ui::CreateChild<Ui::RpWidget>(raw);
right->show();
right->resize(
button.emojiWidth + button.added,
right->height());
rpl::combine(
raw->sizeValue(),
right->widthValue()
) | rpl::start_with_next([=](QSize outer, int width) {
right->resize(width, outer.height());
const auto skip = st::settingsButton.padding.right();
right->moveToRight(skip - button.added, 0, outer.width());
}, right->lifetime());
right->paintRequest(
) | rpl::filter([=] {
return state->icon != nullptr;
}) | rpl::start_with_next([=] {
auto p = QPainter(right);
const auto x = button.added;
const auto y = (right->height() - button.emojiWidth) / 2;
const auto active = right->window()->isActiveWindow();
if (const auto emoji = state->icon) {
if (!state->custom
&& emoji->sticker()
&& emoji->sticker()->setType == Data::StickersType::Emoji) {
auto &manager = emoji->owner().customEmojiManager();
state->custom = manager.create(
emoji->id,
[=] { right->update(); },
{});
}
if (state->custom) {
state->custom->paint(p, Ui::Text::CustomEmoji::Context{
.textColor = st::windowFg->c,
.now = crl::now(),
.position = { x, y },
.paused = !active,
});
}
}
}, right->lifetime());
raw->setClickedCallback([=] {
const auto isEmoji = true;
show->showBox(Box<StickersBox>(show, channel, isEmoji));
});
channel->session().changes().peerFlagsValue(
channel,
Data::PeerUpdate::Flag::EmojiSet
) | rpl::map([=]() -> rpl::producer<DocumentData*> {
const auto id = channel->mgInfo->emojiSet.id;
if (!id) {
return rpl::single<DocumentData*>(nullptr);
}
const auto sets = &channel->owner().stickers().sets();
auto wrapLoaded = [=](Data::StickersSets::const_iterator it) {
return it->second->lookupThumbnailDocument();
};
const auto it = sets->find(id);
if (it != sets->cend()
&& !(it->second->flags & Data::StickersSetFlag::NotLoaded)) {
return rpl::single(wrapLoaded(it));
}
return rpl::single<DocumentData*>(
nullptr
) | rpl::then(channel->owner().stickers().updated(
Data::StickersType::Emoji
) | rpl::filter([=] {
const auto it = sets->find(id);
return (it != sets->cend())
&& !(it->second->flags & Data::StickersSetFlag::NotLoaded);
}) | rpl::map([=] {
return wrapLoaded(sets->find(id));
}));
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](DocumentData *icon) {
if (state->icon != icon) {
state->icon = icon;
state->custom = nullptr;
right->update();
}
}, right->lifetime());
return result;
}
} // namespace
void EditPeerColorBox(
@ -897,7 +1047,12 @@ void EditPeerColorBox(
not_null<PeerData*> peer,
std::shared_ptr<Ui::ChatStyle> style,
std::shared_ptr<Ui::ChatTheme> theme) {
box->setTitle(tr::lng_settings_color_title());
const auto group = peer->isMegagroup();
const auto container = box->verticalLayout();
box->setTitle(peer->isSelf()
? tr::lng_settings_color_title()
: tr::lng_edit_channel_color());
box->setWidth(st::boxWideWidth);
struct State {
@ -914,53 +1069,94 @@ void EditPeerColorBox(
state->emojiId = peer->backgroundEmojiId();
state->statusId = peer->emojiStatusId();
box->addRow(object_ptr<PreviewWrap>(
box,
style,
theme,
peer,
state->index.value(),
state->emojiId.value()
), {});
if (group) {
const auto divider = Ui::CreateChild<Ui::BoxContentDivider>(
box.get());
const auto verticalLayout = box->verticalLayout()->add(
object_ptr<Ui::VerticalLayout>(box.get()));
auto indices = peer->session().api().peerColors().suggestedValue();
const auto margin = st::settingsColorRadioMargin;
const auto skip = st::settingsColorRadioSkip;
box->addRow(
object_ptr<ColorSelector>(
auto icon = CreateLottieIcon(
verticalLayout,
{
.name = u"palette"_q,
.sizeOverride = {
st::settingsCloudPasswordIconSize,
st::settingsCloudPasswordIconSize,
},
},
st::peerAppearanceIconPadding);
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
});
verticalLayout->add(std::move(icon.widget));
verticalLayout->add(
object_ptr<Ui::FlatLabel>(
verticalLayout,
tr::lng_boost_group_about(),
st::peerAppearanceCoverLabel),
st::peerAppearanceCoverLabelMargin);
verticalLayout->geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
divider->setGeometry(r);
}, divider->lifetime());
} else {
box->addRow(object_ptr<PreviewWrap>(
box,
style,
std::move(indices),
state->index.current(),
[=](uint8 index) { state->index = index; }),
{ margin, skip, margin, skip });
theme,
peer,
state->index.value(),
state->emojiId.value()
), {});
const auto container = box->verticalLayout();
Ui::AddDividerText(container, peer->isSelf()
? tr::lng_settings_color_about()
: tr::lng_settings_color_about_channel());
auto indices = peer->session().api().peerColors().suggestedValue();
const auto margin = st::settingsColorRadioMargin;
const auto skip = st::settingsColorRadioSkip;
box->addRow(
object_ptr<ColorSelector>(
box,
style,
std::move(indices),
state->index.current(),
[=](uint8 index) { state->index = index; }),
{ margin, skip, margin, skip });
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(
container,
(peer->isSelf()
? tr::lng_settings_color_about()
: tr::lng_settings_color_about_channel()),
st::peerAppearanceDividerTextMargin);
container->add(CreateEmojiIconButton(
container,
show,
style,
state->index.value(),
state->emojiId.value(),
[=](DocumentId id) { state->emojiId = id; }));
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(container, peer->isSelf()
? tr::lng_settings_color_emoji_about()
: tr::lng_settings_color_emoji_about_channel());
container->add(CreateEmojiIconButton(
container,
show,
style,
state->index.value(),
state->emojiId.value(),
[=](DocumentId id) { state->emojiId = id; }));
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(
container,
(peer->isSelf()
? tr::lng_settings_color_emoji_about()
: tr::lng_settings_color_emoji_about_channel()),
st::peerAppearanceDividerTextMargin);
}
if (const auto channel = peer->asChannel()) {
Ui::AddSkip(container, st::settingsColorSampleSkip);
container->add(object_ptr<Ui::SettingsButton>(
Settings::AddButtonWithIcon(
container,
tr::lng_edit_channel_wallpaper(),
st::settingsButtonNoIcon)
(group
? tr::lng_edit_channel_wallpaper_group()
: tr::lng_edit_channel_wallpaper()),
st::peerAppearanceButton,
{ &st::menuBlueIconWallpaper }
)->setClickedCallback([=] {
const auto usage = ChatHelpers::WindowUsage::PremiumPromo;
if (const auto strong = show->resolveWindow(usage)) {
@ -971,7 +1167,25 @@ void EditPeerColorBox(
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(
container,
tr::lng_edit_channel_wallpaper_about());
(group
? tr::lng_edit_channel_wallpaper_about_group()
: tr::lng_edit_channel_wallpaper_about()),
st::peerAppearanceDividerTextMargin);
if (group) {
Ui::AddSkip(container, st::settingsColorSampleSkip);
container->add(CreateEmojiPackButton(
container,
show,
channel));
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(
container,
tr::lng_group_emoji_description(),
st::peerAppearanceDividerTextMargin);
}
// Preload exceptions list.
const auto peerPhoto = &channel->session().api().peerPhoto();
@ -992,10 +1206,16 @@ void EditPeerColorBox(
state->statusId = id;
state->statusUntil = until;
state->statusChanged = true;
}));
},
group));
Ui::AddSkip(container, st::settingsColorSampleSkip);
Ui::AddDividerText(container, tr::lng_edit_channel_status_about());
Ui::AddDividerText(
container,
(group
? tr::lng_edit_channel_status_about_group()
: tr::lng_edit_channel_status_about()),
st::peerAppearanceDividerTextMargin);
}
box->addButton(tr::lng_settings_apply(), [=] {
@ -1020,19 +1240,11 @@ void EditPeerColorBox(
});
}
void AddPeerColorButton(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer) {
auto label = peer->isSelf()
? tr::lng_settings_theme_name_color()
: tr::lng_edit_channel_color();
const auto button = AddButtonWithIcon(
container,
rpl::duplicate(label),
st::settingsColorButton,
{ &st::menuIconChangeColors });
void SetupPeerColorSample(
not_null<Button*> button,
not_null<PeerData*> peer,
rpl::producer<QString> label,
std::shared_ptr<Ui::ChatStyle> style) {
auto colorIndexValue = peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Color
@ -1041,12 +1253,6 @@ void AddPeerColorButton(
});
const auto name = peer->shortName();
const auto style = std::make_shared<Ui::ChatStyle>(
peer->session().colorIndicesValue());
const auto theme = std::shared_ptr<Ui::ChatTheme>(
Window::Theme::DefaultChatThemeOn(button->lifetime()));
style->apply(theme.get());
const auto sample = Ui::CreateChild<ColorSample>(
button.get(),
style,
@ -1098,6 +1304,30 @@ void AddPeerColorButton(
}, sample->lifetime());
sample->setAttribute(Qt::WA_TransparentForMouseEvents);
}
void AddPeerColorButton(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer) {
auto label = peer->isSelf()
? tr::lng_settings_theme_name_color()
: tr::lng_edit_channel_color();
const auto button = AddButtonWithIcon(
container,
rpl::duplicate(label),
st::settingsColorButton,
{ &st::menuIconChangeColors });
const auto style = std::make_shared<Ui::ChatStyle>(
peer->session().colorIndicesValue());
const auto theme = std::shared_ptr<Ui::ChatTheme>(
Window::Theme::DefaultChatThemeOn(button->lifetime()));
style->apply(theme.get());
if (!peer->isMegagroup()) {
SetupPeerColorSample(button, peer, rpl::duplicate(label), style);
}
button->setClickedCallback([=] {
show->show(Box(EditPeerColorBox, show, peer, style, theme));

View file

@ -198,6 +198,37 @@ void SaveSlowmodeSeconds(
api->registerModifyRequest(key, requestId);
}
void SaveBoostsUnrestrict(
not_null<ChannelData*> channel,
int boostsUnrestrict,
Fn<void()> done) {
const auto api = &channel->session().api();
const auto key = Api::RequestKey("boosts_unrestrict", channel->id);
const auto requestId = api->request(
MTPchannels_SetBoostsToUnblockRestrictions(
channel->inputChannel,
MTP_int(boostsUnrestrict))
).done([=](const MTPUpdates &result) {
api->clearModifyRequest(key);
api->applyUpdates(result);
channel->setBoostsUnrestrict(
channel->boostsApplied(),
boostsUnrestrict);
done();
}).fail([=](const MTP::Error &error) {
api->clearModifyRequest(key);
if (error.type() != u"CHAT_NOT_MODIFIED"_q) {
return;
}
channel->setBoostsUnrestrict(
channel->boostsApplied(),
boostsUnrestrict);
done();
}).send();
api->registerModifyRequest(key, requestId);
}
void ShowEditPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
@ -215,6 +246,10 @@ void ShowEditPermissions(
close);
if (const auto channel = peer->asChannel()) {
SaveSlowmodeSeconds(channel, result.slowmodeSeconds, close);
SaveBoostsUnrestrict(
channel,
result.boostsUnrestrict,
close);
}
};
auto done = [=](EditPeerPermissionsBoxResult result) {
@ -225,7 +260,8 @@ void ShowEditPermissions(
const auto saveFor = peer->migrateToOrMe();
const auto chat = saveFor->asChat();
if (!result.slowmodeSeconds || !chat) {
if (!chat
|| (!result.slowmodeSeconds && !result.boostsUnrestrict)) {
save(saveFor, result);
return;
}
@ -595,8 +631,9 @@ object_ptr<Ui::RpWidget> Controller::createStickersEdit() {
tr::lng_group_stickers_add(),
rpl::single(QString()), //Empty count.
[=, controller = _navigation->parentController()] {
const auto isEmoji = false;
controller->show(
Box<StickersBox>(controller->uiShow(), channel));
Box<StickersBox>(controller->uiShow(), channel, isEmoji));
},
{ &st::menuIconStickers });
@ -1057,9 +1094,7 @@ void Controller::fillManageSection() {
&& (channel->hasAdminRights() || channel->amCreator());
const auto canEditStickers = isChannel && channel->canEditStickers();
const auto canDeleteChannel = isChannel && channel->canDelete();
const auto canEditColorIndex = isChannel
&& !channel->isMegagroup()
&& channel->canEditInformation();
const auto canEditColorIndex = isChannel && channel->canEditEmoji();
const auto canViewOrEditLinkedChat = isChannel
&& (channel->linkedChat()
|| (channel->isBroadcast() && channel->canEditInformation()));

View file

@ -8,8 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_permissions_box.h"
#include "lang/lang_keys.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "ui/effects/toggle_arrow.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
@ -34,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
#include "styles/style_window.h"
@ -42,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kSlowmodeValues = 7;
constexpr auto kBoostsUnrestrictValues = 5;
constexpr auto kSuggestGigagroupThreshold = 199000;
constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);
@ -99,7 +104,7 @@ constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);
using Flag = ChatAdminRight;
if (options.isGroup) {
auto result = std::vector<AdminRightLabel>{
auto first = std::vector<AdminRightLabel>{
{ Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) },
{ Flag::DeleteMessages, tr::lng_rights_group_delete(tr::now) },
{ Flag::BanUsers, tr::lng_rights_group_ban(tr::now) },
@ -108,19 +113,30 @@ constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);
: tr::lng_rights_group_invite(tr::now) },
{ Flag::ManageTopics, tr::lng_rights_group_topics(tr::now) },
{ Flag::PinMessages, tr::lng_rights_group_pin(tr::now) },
};
auto stories = std::vector<AdminRightLabel>{
{ Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) },
{ Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) },
{ Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) },
};
auto second = std::vector<AdminRightLabel>{
{ Flag::ManageCall, tr::lng_rights_group_manage_calls(tr::now) },
{ Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) },
{ Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) },
};
if (!options.isForum) {
result.erase(
first.erase(
ranges::remove(
result,
first,
Flag::ManageTopics | Flag(),
&AdminRightLabel::flags),
end(result));
end(first));
}
return { { std::nullopt, std::move(result) } };
return {
{ std::nullopt, std::move(first) },
{ tr::lng_rights_channel_manage_stories(), std::move(stories) },
{ std::nullopt, std::move(second) },
};
}
auto first = std::vector<AdminRightLabel>{
{ Flag::ChangeInfo, tr::lng_rights_channel_info(tr::now) },
@ -163,6 +179,10 @@ int SlowmodeDelayByIndex(int index) {
Unexpected("Index in SlowmodeDelayByIndex.");
}
[[nodiscard]] int BoostsUnrestrictByIndex(int index) {
return index + 1;
}
template <typename CheckboxesMap, typename DependenciesMap>
void ApplyDependencies(
const CheckboxesMap &checkboxes,
@ -770,14 +790,14 @@ void AddSlowmodeLabels(
}
}
Fn<int()> AddSlowmodeSlider(
rpl::producer<int> AddSlowmodeSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer) {
using namespace rpl::mappers;
if (const auto chat = peer->asChat()) {
if (!chat->amCreator()) {
return [] { return 0; };
return rpl::single(0);
}
}
const auto channel = peer->asChannel();
@ -785,10 +805,6 @@ Fn<int()> AddSlowmodeSlider(
const auto secondsCount = lifetime.make_state<rpl::variable<int>>(
channel ? channel->slowmodeSeconds() : 0);
container->add(
object_ptr<Ui::BoxContentDivider>(container),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
container->add(
object_ptr<Ui::FlatLabel>(
container,
@ -858,7 +874,157 @@ Fn<int()> AddSlowmodeSlider(
st::proxyAboutPadding),
style::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip));
return [=] { return secondsCount->current(); };
return secondsCount->value();
}
void AddBoostsUnrestrictLabels(
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session) {
const auto labels = container->add(
object_ptr<Ui::FixedHeightWidget>(container, st::normalFont->height),
st::slowmodeLabelsMargin);
const auto manager = &session->data().customEmojiManager();
const auto one = Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji(
st::boostMessageIcon,
st::boostMessageIconPadding));
const auto many = Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji(
st::boostsMessageIcon,
st::boostsMessageIconPadding));
const auto context = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [] {},
.customEmojiLoopLimit = 1,
};
for (auto i = 0; i != kBoostsUnrestrictValues; ++i) {
const auto label = Ui::CreateChild<Ui::FlatLabel>(
labels,
st::boostsUnrestrictLabel);
label->setMarkedText(
TextWithEntities(i ? many : one).append(QString::number(i + 1)),
context);
rpl::combine(
labels->widthValue(),
label->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto skip = st::localStorageLimitMargin;
const auto size = st::localStorageLimitSlider.seekSize;
const auto available = outer
- skip.left()
- skip.right()
- size.width();
const auto shift = (i == 0)
? -(size.width() / 2)
: (i + 1 == kBoostsUnrestrictValues)
? (size.width() - (size.width() / 2) - inner)
: (-inner / 2);
const auto left = skip.left()
+ (size.width() / 2)
+ (i * available) / (kBoostsUnrestrictValues - 1)
+ shift;
label->moveToLeft(left, 0, outer);
}, label->lifetime());
}
}
rpl::producer<int> AddBoostsUnrestrictSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer) {
using namespace rpl::mappers;
if (const auto chat = peer->asChat()) {
if (!chat->amCreator()) {
return rpl::single(0);
}
}
const auto channel = peer->asChannel();
auto &lifetime = container->lifetime();
const auto boostsUnrestrict = lifetime.make_state<rpl::variable<int>>(
channel ? channel->boostsUnrestrict() : 0);
container->add(
object_ptr<Ui::BoxContentDivider>(container),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
auto enabled = boostsUnrestrict->value(
) | rpl::map(_1 > 0);
container->add(object_ptr<Ui::SettingsButton>(
container,
tr::lng_rights_boosts_no_restrict(),
st::defaultSettingsButton
))->toggleOn(rpl::duplicate(enabled))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
if (toggled && !boostsUnrestrict->current()) {
*boostsUnrestrict = 1;
} else if (!toggled && boostsUnrestrict->current()) {
*boostsUnrestrict = 0;
}
}, container->lifetime());
const auto outer = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
outer->toggleOn(rpl::duplicate(enabled), anim::type::normal);
outer->finishAnimating();
const auto inner = outer->entity();
AddBoostsUnrestrictLabels(inner, &peer->session());
const auto slider = inner->add(
object_ptr<Ui::MediaSlider>(inner, st::localStorageLimitSlider),
st::localStorageLimitMargin);
slider->resize(st::localStorageLimitSlider.seekSize);
slider->setPseudoDiscrete(
kBoostsUnrestrictValues,
BoostsUnrestrictByIndex,
boostsUnrestrict->current(),
[=](int boosts) {
(*boostsUnrestrict) = boosts;
});
inner->add(
object_ptr<Ui::DividerLabel>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
rpl::conditional(
boostsUnrestrict->value() | rpl::map(_1 > 0),
tr::lng_rights_boosts_about_on(),
tr::lng_rights_boosts_about()),
st::boxDividerLabel),
st::proxyAboutPadding),
style::margins(0, st::infoProfileSkip, 0, 0));
return boostsUnrestrict->value();
}
rpl::producer<int> AddBoostsUnrestrictWrapped(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
rpl::producer<bool> shown) {
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
wrap->toggleOn(rpl::duplicate(shown), anim::type::normal);
wrap->finishAnimating();
auto result = AddBoostsUnrestrictSlider(wrap->entity(), peer);
const auto divider = container->add(
object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
container,
object_ptr<Ui::BoxContentDivider>(container),
QMargins{ 0, st::infoProfileSkip, 0, st::infoProfileSkip }));
divider->toggleOn(rpl::combine(
std::move(shown),
rpl::duplicate(result),
!rpl::mappers::_1 || !rpl::mappers::_2));
divider->finishAnimating();
return result;
}
void AddSuggestGigagroup(
@ -985,7 +1151,40 @@ void ShowEditPeerPermissionsBox(
inner->add(std::move(checkboxes));
const auto getSlowmodeSeconds = AddSlowmodeSlider(inner, peer);
struct State {
rpl::variable<int> slowmodeSeconds;
rpl::variable<int> boostsUnrestrict;
rpl::variable<bool> hasSendRestrictions;
};
static constexpr auto kSendRestrictions = Flag::EmbedLinks
| Flag::SendGames
| Flag::SendGifs
| Flag::SendInline
| Flag::SendPolls
| Flag::SendStickers
| Flag::SendPhotos
| Flag::SendVideos
| Flag::SendVideoMessages
| Flag::SendMusic
| Flag::SendVoiceMessages
| Flag::SendFiles
| Flag::SendOther;
const auto state = inner->lifetime().make_state<State>();
state->hasSendRestrictions = ((restrictions & kSendRestrictions) != 0)
|| (peer->isChannel() && peer->asChannel()->slowmodeSeconds() > 0);
state->boostsUnrestrict = AddBoostsUnrestrictWrapped(
inner,
peer,
state->hasSendRestrictions.value());
state->slowmodeSeconds = AddSlowmodeSlider(inner, peer);
state->hasSendRestrictions = rpl::combine(
rpl::single(
restrictions
) | rpl::then(std::move(changes)),
state->slowmodeSeconds.value()
) | rpl::map([](ChatRestrictions restrictions, int slowmodeSeconds) {
return ((restrictions & kSendRestrictions) != 0) || slowmodeSeconds;
});
if (const auto channel = peer->asChannel()) {
if (channel->amCreator()
@ -1001,7 +1200,18 @@ void ShowEditPeerPermissionsBox(
AddBannedButtons(inner, navigation, peer);
box->addButton(tr::lng_settings_save(), [=, rights = getRestrictions] {
done({ rights(), getSlowmodeSeconds() });
const auto restrictions = rights();
const auto slowmodeSeconds = state->slowmodeSeconds.current();
const auto hasRestrictions = (slowmodeSeconds > 0)
|| ((restrictions & kSendRestrictions) != 0);
const auto boostsUnrestrict = hasRestrictions
? state->boostsUnrestrict.current()
: 0;
done({
restrictions,
slowmodeSeconds,
boostsUnrestrict,
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });

View file

@ -37,6 +37,7 @@ class SessionNavigation;
struct EditPeerPermissionsBoxResult final {
ChatRestrictions rights;
int slowmodeSeconds = 0;
int boostsUnrestrict = 0;
};
void ShowEditPeerPermissionsBox(

View file

@ -7,10 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/replace_boost_box.h"
#include "api/api_peer_colors.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "boxes/peer_list_box.h"
#include "data/data_channel.h"
#include "data/data_cloud_themes.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
@ -313,21 +316,23 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
}
}
object_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds) {
object_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds, bool group) {
const auto days = seconds / 86400;
const auto hours = seconds / 3600;
const auto minutes = seconds / 60;
return Ui::MakeInformBox({
.text = tr::lng_boost_error_flood_text(
lt_left,
rpl::single(Ui::Text::Bold((days > 1)
? tr::lng_days(tr::now, lt_count, days)
: (hours > 1)
? tr::lng_hours(tr::now, lt_count, hours)
: (minutes > 1)
? tr::lng_minutes(tr::now, lt_count, minutes)
: tr::lng_seconds(tr::now, lt_count, seconds))),
Ui::Text::RichLangValue),
.text = (group
? tr::lng_boost_error_flood_text_group
: tr::lng_boost_error_flood_text)(
lt_left,
rpl::single(Ui::Text::Bold((days > 1)
? tr::lng_days(tr::now, lt_count, days)
: (hours > 1)
? tr::lng_hours(tr::now, lt_count, hours)
: (minutes > 1)
? tr::lng_minutes(tr::now, lt_count, minutes)
: tr::lng_seconds(tr::now, lt_count, seconds))),
Ui::Text::RichLangValue),
.title = tr::lng_boost_error_flood_title(),
});
}
@ -335,14 +340,15 @@ object_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds) {
object_ptr<Ui::BoxContent> ReassignBoostSingleBox(
not_null<ChannelData*> to,
TakenBoostSlot from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
Fn<void()> cancel) {
const auto reassigned = std::make_shared<bool>();
const auto slot = from.id;
const auto peer = to->owner().peer(from.peerId);
const auto group = peer->isMegagroup();
const auto confirmed = [=](Fn<void()> close) {
*reassigned = true;
reassign({ slot }, 1);
reassign({ slot }, group ? 1 : 0, group ? 0 : 1);
close();
};
@ -417,35 +423,94 @@ Ui::BoostCounters ParseBoostCounters(
};
}
Ui::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) {
const auto group = channel->isMegagroup();
const auto appConfig = &channel->session().account().appConfig();
const auto get = [&](const QString &key, int fallback, bool ok = true) {
return ok ? appConfig->get<int>(key, fallback) : 0;
};
auto nameColorsByLevel = base::flat_map<int, int>();
auto linkStylesByLevel = base::flat_map<int, int>();
const auto peerColors = &channel->session().api().peerColors();
const auto &list = group
? peerColors->requiredLevelsGroup()
: peerColors->requiredLevelsChannel();
const auto indices = peerColors->indicesCurrent();
for (const auto &[index, level] : list) {
if (!Ui::ColorPatternIndex(indices, index, false)) {
++nameColorsByLevel[level];
}
++linkStylesByLevel[level];
}
const auto &themes = channel->owner().cloudThemes().chatThemes();
if (themes.empty()) {
channel->owner().cloudThemes().refreshChatThemes();
}
return Ui::BoostFeatures{
.nameColorsByLevel = std::move(nameColorsByLevel),
.linkStylesByLevel = std::move(linkStylesByLevel),
.linkLogoLevel = get(u"channel_bg_icon_level_min"_q, 4, !group),
.transcribeLevel = get(u"group_transcribe_level_min"_q, 6, group),
.emojiPackLevel = get(u"group_emoji_stickers_level_min"_q, 4, group),
.emojiStatusLevel = get(group
? u"group_emoji_status_level_min"_q
: u"channel_emoji_status_level_min"_q, 8),
.wallpaperLevel = get(group
? u"group_wallpaper_level_min"_q
: u"channel_wallpaper_level_min"_q, 9),
.wallpapersCount = themes.empty() ? 8 : int(themes.size()),
.customWallpaperLevel = get(group
? u"channel_custom_wallpaper_level_min"_q
: u"group_custom_wallpaper_level_min"_q, 10),
};
}
int BoostsForGift(not_null<Main::Session*> session) {
const auto key = u"boosts_per_sent_gift"_q;
return session->account().appConfig().get<int>(key, 0);
}
[[nodiscard]] int SourcesCount(
struct Sources {
int groups = 0;
int channels = 0;
};
[[nodiscard]] Sources SourcesCount(
not_null<ChannelData*> to,
const std::vector<TakenBoostSlot> &from,
const std::vector<int> &slots) {
auto checked = base::flat_set<PeerId>();
checked.reserve(slots.size());
auto groups = base::flat_set<PeerId>();
groups.reserve(slots.size());
auto channels = base::flat_set<PeerId>();
channels.reserve(slots.size());
const auto owner = &to->owner();
for (const auto slot : slots) {
const auto i = ranges::find(from, slot, &TakenBoostSlot::id);
Assert(i != end(from));
checked.emplace(i->peerId);
const auto id = i->peerId;
if (!groups.contains(id) && !channels.contains(id)) {
(owner->peer(id)->isMegagroup() ? groups : channels).insert(id);
}
}
return checked.size();
return {
.groups = int(groups.size()),
.channels = int(channels.size()),
};
}
object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
Fn<void()> cancel) {
Expects(!from.empty());
const auto now = base::unixtime::now();
if (from.size() == 1 && from.front().cooldown > now) {
cancel();
return ReassignBoostFloodBox(from.front().cooldown - now);
return ReassignBoostFloodBox(
from.front().cooldown - now,
to->owner().peer(from.front().peerId)->isMegagroup());
} else if (from.size() == 1 && from.front().peerId) {
return ReassignBoostSingleBox(to, from.front(), reassign, cancel);
}
@ -457,10 +522,10 @@ object_ptr<Ui::BoxContent> ReassignBoostsBox(
) | rpl::start_with_next([=](std::vector<int> slots) {
box->clearButtons();
if (!slots.empty()) {
const auto sources = SourcesCount(from, slots);
const auto sources = SourcesCount(to, from, slots);
box->addButton(tr::lng_boost_reassign_button(), [=] {
*reassigned = true;
reassign(slots, sources);
reassign(slots, sources.groups, sources.channels);
});
}
box->addButton(tr::lng_cancel(), [=] {

View file

@ -9,12 +9,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
class ChannelData;
namespace Main {
class Session;
} // namespace Main
namespace Ui {
struct BoostCounters;
struct BoostFeatures;
class BoxContent;
class RpWidget;
} // namespace Ui
@ -39,12 +42,15 @@ struct ForChannelBoostSlots {
[[nodiscard]] Ui::BoostCounters ParseBoostCounters(
const MTPpremium_BoostsStatus &status);
[[nodiscard]] Ui::BoostFeatures LookupBoostFeatures(
not_null<ChannelData*> channel);
[[nodiscard]] int BoostsForGift(not_null<Main::Session*> session);
object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
Fn<void()> cancel);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(

View file

@ -112,6 +112,10 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_subtitle_infinite_reactions();
case PremiumPreview::TagsForMessages:
return tr::lng_premium_summary_subtitle_tags_for_messages();
case PremiumPreview::LastSeen:
return tr::lng_premium_summary_subtitle_last_seen();
case PremiumPreview::MessagePrivacy:
return tr::lng_premium_summary_subtitle_message_privacy();
case PremiumPreview::Stickers:
return tr::lng_premium_summary_subtitle_premium_stickers();
case PremiumPreview::AnimatedEmoji:
@ -150,6 +154,10 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_about_infinite_reactions();
case PremiumPreview::TagsForMessages:
return tr::lng_premium_summary_about_tags_for_messages();
case PremiumPreview::LastSeen:
return tr::lng_premium_summary_about_last_seen();
case PremiumPreview::MessagePrivacy:
return tr::lng_premium_summary_about_message_privacy();
case PremiumPreview::Stickers:
return tr::lng_premium_summary_about_premium_stickers();
case PremiumPreview::AnimatedEmoji:
@ -480,6 +488,8 @@ struct VideoPreviewDocument {
case PremiumPreview::AnimatedUserpics: return "animated_userpics";
case PremiumPreview::RealTimeTranslation: return "translations";
case PremiumPreview::Wallpapers: return "wallpapers";
case PremiumPreview::LastSeen: return "last_seen";
case PremiumPreview::MessagePrivacy: return "message_privacy";
}
return "";
}();

View file

@ -62,6 +62,8 @@ enum class PremiumPreview {
RealTimeTranslation,
Wallpapers,
TagsForMessages,
LastSeen,
MessagePrivacy,
kCount,
};

View file

@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/send_files_box.h"
#include "lang/lang_keys.h"
#include "storage/localimageloader.h"
#include "storage/localstorage.h"
#include "storage/storage_media_prepare.h"
#include "mainwidget.h"
@ -22,8 +21,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_selector.h"
#include "editor/photo_editor_layer_widget.h"
#include "history/history_drag_area.h"
#include "history/view/controls/history_view_characters_limit.h"
#include "history/view/history_view_schedule_box.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
#include "base/event_filter.h"
#include "base/call_delayed.h"
@ -31,13 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_preview_box.h"
#include "ui/effects/scroll_content_shadow.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/chat/attach/attach_album_preview.h"
#include "ui/chat/attach/attach_single_file_preview.h"
#include "ui/chat/attach/attach_single_media_preview.h"
@ -48,17 +42,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lottie/lottie_single_player.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "data/data_peer_values.h" // Data::AmPremiumValue.
#include "data/data_premium_limits.h"
#include "data/stickers/data_stickers.h"
#include "data/stickers/data_custom_emoji.h"
#include "api/api_common.h"
#include "window/window_session_controller.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QMimeData>
@ -339,16 +332,16 @@ SendFilesBox::SendFilesBox(
not_null<Window::SessionController*> controller,
Ui::PreparedList &&list,
const TextWithTags &caption,
SendFilesLimits limits,
SendFilesCheck check,
not_null<PeerData*> toPeer,
Api::SendType sendType,
SendMenu::Type sendMenuType)
: SendFilesBox(nullptr, {
.show = controller->uiShow(),
.list = std::move(list),
.caption = caption,
.limits = limits,
.check = check,
.captionToPeer = toPeer,
.limits = DefaultLimitsForPeer(toPeer),
.check = DefaultCheckForPeer(controller, toPeer),
.sendType = sendType,
.sendMenuType = sendMenuType,
}) {
@ -364,6 +357,7 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
, _list(std::move(descriptor.list))
, _limits(descriptor.limits)
, _sendMenuType(descriptor.sendMenuType)
, _captionToPeer(descriptor.captionToPeer)
, _check(std::move(descriptor.check))
, _confirmedCallback(std::move(descriptor.confirmed))
, _cancelledCallback(std::move(descriptor.cancelled))
@ -1034,8 +1028,10 @@ void SendFilesBox::updateSendWayControls() {
}
void SendFilesBox::setupCaption() {
const auto allow = [=](const auto &) {
return (_limits & SendFilesAllow::EmojiWithoutPremium);
const auto allow = [=](not_null<DocumentData*> emoji) {
return _captionToPeer
? Data::AllowEmojiWithoutPremium(_captionToPeer, emoji)
: (_limits & SendFilesAllow::EmojiWithoutPremium);
};
const auto show = _show;
InitMessageFieldHandlers(
@ -1096,6 +1092,39 @@ void SendFilesBox::setupCaption() {
updateCaptionPlaceholder();
setupEmojiPanel();
rpl::single(rpl::empty_value()) | rpl::then(
_caption->changes()
) | rpl::start_with_next([=] {
checkCharsLimitation();
}, _caption->lifetime());
}
void SendFilesBox::checkCharsLimitation() {
const auto limits = Data::PremiumLimits(&_show->session());
const auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown()
: TextWithTags();
const auto remove = caption.text.size() - limits.captionLengthCurrent();
if ((remove > 0) && _emojiToggle) {
if (!_charsLimitation) {
_charsLimitation = base::make_unique_q<CharactersLimitLabel>(
this,
_emojiToggle.data(),
style::al_top);
_charsLimitation->show();
Data::AmPremiumValue(
&_show->session()
) | rpl::start_with_next([=] {
checkCharsLimitation();
}, _charsLimitation->lifetime());
}
_charsLimitation->setLeft(remove);
} else {
if (_charsLimitation) {
_charsLimitation = nullptr;
}
}
}
void SendFilesBox::setupEmojiPanel() {
@ -1114,7 +1143,6 @@ void SendFilesBox::setupEmojiPanel() {
.level = Window::GifPauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
.features = {
.megagroupSet = false,
.stickersSettings = false,
.openStickerSets = false,
},
@ -1125,6 +1153,7 @@ void SendFilesBox::setupEmojiPanel() {
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_emojiPanel->hide();
_emojiPanel->selector()->setCurrentPeer(_captionToPeer);
_emojiPanel->selector()->setAllowEmojiWithoutPremium(
_limits & SendFilesAllow::EmojiWithoutPremium);
_emojiPanel->selector()->emojiChosen(
@ -1137,7 +1166,11 @@ void SendFilesBox::setupEmojiPanel() {
if (info
&& info->setType == Data::StickersType::Emoji
&& !_show->session().premium()
&& !(_limits & SendFilesAllow::EmojiWithoutPremium)) {
&& !(_captionToPeer
? Data::AllowEmojiWithoutPremium(
_captionToPeer,
data.document)
: (_limits & SendFilesAllow::EmojiWithoutPremium))) {
ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji);
} else {
Data::InsertCustomEmoji(_caption.data(), data.document);

View file

@ -50,6 +50,10 @@ namespace SendMenu {
enum class Type;
} // namespace SendMenu
namespace HistoryView::Controls {
class CharactersLimitLabel;
} // namespace HistoryView::Controls
enum class SendFilesAllow {
OnlyOne = (1 << 0),
Photos = (1 << 1),
@ -88,6 +92,7 @@ struct SendFilesBoxDescriptor {
std::shared_ptr<ChatHelpers::Show> show;
Ui::PreparedList list;
TextWithTags caption;
PeerData *captionToPeer = nullptr;
SendFilesLimits limits = {};
SendFilesCheck check;
Api::SendType sendType = {};
@ -108,8 +113,7 @@ public:
not_null<Window::SessionController*> controller,
Ui::PreparedList &&list,
const TextWithTags &caption,
SendFilesLimits limits,
SendFilesCheck check,
not_null<PeerData*> toPeer,
Api::SendType sendType,
SendMenu::Type sendMenuType);
SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor);
@ -221,6 +225,8 @@ private:
void enqueueNextPrepare();
void addPreparedAsyncFile(Ui::PreparedFile &&file);
void checkCharsLimitation();
const std::shared_ptr<ChatHelpers::Show> _show;
const style::ComposeControls &_st;
const Api::SendType _sendType = Api::SendType();
@ -233,7 +239,7 @@ private:
SendFilesLimits _limits = {};
SendMenu::Type _sendMenuType = {};
PeerData *_captionToPeer = nullptr;
SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback;
Fn<void()> _cancelledCallback;
@ -244,6 +250,8 @@ private:
object_ptr<Ui::EmojiButton> _emojiToggle = { nullptr };
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<QObject> _emojiFilter;
using CharactersLimitLabel = HistoryView::Controls::CharactersLimitLabel;
base::unique_qptr<CharactersLimitLabel> _charsLimitation;
object_ptr<Ui::Checkbox> _groupFiles = { nullptr };
object_ptr<Ui::Checkbox> _sendImagesAsPhotos = { nullptr };

View file

@ -17,7 +17,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "ui/boxes/boost_box.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/peers/edit_peer_color_box.h"
#include "boxes/sticker_set_box.h"
#include "apiwrap.h"
#include "storage/storage_account.h"
@ -38,6 +40,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/unread_badge_paint.h"
#include "media/clip/media_clip_reader.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
@ -99,7 +103,8 @@ public:
Inner(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> megagroup);
not_null<ChannelData*> megagroup,
bool isEmoji);
[[nodiscard]] Main::Session &session() const;
@ -108,7 +113,7 @@ public:
}
void setInnerFocus();
void saveGroupSet();
void saveGroupSet(Fn<void()> done);
void rebuild(bool masks);
void updateSize(int newWidth = 0);
@ -221,6 +226,7 @@ private:
StickersSetsOrder collectSets(Check check) const;
void updateSelected();
void checkGroupLevel(Fn<void()> done);
void checkLoadMore();
void updateScrollbarWidth();
@ -323,6 +329,8 @@ private:
int _scrollbar = 0;
ChannelData *_megagroupSet = nullptr;
bool _megagroupSetEmoji = false;
bool _checkingGroupLevel = false;
StickerSetIdentifier _megagroupSetInput;
std::unique_ptr<Row> _megagroupSelectedSet;
object_ptr<AddressField> _megagroupSetField = { nullptr };
@ -429,15 +437,16 @@ StickersBox::StickersBox(
StickersBox::StickersBox(
QWidget*,
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> megagroup)
not_null<ChannelData*> megagroup,
bool isEmoji)
: _st(st::stickersRowItem)
, _show(std::move(show))
, _session(&_show->session())
, _api(&_session->mtp())
, _section(Section::Installed)
, _isMasks(false)
, _isEmoji(false)
, _installed(0, this, _show, megagroup)
, _isEmoji(isEmoji)
, _installed(0, this, _show, megagroup, isEmoji)
, _megagroupSet(megagroup) {
_installed.widget()->scrollsToY(
) | rpl::start_with_next([=](int y) {
@ -581,7 +590,9 @@ void StickersBox::prepare() {
session().local().readArchivedStickers();
}
} else {
setTitle(tr::lng_stickers_group_set());
setTitle(_isEmoji
? tr::lng_emoji_group_set()
: tr::lng_stickers_group_set());
}
} else if (_section == Section::Archived) {
requestArchivedSets();
@ -659,9 +670,11 @@ void StickersBox::prepare() {
}
if (_megagroupSet) {
addButton(
tr::lng_settings_save(),
[=] { _installed.widget()->saveGroupSet(); closeBox(); });
addButton(tr::lng_settings_save(), [=] {
_installed.widget()->saveGroupSet(crl::guard(this, [=] {
closeBox();
}));
});
addButton(tr::lng_cancel(), [=] { closeBox(); });
} else {
const auto close = _section == Section::Attached;
@ -1220,7 +1233,8 @@ StickersBox::Inner::Inner(
StickersBox::Inner::Inner(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> megagroup)
not_null<ChannelData*> megagroup,
bool isEmoji)
: RpWidget(parent)
, _st(st::stickersRowItem)
, _show(std::move(show))
@ -1248,19 +1262,30 @@ StickersBox::Inner::Inner(
})
, _itemsTop(st::lineWidth)
, _megagroupSet(megagroup)
, _megagroupSetInput(_megagroupSet->mgInfo->stickerSet)
, _megagroupSetEmoji(isEmoji)
, _megagroupSetInput(isEmoji
? _megagroupSet->mgInfo->emojiSet
: _megagroupSet->mgInfo->stickerSet)
, _megagroupSetField(
this,
st::groupStickersField,
rpl::single(u"stickerset"_q),
rpl::single(isEmoji ? u"emojipack"_q : u"stickerset"_q),
QString(),
_session->createInternalLink(QString()))
, _megagroupDivider(this)
, _megagroupSubTitle(this, tr::lng_stickers_group_from_your(tr::now), st::boxTitle) {
, _megagroupSubTitle(
this,
(isEmoji
? tr::lng_emoji_group_from_your
: tr::lng_stickers_group_from_your)(tr::now),
st::boxTitle) {
_megagroupSetField->setLinkPlaceholder(
_session->createInternalLink(u"addstickers/"_q));
_session->createInternalLink(
isEmoji ? u"addemoji/"_q : u"addstickers/"_q));
_megagroupSetField->setPlaceholderHidden(false);
_megagroupSetAddressChangedTimer.setCallback([this] { handleMegagroupSetAddressChange(); });
_megagroupSetAddressChangedTimer.setCallback([this] {
handleMegagroupSetAddressChange();
});
connect(
_megagroupSetField,
&Ui::MaskedInputField::changed,
@ -1689,7 +1714,9 @@ void StickersBox::Inner::paintFakeButton(Painter &p, not_null<Row*> row, int ind
}
void StickersBox::Inner::mousePressEvent(QMouseEvent *e) {
if (_dragging >= 0) mouseReleaseEvent(e);
if (_dragging >= 0) {
mouseReleaseEvent(e);
}
_mouse = e->globalPos();
updateSelected();
@ -1979,18 +2006,64 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
setActionDown(-1);
}
void StickersBox::Inner::saveGroupSet() {
void StickersBox::Inner::saveGroupSet(Fn<void()> done) {
Expects(_megagroupSet != nullptr);
auto oldId = _megagroupSet->mgInfo->stickerSet.id;
auto oldId = _megagroupSetEmoji
? _megagroupSet->mgInfo->emojiSet.id
: _megagroupSet->mgInfo->stickerSet.id;
auto newId = _megagroupSetInput.id;
if (newId != oldId) {
if (newId == oldId) {
done();
} else if (_megagroupSetEmoji) {
checkGroupLevel(done);
} else {
session().api().setGroupStickerSet(_megagroupSet, _megagroupSetInput);
session().data().stickers().notifyStickerSetInstalled(
Data::Stickers::MegagroupSetId);
}
}
void StickersBox::Inner::checkGroupLevel(Fn<void()> done) {
Expects(_megagroupSet != nullptr);
Expects(_megagroupSetEmoji);
const auto peer = _megagroupSet;
const auto save = [=] {
session().api().setGroupEmojiSet(peer, _megagroupSetInput);
session().data().stickers().notifyEmojiSetInstalled(
Data::Stickers::MegagroupSetId);
done();
};
if (!_megagroupSetInput) {
save();
return;
} else if (_checkingGroupLevel) {
return;
}
_checkingGroupLevel = true;
const auto weak = Ui::MakeWeak(this);
CheckBoostLevel(_show, peer, [=](int level) {
if (!weak) {
return std::optional<Ui::AskBoostReason>();
}
_checkingGroupLevel = false;
const auto appConfig = &peer->session().account().appConfig();
const auto required = appConfig->get<int>(
"group_emoji_stickers_level_min",
4);
if (level >= required) {
save();
return std::optional<Ui::AskBoostReason>();
}
return std::make_optional(Ui::AskBoostReason{
Ui::AskBoostEmojiPack{ required }
});
}, [=] { _checkingGroupLevel = false; });
}
void StickersBox::Inner::setRowRemovedBySetId(uint64 setId, bool removed) {
const auto index = getRowIndex(setId);
if (index >= 0) {
@ -2233,9 +2306,13 @@ void StickersBox::Inner::rebuild(bool masks) {
clear();
const auto &order = ([&]() -> const StickersSetsOrder & {
if (_section == Section::Installed) {
auto &result = session().data().stickers().setsOrder();
auto &result = _megagroupSetEmoji
? session().data().stickers().emojiSetsOrder()
: session().data().stickers().setsOrder();
if (_megagroupSet && result.empty()) {
return session().data().stickers().featuredSetsOrder();
return _megagroupSetEmoji
? session().data().stickers().featuredEmojiSetsOrder()
: session().data().stickers().featuredSetsOrder();
}
return result;
} else if (_section == Section::Masks) {
@ -2252,9 +2329,15 @@ void StickersBox::Inner::rebuild(bool masks) {
const auto &sets = session().data().stickers().sets();
if (_megagroupSet) {
auto usingFeatured = session().data().stickers().setsOrder().empty();
auto usingFeatured = _megagroupSetEmoji
? session().data().stickers().emojiSetsOrder().empty()
: session().data().stickers().setsOrder().empty();
_megagroupSubTitle->setText(usingFeatured
? tr::lng_stickers_group_from_featured(tr::now)
? (_megagroupSetEmoji
? tr::lng_stickers_group_from_featured(tr::now)
: tr::lng_emoji_group_from_featured(tr::now))
: _megagroupSetEmoji
? tr::lng_emoji_group_from_your(tr::now)
: tr::lng_stickers_group_from_your(tr::now));
updateControlsGeometry();
} else if (_isInstalledTab) {

View file

@ -66,7 +66,8 @@ public:
StickersBox(
QWidget*,
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> megagroup);
not_null<ChannelData*> megagroup,
bool isEmoji);
StickersBox(
QWidget*,
std::shared_ptr<ChatHelpers::Show> show,

View file

@ -968,6 +968,7 @@ historyComposeField: InputField(defaultInputField) {
duration: 100;
}
historyComposeFieldMaxHeight: 224px;
historyComposeFieldFadeHeight: 6px;
// historyMinHeight: 56px;
historyAttach: IconButton(defaultIconButton) {
@ -1070,6 +1071,13 @@ historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
historyCharsLimitationLabel: FlatLabel(defaultFlatLabel) {
// The same as a width of the historySendSize.
minWidth: 44px;
align: align(center);
textFg: attentionButtonFg;
}
historyRecordVoiceFg: historyComposeIconFg;
historyRecordVoiceFgOver: historyComposeIconFgOver;
historyRecordVoiceFgInactive: attentionButtonFg;

View file

@ -31,6 +31,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "layout/layout_position.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer_values.h"
#include "data/stickers/data_stickers.h"
@ -41,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "emoji_suggestions_data.h"
#include "emoji_suggestions_helper.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "settings/settings_premium.h"
@ -467,6 +470,7 @@ EmojiListWidget::EmojiListWidget(
, _show(std::move(descriptor.show))
, _features(descriptor.features)
, _mode(descriptor.mode)
, _api(&session().mtp())
, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)
, _premiumIcon(_mode == Mode::EmojiStatus
? std::make_unique<GradientPremiumStar>()
@ -523,6 +527,15 @@ EmojiListWidget::EmojiListWidget(
pickerHidden();
}, lifetime());
session().changes().peerUpdates(
Data::PeerUpdate::Flag::EmojiSet
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer.get() == _megagroupSet);
}) | rpl::start_with_next([=] {
refreshCustom();
resizeToWidth(width());
}, lifetime());
session().data().stickers().updated(
Data::StickersType::Emoji
) | rpl::start_with_next([=] {
@ -1693,6 +1706,13 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
}
void EmojiListWidget::displaySet(uint64 setId) {
if (setId == Data::Stickers::MegagroupSetId) {
if (_megagroupSet->mgInfo->emojiSet.id) {
setId = _megagroupSet->mgInfo->emojiSet.id;
} else {
return;
}
}
const auto &sets = session().data().stickers().sets();
auto it = sets.find(setId);
if (it != sets.cend()) {
@ -1700,9 +1720,37 @@ void EmojiListWidget::displaySet(uint64 setId) {
}
}
void EmojiListWidget::removeMegagroupSet(bool locally) {
if (locally) {
session().settings().setGroupEmojiSectionHidden(_megagroupSet->id);
session().saveSettings();
refreshCustom();
return;
}
checkHideWithBox(Ui::MakeConfirmBox({
.text = tr::lng_emoji_remove_group_set(),
.confirmed = crl::guard(this, [this, group = _megagroupSet](
Fn<void()> &&close) {
Expects(group->mgInfo != nullptr);
if (group->mgInfo->emojiSet) {
session().api().setGroupEmojiSet(group, {});
}
close();
}),
.cancelled = [](Fn<void()> &&close) { close(); },
.labelStyle = &st().boxLabel,
}));
}
void EmojiListWidget::removeSet(uint64 setId) {
const auto &labelSt = st().boxLabel;
if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) {
if (setId == Data::Stickers::MegagroupSetId) {
const auto i = ranges::find(_custom, setId, &CustomSet::id);
Assert(i != end(_custom));
const auto removeLocally = !_megagroupSet->canEditEmoji();
removeMegagroupSet(removeLocally);
} else if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) {
checkHideWithBox(std::move(box));
}
}
@ -1799,6 +1847,13 @@ bool EmojiListWidget::hasRemoveButton(int index) const {
return false;
}
const auto &set = _custom[index - _staticCount];
if (set.id == Data::Stickers::MegagroupSetId) {
Assert(_megagroupSet != nullptr);
if (index + 1 != _staticCount + _custom.size()) {
return true;
}
return !set.list.empty() && _megagroupSet->canEditEmoji();
}
return set.canRemove && !set.premiumRequired;
}
@ -1825,7 +1880,9 @@ bool EmojiListWidget::hasAddButton(int index) const {
return false;
}
const auto &set = _custom[index - _staticCount];
return !set.canRemove && !set.premiumRequired;
return !set.canRemove
&& !set.premiumRequired
&& set.id != Data::Stickers::MegagroupSetId;
}
QRect EmojiListWidget::addButtonRect(int index) const {
@ -1849,10 +1906,13 @@ QRect EmojiListWidget::unlockButtonRect(int index) const {
}
bool EmojiListWidget::hasButton(int index) const {
if (hasColorButton(index)
|| (index >= _staticCount
&& index < _staticCount + _custom.size())) {
if (hasColorButton(index)) {
return true;
} else if (index >= _staticCount
&& index < _staticCount + _custom.size()) {
const auto &custom = _custom[index - _staticCount];
return (custom.id != Data::Stickers::MegagroupSetId)
|| custom.canRemove;
}
return false;
}
@ -1982,6 +2042,16 @@ void EmojiListWidget::setAllowWithoutPremium(bool allow) {
resizeToWidth(width());
}
void EmojiListWidget::showMegagroupSet(ChannelData *megagroup) {
Expects(!megagroup || megagroup->isMegagroup());
if (_megagroupSet != megagroup) {
_megagroupSet = megagroup;
refreshCustom();
resizeToWidth(width());
}
}
QString EmojiListWidget::tooltipText() const {
if (_mode != Mode::Full) {
return {};
@ -2052,7 +2122,25 @@ void EmojiListWidget::refreshCustom() {
const auto owner = &session->data();
const auto &sets = owner->stickers().sets();
const auto push = [&](uint64 setId, bool installed) {
auto it = sets.find(setId);
const auto megagroup = _megagroupSet
&& (setId == Data::Stickers::MegagroupSetId);
const auto lookupId = megagroup
? _megagroupSet->mgInfo->emojiSet.id
: setId;
if (!lookupId) {
return;
} else if (!megagroup
&& !_custom.empty()
&& _custom.front().id == Data::Stickers::MegagroupSetId
&& _megagroupSet->mgInfo->emojiSet.id == setId) {
// Skip the set that is already added as a megagroup set.
return;
} else if (megagroup
&& ranges::contains(_custom, lookupId, &CustomSet::id)) {
// Skip the set that is already added as a custom set.
return;
}
auto it = sets.find(lookupId);
if (it == sets.cend()
|| it->second->stickers.isEmpty()
|| (_mode == Mode::BackgroundEmoji && !it->second->textColor())
@ -2060,12 +2148,13 @@ void EmojiListWidget::refreshCustom() {
&& !it->second->channelStatus())) {
return;
}
const auto canRemove = !!(it->second->flags
& Data::StickersSetFlag::Installed);
const auto canRemove = megagroup
? (_megagroupSet->canEditEmoji() || installed)
: !!(it->second->flags & Data::StickersSetFlag::Installed);
const auto sortAsInstalled = canRemove
&& (!(it->second->flags & Data::StickersSetFlag::Featured)
|| !_localSetsManager->isInstalledLocally(setId));
if (sortAsInstalled != installed) {
|| !_localSetsManager->isInstalledLocally(lookupId));
if (!megagroup && sortAsInstalled != installed) {
return;
}
auto premium = false;
@ -2078,7 +2167,7 @@ void EmojiListWidget::refreshCustom() {
return false;
}
for (auto k = 0; k != count; ++k) {
if (!premium && list[k]->isPremiumEmoji()) {
if (!premium && !megagroup && list[k]->isPremiumEmoji()) {
premium = true;
}
if (i->list[k].document != list[k]) {
@ -2112,11 +2201,11 @@ void EmojiListWidget::refreshCustom() {
continue;
} else if (const auto sticker = document->sticker()) {
set.push_back({
.custom = resolveCustomEmoji(document, setId),
.custom = resolveCustomEmoji(document, lookupId),
.document = document,
.emoji = Ui::Emoji::Find(sticker->alt),
});
if (!premium && document->isPremiumEmoji()) {
if (!premium && !megagroup && document->isPremiumEmoji()) {
premium = true;
}
}
@ -2134,12 +2223,14 @@ void EmojiListWidget::refreshCustom() {
.premiumRequired = premium && premiumMayBeBought,
});
};
refreshMegagroupStickers(push, GroupStickersPlace::Visible);
for (const auto setId : owner->stickers().emojiSetsOrder()) {
push(setId, true);
}
for (const auto setId : owner->stickers().featuredEmojiSetsOrder()) {
push(setId, false);
}
refreshMegagroupStickers(push, GroupStickersPlace::Hidden);
_footer->refreshIcons(
fillIcons(),
@ -2232,6 +2323,60 @@ not_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomRecent(
).first->second.emoji.get();
}
void EmojiListWidget::refreshMegagroupStickers(
Fn<void(uint64 setId, bool installed)> push,
GroupStickersPlace place) {
if (!_features.megagroupSet
|| !_megagroupSet
|| !_megagroupSet->mgInfo->emojiSet) {
return;
}
auto canEdit = _megagroupSet->canEditEmoji();
auto isShownHere = [place](bool hidden) {
return (hidden == (place == GroupStickersPlace::Hidden));
};
auto hidden = session().settings().isGroupEmojiSectionHidden(_megagroupSet->id);
auto removeHiddenForGroup = [this, &hidden] {
if (hidden) {
session().settings().removeGroupEmojiSectionHidden(_megagroupSet->id);
session().saveSettings();
hidden = false;
}
};
if (canEdit && hidden) {
removeHiddenForGroup();
}
const auto &set = _megagroupSet->mgInfo->emojiSet;
if (!set.id || !isShownHere(hidden)) {
return;
}
push(Data::Stickers::MegagroupSetId, !hidden);
if (!_custom.empty()
&& _custom.back().id == Data::Stickers::MegagroupSetId) {
return;
} else if (_megagroupSetIdRequested == set.id) {
return;
}
_megagroupSetIdRequested = set.id;
_api.request(MTPmessages_GetStickerSet(
Data::InputStickerSet(set),
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &data) {
if (const auto set = session().data().stickers().feedSetFull(data)) {
refreshCustom();
if (set->id == _megagroupSetIdRequested) {
_megagroupSetIdRequested = 0;
} else {
LOG(("API Error: Got different set."));
}
}
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).send();
}
std::vector<StickerIcon> EmojiListWidget::fillIcons() {
auto result = std::vector<StickerIcon>();
result.reserve(2 + _custom.size());
@ -2248,6 +2393,11 @@ std::vector<StickerIcon> EmojiListWidget::fillIcons() {
}
const auto esize = StickersListFooter::IconFrameSize();
for (const auto &custom : _custom) {
if (custom.id == Data::Stickers::MegagroupSetId) {
result.emplace_back(Data::Stickers::MegagroupSetId);
result.back().megagroup = _megagroupSet;
continue;
}
const auto set = custom.set;
result.emplace_back(set, custom.thumbnailDocument, esize, esize);
}

View file

@ -117,6 +117,7 @@ public:
void showSet(uint64 setId);
[[nodiscard]] uint64 currentSet(int yOffset) const;
void setAllowWithoutPremium(bool allow);
void showMegagroupSet(ChannelData *megagroup);
// Ui::AbstractTooltipShower interface.
QString tooltipText() const override;
@ -257,6 +258,13 @@ private:
void colorChosen(EmojiChosen data);
bool checkPickerHide();
void refreshCustom();
enum class GroupStickersPlace {
Visible,
Hidden,
};
void refreshMegagroupStickers(
Fn<void(uint64 setId, bool installed)> push,
GroupStickersPlace place);
void unloadNotSeenCustom(int visibleTop, int visibleBottom);
void unloadAllCustom();
void unloadCustomIn(const SectionInfo &info);
@ -340,6 +348,7 @@ private:
void displaySet(uint64 setId);
void removeSet(uint64 setId);
void removeMegagroupSet(bool locally);
void initButton(RightButton &button, const QString &text, bool gradient);
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
@ -370,10 +379,13 @@ private:
const ComposeFeatures _features;
Mode _mode = Mode::Full;
std::unique_ptr<Ui::TabbedSearch> _search;
MTP::Sender _api;
const int _staticCount = 0;
StickersListFooter *_footer = nullptr;
std::unique_ptr<GradientPremiumStar> _premiumIcon;
std::unique_ptr<LocalStickersManager> _localSetsManager;
ChannelData *_megagroupSet = nullptr;
uint64 _megagroupSetIdRequested = 0;
Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId,
Fn<void()>)> _customRecentFactory;

View file

@ -10,11 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_widget.h"
#include "history/history.h" // History::session
#include "history/history_item.h" // HistoryItem::originalText
#include "history/history_item_helpers.h" // DropCustomEmoji
#include "history/history_item_helpers.h" // DropDisallowedCustomEmoji
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "base/event_filter.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "core/shortcuts.h"
#include "core/application.h"
#include "core/core_settings.h"
@ -277,11 +278,9 @@ TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
auto original = item->history()->session().supportMode()
? StripSupportHashtag(item->originalText())
: item->originalText();
const auto dropCustomEmoji = !item->history()->session().premium()
&& !item->history()->peer->isSelf();
if (dropCustomEmoji) {
original = DropCustomEmoji(std::move(original));
}
original = DropDisallowedCustomEmoji(
item->history()->peer,
std::move(original));
return TextWithTags{
original.text,
TextUtilities::ConvertEntitiesToTextTags(original.entities)
@ -442,6 +441,73 @@ bool HasSendText(not_null<const Ui::InputField*> field) {
return false;
}
void InitMessageFieldFade(
not_null<Ui::InputField*> field,
const style::color &bg) {
class Fade final : public Ui::RpWidget {
public:
using Ui::RpWidget::RpWidget;
void setFade(QPixmap &&fade) {
_fade = std::move(fade);
}
int resizeGetHeight(int newWidth) override {
return st::historyComposeFieldFadeHeight;
}
private:
void paintEvent(QPaintEvent *event) override {
auto p = QPainter(this);
p.drawTiledPixmap(rect(), _fade);
}
QPixmap _fade;
};
const auto topFade = Ui::CreateChild<Fade>(field.get());
const auto bottomFade = Ui::CreateChild<Fade>(field.get());
const auto generateFade = [=] {
const auto size = QSize(1, st::historyComposeFieldFadeHeight);
auto fade = QPixmap(size * style::DevicePixelRatio());
fade.setDevicePixelRatio(style::DevicePixelRatio());
fade.fill(Qt::transparent);
{
auto p = QPainter(&fade);
auto gradient = QLinearGradient(0, 1, 0, size.height());
gradient.setStops({ { 0., bg->c }, { .9, Qt::transparent } });
p.setPen(Qt::NoPen);
p.setBrush(gradient);
p.drawRect(Rect(size));
}
bottomFade->setFade(fade.transformed(QTransform().scale(1, -1)));
topFade->setFade(std::move(fade));
};
generateFade();
style::PaletteChanged(
) | rpl::start_with_next([=] {
generateFade();
}, topFade->lifetime());
field->sizeValue(
) | rpl::start_with_next_done([=](const QSize &size) {
topFade->resizeToWidth(size.width());
bottomFade->resizeToWidth(size.width());
bottomFade->move(
0,
size.height() - st::historyComposeFieldFadeHeight);
}, [t = Ui::MakeWeak(topFade), b = Ui::MakeWeak(bottomFade)] {
Ui::DestroyChild(t.data());
Ui::DestroyChild(b.data());
}, topFade->lifetime());
topFade->show();
bottomFade->show();
}
InlineBotQuery ParseInlineBotQuery(
not_null<Main::Session*> session,
not_null<const Ui::InputField*> field) {

View file

@ -79,6 +79,10 @@ void InitSpellchecker(
bool HasSendText(not_null<const Ui::InputField*> field);
void InitMessageFieldFade(
not_null<Ui::InputField*> field,
const style::color &bg);
struct InlineBotQuery {
QString query;
QString username;

View file

@ -1790,7 +1790,8 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
removeSet(sets[button->section].id);
}
} else if (std::get_if<OverGroupAdd>(&pressed)) {
_show->showBox(Box<StickersBox>(_show, _megagroupSet));
const auto isEmoji = false;
_show->showBox(Box<StickersBox>(_show, _megagroupSet, isEmoji));
}
}
}
@ -2642,7 +2643,8 @@ void StickersListWidget::setupSearch() {
void StickersListWidget::displaySet(uint64 setId) {
if (setId == Data::Stickers::MegagroupSetId) {
if (_megagroupSet->canEditStickers()) {
checkHideWithBox(Box<StickersBox>(_show, _megagroupSet));
const auto isEmoji = false;
checkHideWithBox(Box<StickersBox>(_show, _megagroupSet, isEmoji));
return;
} else if (_megagroupSet->mgInfo->stickerSet.id) {
setId = _megagroupSet->mgInfo->stickerSet.id;

View file

@ -966,6 +966,9 @@ void TabbedSelector::setCurrentPeer(PeerData *peer) {
}
_currentPeer = peer;
checkRestrictedPeer();
if (hasEmojiTab()) {
emoji()->showMegagroupSet(peer ? peer->asMegagroup() : nullptr);
}
if (hasStickersTab()) {
stickers()->showMegagroupSet(peer ? peer->asMegagroup() : nullptr);
}

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QSessionManager>
#include <QtGui/QScreen>
#include <QtGui/qpa/qplatformscreen.h>
#include <ksandbox.h>
namespace Core {
namespace {
@ -517,8 +518,10 @@ void Sandbox::refreshGlobalProxy() {
|| proxy.type == MTP::ProxyData::Type::Http) {
QNetworkProxy::setApplicationProxy(
MTP::ToNetworkProxy(MTP::ToDirectIpProxy(proxy)));
} else if (!Core::IsAppLaunched()
|| Core::App().settings().proxy().isSystem()) {
} else if ((!Core::IsAppLaunched()
|| Core::App().settings().proxy().isSystem())
// this works stable only in sandboxed environment where it works through portal
&& (!Platform::IsLinux() || KSandbox::isInside() || cDebugMode())) {
QNetworkProxyFactory::setUseSystemConfiguration(true);
} else {
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);

View file

@ -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 = 4014013;
constexpr auto AppVersionStr = "4.14.13";
constexpr auto AppVersion = 4015000;
constexpr auto AppVersionStr = "4.15";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct BoostsOverview final {
bool group = false;
int mine = 0;
int level = 0;
int boostCount = 0;

View file

@ -101,13 +101,14 @@ struct PeerUpdate {
// For channels
ChannelAmIn = (1ULL << 36),
StickersSet = (1ULL << 37),
ChannelLinkedChat = (1ULL << 38),
ChannelLocation = (1ULL << 39),
Slowmode = (1ULL << 40),
GroupCall = (1ULL << 41),
EmojiSet = (1ULL << 38),
ChannelLinkedChat = (1ULL << 39),
ChannelLocation = (1ULL << 40),
Slowmode = (1ULL << 41),
GroupCall = (1ULL << 42),
// For iteration
LastUsedBit = (1ULL << 41),
LastUsedBit = (1ULL << 42),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -556,25 +556,16 @@ bool ChannelData::canDeleteMessages() const {
}
bool ChannelData::canPostStories() const {
if (!isBroadcast()) {
return false;
}
return amCreator()
|| (adminRights() & AdminRight::PostStories);
}
bool ChannelData::canEditStories() const {
if (!isBroadcast()) {
return false;
}
return amCreator()
|| (adminRights() & AdminRight::EditStories);
}
bool ChannelData::canDeleteStories() const {
if (!isBroadcast()) {
return false;
}
return amCreator()
|| (adminRights() & AdminRight::DeleteStories);
}
@ -648,6 +639,10 @@ bool ChannelData::canEditStickers() const {
return (flags() & Flag::CanSetStickers);
}
bool ChannelData::canEditEmoji() const {
return amCreator() || (adminRights() & ChatAdminRight::ChangeInfo);
}
bool ChannelData::canDelete() const {
constexpr auto kDeleteChannelMembersLimit = 1000;
return amCreator()
@ -772,34 +767,93 @@ void ChannelData::setMigrateFromChat(ChatData *chat) {
}
int ChannelData::slowmodeSeconds() const {
return _slowmodeSeconds;
if (const auto info = mgInfo.get()) {
return info->slowmodeSeconds;
}
return 0;
}
void ChannelData::setSlowmodeSeconds(int seconds) {
if (_slowmodeSeconds == seconds) {
if (!mgInfo || slowmodeSeconds() == seconds) {
return;
}
_slowmodeSeconds = seconds;
mgInfo->slowmodeSeconds = seconds;
session().changes().peerUpdated(this, UpdateFlag::Slowmode);
}
TimeId ChannelData::slowmodeLastMessage() const {
return (hasAdminRights() || amCreator()) ? 0 : _slowmodeLastMessage;
return (hasAdminRights()
|| amCreator()
|| unrestrictedByBoosts()
|| !mgInfo)
? 0
: mgInfo->slowmodeLastMessage;
}
void ChannelData::growSlowmodeLastMessage(TimeId when) {
const auto info = mgInfo.get();
const auto now = base::unixtime::now();
accumulate_min(when, now);
if (_slowmodeLastMessage > now) {
_slowmodeLastMessage = when;
} else if (_slowmodeLastMessage >= when) {
if (!info) {
return;
} else if (info->slowmodeLastMessage > now) {
info->slowmodeLastMessage = when;
} else if (info->slowmodeLastMessage >= when) {
return;
} else {
_slowmodeLastMessage = when;
info->slowmodeLastMessage = when;
}
session().changes().peerUpdated(this, UpdateFlag::Slowmode);
}
int ChannelData::boostsApplied() const {
if (const auto info = mgInfo.get()) {
return info->boostsApplied;
}
return 0;
}
int ChannelData::boostsUnrestrict() const {
if (const auto info = mgInfo.get()) {
return info->boostsUnrestrict;
}
return 0;
}
bool ChannelData::unrestrictedByBoosts() const {
if (const auto info = mgInfo.get()) {
return (info->boostsUnrestrict > 0)
&& (info->boostsApplied >= info->boostsUnrestrict);
}
return 0;
}
rpl::producer<bool> ChannelData::unrestrictedByBoostsValue() const {
return mgInfo
? mgInfo->unrestrictedByBoostsChanges.events_starting_with(
unrestrictedByBoosts())
: (rpl::single(false) | rpl::type_erased());
}
void ChannelData::setBoostsUnrestrict(int applied, int unrestrict) {
if (const auto info = mgInfo.get()) {
if (info->boostsApplied == applied
&& info->boostsUnrestrict == unrestrict) {
return;
}
const auto wasUnrestricted = unrestrictedByBoosts();
info->boostsApplied = applied;
info->boostsUnrestrict = unrestrict;
const auto nowUnrestricted = unrestrictedByBoosts();
if (wasUnrestricted != nowUnrestricted) {
info->unrestrictedByBoostsChanges.fire_copy(nowUnrestricted);
session().changes().peerUpdated(
this,
UpdateFlag::Rights | UpdateFlag::Slowmode);
}
}
}
void ChannelData::setInvitePeek(const QString &hash, TimeId expires) {
if (!_invitePeek) {
_invitePeek = std::make_unique<InvitePeek>();
@ -1104,20 +1158,37 @@ void ApplyChannelUpdate(
channel->owner().botCommandsChanged(channel);
}
const auto stickerSet = update.vstickerset();
const auto set = stickerSet ? &stickerSet->c_stickerSet() : nullptr;
const auto newSetId = (set ? set->vid().v : 0);
const auto oldSetId = channel->mgInfo->stickerSet.id;
const auto sset = stickerSet ? &stickerSet->c_stickerSet() : nullptr;
const auto newStickerSetId = (sset ? sset->vid().v : 0);
const auto oldStickerSetId = channel->mgInfo->stickerSet.id;
const auto stickersChanged = (canEditStickers != channel->canEditStickers())
|| (oldSetId != newSetId);
if (oldSetId != newSetId) {
|| (oldStickerSetId != newStickerSetId);
if (oldStickerSetId != newStickerSetId) {
channel->mgInfo->stickerSet = StickerSetIdentifier{
.id = set ? set->vid().v : 0,
.accessHash = set ? set->vaccess_hash().v : 0,
.id = sset ? sset->vid().v : 0,
.accessHash = sset ? sset->vaccess_hash().v : 0,
};
}
if (stickersChanged) {
session->changes().peerUpdated(channel, UpdateFlag::StickersSet);
}
const auto emojiSet = update.vemojiset();
const auto eset = emojiSet ? &emojiSet->c_stickerSet() : nullptr;
const auto newEmojiSetId = (eset ? eset->vid().v : 0);
const auto oldEmojiSetId = channel->mgInfo->emojiSet.id;
const auto emojiChanged = (oldEmojiSetId != newEmojiSetId);
if (oldEmojiSetId != newEmojiSetId) {
channel->mgInfo->emojiSet = StickerSetIdentifier{
.id = eset ? eset->vid().v : 0,
.accessHash = eset ? eset->vaccess_hash().v : 0,
};
}
if (emojiChanged) {
session->changes().peerUpdated(channel, UpdateFlag::EmojiSet);
}
channel->setBoostsUnrestrict(
update.vboosts_applied().value_or_empty(),
update.vboosts_unrestrict().value_or_empty());
}
channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
channel->setTranslationDisabled(update.is_translations_disabled());

View file

@ -114,6 +114,7 @@ public:
base::flat_map<not_null<UserData*>, Restricted> lastRestricted;
base::flat_set<not_null<PeerData*>> markupSenders;
base::flat_set<not_null<UserData*>> bots;
rpl::event_stream<bool> unrestrictedByBoostsChanges;
// For admin badges, full admins list with ranks.
base::flat_map<UserId, QString> admins;
@ -124,6 +125,7 @@ public:
bool joinedMessageFound = false;
bool adminsLoaded = false;
StickerSetIdentifier stickerSet;
StickerSetIdentifier emojiSet;
enum LastParticipantsStatus {
LastParticipantsUpToDate = 0x00,
@ -132,6 +134,11 @@ public:
};
mutable int lastParticipantsStatus = LastParticipantsUpToDate;
int lastParticipantsCount = 0;
int boostsApplied = 0;
int boostsUnrestrict = 0;
int slowmodeSeconds = 0;
TimeId slowmodeLastMessage = 0;
private:
ChatData *_migratedFrom = nullptr;
@ -354,6 +361,7 @@ public:
[[nodiscard]] bool canViewBanned() const;
[[nodiscard]] bool canEditSignatures() const;
[[nodiscard]] bool canEditStickers() const;
[[nodiscard]] bool canEditEmoji() const;
[[nodiscard]] bool canDelete() const;
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictParticipant(
@ -432,6 +440,12 @@ public:
[[nodiscard]] TimeId slowmodeLastMessage() const;
void growSlowmodeLastMessage(TimeId when);
[[nodiscard]] int boostsApplied() const;
[[nodiscard]] int boostsUnrestrict() const;
[[nodiscard]] bool unrestrictedByBoosts() const;
[[nodiscard]] rpl::producer<bool> unrestrictedByBoostsValue() const;
void setBoostsUnrestrict(int applied, int unrestrict);
void setInvitePeek(const QString &hash, TimeId expires);
void clearInvitePeek();
[[nodiscard]] TimeId invitePeekExpires() const;
@ -519,9 +533,6 @@ private:
std::unique_ptr<Data::GroupCall> _call;
PeerId _callDefaultJoinAs = 0;
int _slowmodeSeconds = 0;
TimeId _slowmodeLastMessage = 0;
};
namespace Data {

View file

@ -44,11 +44,9 @@ MTPInputReplyTo ReplyToForMTP(
const auto owner = &history->owner();
if (replyTo.storyId) {
if (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) {
if (const auto user = peer->asUser()) {
return MTP_inputReplyToStory(
user->inputUser,
MTP_int(replyTo.storyId.story));
}
return MTP_inputReplyToStory(
peer->input,
MTP_int(replyTo.storyId.story));
}
} else if (replyTo.messageId || replyTo.topicRootId) {
const auto to = LookupReplyTo(history, replyTo.messageId);

View file

@ -302,20 +302,27 @@ bool UpdateExtendedMedia(
});
}
} // namespace
TextForMimeData WithCaptionClipboardText(
const QString &attachType,
TextForMimeData &&caption) {
auto result = TextForMimeData();
result.reserve(5 + attachType.size() + caption.expanded.size());
result.append(u"[ "_q).append(attachType).append(u" ]"_q);
if (!caption.empty()) {
result.append('\n').append(std::move(caption));
if (attachType.isEmpty()) {
result.reserve(1 + caption.expanded.size());
if (!caption.empty()) {
result.append(std::move(caption));
}
} else {
result.reserve(5 + attachType.size() + caption.expanded.size());
result.append(u"[ "_q).append(attachType).append(u" ]"_q);
if (!caption.empty()) {
result.append('\n').append(std::move(caption));
}
}
return result;
}
} // namespace
Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaInvoice &data) {
@ -767,9 +774,7 @@ QString MediaPhoto::pinnedTextSubstring() const {
}
TextForMimeData MediaPhoto::clipboardText() const {
return WithCaptionClipboardText(
tr::lng_in_dlg_photo(tr::now),
parent()->clipboardText());
return TextForMimeData();
}
bool MediaPhoto::allowsEditCaption() const {
@ -1072,42 +1077,9 @@ QString MediaFile::pinnedTextSubstring() const {
}
TextForMimeData MediaFile::clipboardText() const {
const auto attachType = [&] {
const auto name = Ui::Text::FormatSongNameFor(_document).string();
const auto addName = !name.isEmpty()
? u" : "_q + name
: QString();
if (const auto sticker = _document->sticker()) {
if (!_emoji.isEmpty()) {
return tr::lng_in_dlg_sticker_emoji(
tr::now,
lt_emoji,
_emoji);
}
return tr::lng_in_dlg_sticker(tr::now);
} else if (_document->isAnimation()) {
if (_document->isVideoMessage()) {
const auto media = parent()->media();
return (media && media->ttlSeconds())
? tr::lng_in_dlg_video_message_ttl(tr::now)
: tr::lng_in_dlg_video_message(tr::now);
}
return u"GIF"_q;
} else if (_document->isVideoFile()) {
return tr::lng_in_dlg_video(tr::now);
} else if (_document->isVoiceMessage()) {
const auto media = parent()->media();
return ((media && media->ttlSeconds())
? tr::lng_in_dlg_voice_message_ttl
: tr::lng_in_dlg_audio)(tr::now) + addName;;
} else if (_document->isSong()) {
return tr::lng_in_dlg_audio_file(tr::now) + addName;
}
return tr::lng_in_dlg_file(tr::now) + addName;
}();
auto caption = parent()->clipboardText();
if (_document->isVoiceMessage()) {
if (_document->isVoiceMessage() || _document->isVideoMessage()) {
const auto &entry = _document->session().api().transcribes().entry(
parent());
if (!entry.requestId
@ -1115,17 +1087,18 @@ TextForMimeData MediaFile::clipboardText() const {
&& !entry.toolong
&& !entry.failed
&& (entry.pending || !entry.result.isEmpty())) {
const auto text = "{{\n"
const auto hasCaption = !caption.rich.text.isEmpty();
const auto text = (hasCaption ? "{{\n" : "")
+ entry.result
+ (entry.result.isEmpty() ? "" : " ")
+ (entry.pending ? "[...]" : "")
+ "\n}}"
+ (caption.rich.text.isEmpty() ? "" : "\n");
caption = TextForMimeData{ text, { text } }.append(std::move(caption));
+ (hasCaption ? "\n}}\n" : "");
caption = TextForMimeData{ text, { text } }.append(
std::move(caption));
}
}
return WithCaptionClipboardText(attachType, std::move(caption));
return caption;
}
bool MediaFile::allowsEditCaption() const {

View file

@ -709,10 +709,6 @@ private:
};
[[nodiscard]] TextForMimeData WithCaptionClipboardText(
const QString &attachType,
TextForMimeData &&caption);
[[nodiscard]] Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaInvoice &data);

View file

@ -164,7 +164,7 @@ struct FullReplyTo {
int quoteOffset = 0;
[[nodiscard]] bool valid() const {
return messageId || (storyId && peerIsUser(storyId.peer));
return messageId || (storyId && storyId.peer);
}
explicit operator bool() const {
return valid();

View file

@ -1122,7 +1122,8 @@ Data::RestrictionCheckResult PeerData::amRestricted(
: ChatRestrictions(0));
return (channel->amCreator() || allowByAdminRights(right, channel))
? Result::Allowed()
: (defaultRestrictions & right)
: ((defaultRestrictions & right)
&& !channel->unrestrictedByBoosts())
? Result::WithEveryone()
: (channel->restrictions() & right)
? Result::Explicit()

View file

@ -271,11 +271,13 @@ inline auto DefaultRestrictionValue(
AdminRightValue(
channel,
ChatAdminRight::PostMessages),
channel->unrestrictedByBoostsValue(),
RestrictionsValue(channel, rights),
DefaultRestrictionsValue(channel, rights),
[=](
ChannelDataFlags flags,
bool postMessagesRight,
bool unrestrictedByBoosts,
ChatRestrictions sendRestriction,
ChatRestrictions defaultSendRestriction) {
const auto notAmInFlags = Flag::Left | Flag::Forbidden;
@ -285,7 +287,7 @@ inline auto DefaultRestrictionValue(
|| ((flags & Flag::HasLink)
&& !(flags & Flag::JoinToWrite));
const auto restricted = sendRestriction
| defaultSendRestriction;
| (defaultSendRestriction && !unrestrictedByBoosts);
return allowed
&& !forumRestriction
&& (postMessagesRight

View file

@ -67,6 +67,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTP_flags(data.vflags().v | MTPDmessage::Flag::f_from_scheduled),
data.vid(),
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
MTPint(), // from_boosts_applied
data.vpeer_id(),
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
@ -216,6 +217,7 @@ void ScheduledMessages::sendNowSimpleMessage(
MTP_flags(flags),
update.vid(),
peerToMTP(local->from()->id),
MTPint(), // from_boosts_applied
peerToMTP(history->peer->id),
MTPPeer(), // saved_peer_id
MTPMessageFwdHeader(),

View file

@ -245,6 +245,12 @@ Session::Session(not_null<Main::Session*> session)
, _bigFileCache(Core::App().databases().get(
_session->local().cacheBigFilePath(),
_session->local().cacheBigFileSettings()))
, _groupFreeTranscribeLevel(session->account().appConfig().value(
) | rpl::map([=] {
return session->account().appConfig().get<int>(
u"group_transcribe_level_min"_q,
6);
}))
, _chatsList(
session,
FilterId(),
@ -2189,8 +2195,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
// We always use premium limit in the MainList limit producer,
// because it slices the list to that limit. We don't want to slice
// premium-ly added chats from the pinned list because of sync issues.
return rpl::single(rpl::empty_value()) | rpl::then(
_session->account().appConfig().refreshed()
return _session->account().appConfig().value(
) | rpl::map([=] {
const auto limits = Data::PremiumLimits(_session);
return folder
@ -2205,8 +2210,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
// We always use premium limit in the MainList limit producer,
// because it slices the list to that limit. We don't want to slice
// premium-ly added chats from the pinned list because of sync issues.
return rpl::single(rpl::empty_value()) | rpl::then(
_session->account().appConfig().refreshed()
return _session->account().appConfig().value(
) | rpl::map([=] {
const auto limits = Data::PremiumLimits(_session);
return limits.dialogFiltersChatsPremium();
@ -2215,8 +2219,7 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
rpl::producer<int> Session::maxPinnedChatsLimitValue(
not_null<Data::Forum*> forum) const {
return rpl::single(rpl::empty_value()) | rpl::then(
_session->account().appConfig().refreshed()
return _session->account().appConfig().value(
) | rpl::map([=] {
const auto limits = Data::PremiumLimits(_session);
return limits.topicsPinnedCurrent();
@ -2229,14 +2232,17 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
// We always use premium limit in the MainList limit producer,
// because it slices the list to that limit. We don't want to slice
// premium-ly added chats from the pinned list because of sync issues.
return rpl::single(rpl::empty_value()) | rpl::then(
_session->account().appConfig().refreshed()
return _session->account().appConfig().value(
) | rpl::map([=] {
const auto limits = Data::PremiumLimits(_session);
return limits.savedSublistsPinnedPremium();
});
}
int Session::groupFreeTranscribeLevel() const {
return _groupFreeTranscribeLevel.current();
}
const std::vector<Dialogs::Key> &Session::pinnedChatsOrder(
Data::Folder *folder) const {
return chatsList(folder)->pinned()->order();
@ -4518,6 +4524,7 @@ void Session::insertCheckedServiceNotification(
MTP_flags(flags),
MTP_int(0), // Not used (would've been trimmed to 32 bits).
peerToMTP(PeerData::kServiceNotificationsId),
MTPint(), // from_boosts_applied
peerToMTP(PeerData::kServiceNotificationsId),
MTPPeer(), // saved_peer_id
MTPMessageFwdHeader(),

View file

@ -367,6 +367,7 @@ public:
not_null<Forum*> forum) const;
[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(
not_null<SavedMessages*> saved) const;
[[nodiscard]] int groupFreeTranscribeLevel() const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
Folder *folder) const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
@ -887,6 +888,7 @@ private:
QPointer<Ui::BoxContent> _exportSuggestion;
rpl::variable<bool> _contactsLoaded = false;
rpl::variable<int> _groupFreeTranscribeLevel;
rpl::event_stream<Folder*> _chatsListLoadedEvents;
rpl::event_stream<Folder*> _chatsListChanged;
rpl::event_stream<not_null<UserData*>> _userIsBotChanges;

View file

@ -164,6 +164,15 @@ using UpdateFlag = StoryUpdate::Flag;
return false;
}
[[nodiscard]] PeerData *FromPeer(
not_null<Session*> owner,
const MTPDstoryItem &data) {
if (const auto from = data.vfrom_id()) {
return owner->peer(peerFromMTP(*from));
}
return nullptr;
}
} // namespace
class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask {
@ -278,6 +287,7 @@ Story::Story(
, _repostSourcePeer(RepostSourcePeer(&peer->owner(), data))
, _repostSourceName(RepostSourceName(data))
, _repostSourceId(RepostSourceId(data))
, _fromPeer(FromPeer(&peer->owner(), data))
, _date(data.vdate().v)
, _expires(data.vexpire_date().v)
, _repostModified(RepostModified(data)) {
@ -890,6 +900,10 @@ StoryId Story::repostSourceId() const {
return _repostSourceId;
}
PeerData *Story::fromPeer() const {
return _fromPeer;
}
StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)
: _story(story)
, _done(std::move(done)) {

View file

@ -208,6 +208,8 @@ public:
[[nodiscard]] QString repostSourceName() const;
[[nodiscard]] StoryId repostSourceId() const;
[[nodiscard]] PeerData *fromPeer() const;
private:
struct ViewsCounts {
int views = 0;
@ -234,6 +236,7 @@ private:
PeerData * const _repostSourcePeer = nullptr;
const QString _repostSourceName;
const StoryId _repostSourceId = 0;
PeerData * const _fromPeer = nullptr;
Data::ReactionId _sentReactionId;
StoryMedia _media;
TextWithEntities _caption;

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
@ -18,14 +19,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h"
#include "data/data_message_reactions.h"
#include "data/stickers/data_stickers.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_frame_generator.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "chat_helpers/stickers_lottie.h"
#include "storage/file_download.h" // kMaxFileInMemory
#include "ui/widgets/fields/input_field.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/ui_utility.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
@ -93,6 +98,10 @@ private:
return u"internal:"_q;
}
[[nodiscard]] QString UserpicEmojiPrefix() {
return u"userpic:"_q;
}
[[nodiscard]] QString InternalPadding(QMargins value) {
return value.isNull() ? QString() : QString(",%1,%2,%3,%4"
).arg(value.left()
@ -527,6 +536,10 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
int sizeOverride) {
if (data.startsWith(InternalPrefix())) {
return internal(data);
} else if (data.startsWith(UserpicEmojiPrefix())) {
const auto ratio = style::DevicePixelRatio();
const auto size = EmojiSizeFromTag(tag) / ratio;
return userpic(data, std::move(update), size);
}
const auto parsed = ParseCustomEmojiData(data);
return parsed
@ -574,6 +587,26 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::internal(
info.textColor);
}
std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::userpic(
QStringView data,
Fn<void()> update,
int size) {
const auto v = data.mid(UserpicEmojiPrefix().size()).split(',');
if (v.size() != 5 && v.size() != 1) {
return nullptr;
}
const auto id = PeerId(v[0].toULongLong());
const auto padding = (v.size() == 5)
? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt())
: QMargins();
return std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>(
data.toString(),
Ui::MakeUserpicThumbnail(_owner->peer(id)),
std::move(update),
padding,
size);
}
void CustomEmojiManager::resolve(
QStringView data,
not_null<Listener*> listener) {
@ -954,6 +987,14 @@ QString CustomEmojiManager::registerInternalEmoji(
return result + InternalPadding(padding);
}
[[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData(
not_null<PeerData*> peer,
QMargins padding) {
return UserpicEmojiPrefix()
+ QString::number(peer->id.value)
+ InternalPadding(padding);
}
int FrameSizeFromTag(SizeTag tag) {
const auto emoji = EmojiSizeFromTag(tag);
const auto factor = style::DevicePixelRatio();
@ -980,8 +1021,21 @@ TextWithEntities SingleCustomEmoji(not_null<DocumentData*> document) {
return SingleCustomEmoji(document->id);
}
bool AllowEmojiWithoutPremium(not_null<PeerData*> peer) {
return peer->isSelf();
bool AllowEmojiWithoutPremium(
not_null<PeerData*> peer,
DocumentData *exactEmoji) {
if (peer->isSelf()) {
return true;
} else if (!exactEmoji) {
return false;
} else if (const auto sticker = exactEmoji->sticker()) {
if (const auto channel = peer->asMegagroup()) {
if (channel->mgInfo->emojiSet.id == sticker->set.id) {
return (sticker->set.id != 0);
}
}
}
return false;
}
void InsertCustomEmoji(

View file

@ -92,6 +92,10 @@ public:
QMargins padding = {},
bool textColor = true);
[[nodiscard]] QString peerUserpicEmojiData(
not_null<PeerData*> peer,
QMargins padding = {});
[[nodiscard]] uint64 coloredSetId() const;
private:
@ -146,6 +150,10 @@ private:
LoaderFactory factory);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> internal(
QStringView data);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> userpic(
QStringView data,
Fn<void()> update,
int size);
[[nodiscard]] static int SizeIndex(SizeTag tag);
const not_null<Session*> _owner;
@ -201,7 +209,9 @@ private:
[[nodiscard]] TextWithEntities SingleCustomEmoji(
not_null<DocumentData*> document);
[[nodiscard]] bool AllowEmojiWithoutPremium(not_null<PeerData*> peer);
[[nodiscard]] bool AllowEmojiWithoutPremium(
not_null<PeerData*> peer,
DocumentData *exactEmoji = nullptr);
void InsertCustomEmoji(
not_null<Ui::InputField*> field,

View file

@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_three_state_icon.h"
#include "dialogs/ui/dialogs_layout.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "dialogs/ui/dialogs_video_userpic.h"
#include "dialogs/dialogs_indexed_list.h"
#include "dialogs/dialogs_widget.h"

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