diff --git a/.gitmodules b/.gitmodules index fd43d345f..333aa6d9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/telegramdesktop/libtgvoip [submodule "Telegram/ThirdParty/GSL"] path = Telegram/ThirdParty/GSL - url = https://github.com/Microsoft/GSL.git + url = https://github.com/desktop-app/GSL.git [submodule "Telegram/ThirdParty/xxHash"] path = Telegram/ThirdParty/xxHash url = https://github.com/Cyan4973/xxHash.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a4b102499..0042cc86e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -392,6 +392,7 @@ PRIVATE calls/calls_video_bubble.h calls/calls_video_incoming.cpp calls/calls_video_incoming.h + chat_helpers/compose/compose_features.h chat_helpers/compose/compose_show.cpp chat_helpers/compose/compose_show.h chat_helpers/bot_command.cpp @@ -592,6 +593,12 @@ PRIVATE data/data_sparse_ids.h data/data_sponsored_messages.cpp data/data_sponsored_messages.h + data/data_stories.cpp + data/data_stories.h + data/data_stories_ids.cpp + data/data_stories_ids.h + data/data_story.cpp + data/data_story.h data/data_streaming.cpp data/data_streaming.h data/data_thread.cpp @@ -632,6 +639,8 @@ PRIVATE dialogs/ui/dialogs_layout.h dialogs/ui/dialogs_message_view.cpp dialogs/ui/dialogs_message_view.h + dialogs/ui/dialogs_stories_content.cpp + dialogs/ui/dialogs_stories_content.h dialogs/ui/dialogs_topics_view.cpp dialogs/ui/dialogs_topics_view.h dialogs/ui/dialogs_video_userpic.cpp @@ -735,6 +744,8 @@ PRIVATE history/view/media/history_view_sticker_player.cpp history/view/media/history_view_sticker_player.h history/view/media/history_view_sticker_player_abstract.h + history/view/media/history_view_story_mention.cpp + history/view/media/history_view_story_mention.h history/view/media/history_view_theme_document.cpp history/view/media/history_view_theme_document.h history/view/media/history_view_userpic_suggestion.cpp @@ -910,6 +921,12 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/stories/info_stories_inner_widget.cpp + info/stories/info_stories_inner_widget.h + info/stories/info_stories_provider.cpp + info/stories/info_stories_provider.h + info/stories/info_stories_widget.cpp + info/stories/info_stories_widget.h info/userpic/info_userpic_colors_editor.cpp info/userpic/info_userpic_colors_editor.h info/userpic/info_userpic_emoji_builder.cpp @@ -980,8 +997,6 @@ PRIVATE main/session/send_as_peers.h main/session/session_show.cpp main/session/session_show.h - media/system_media_controls_manager.h - media/system_media_controls_manager.cpp media/audio/media_audio.cpp media/audio/media_audio.h media/audio/media_audio_capture.cpp @@ -1006,6 +1021,28 @@ PRIVATE media/player/media_player_volume_controller.h media/player/media_player_widget.cpp media/player/media_player_widget.h + media/stories/media_stories_caption_full_view.cpp + media/stories/media_stories_caption_full_view.h + media/stories/media_stories_controller.cpp + media/stories/media_stories_controller.h + media/stories/media_stories_delegate.cpp + media/stories/media_stories_delegate.h + media/stories/media_stories_header.cpp + media/stories/media_stories_header.h + media/stories/media_stories_reactions.cpp + media/stories/media_stories_reactions.h + media/stories/media_stories_recent_views.cpp + media/stories/media_stories_recent_views.h + media/stories/media_stories_reply.cpp + media/stories/media_stories_reply.h + media/stories/media_stories_share.cpp + media/stories/media_stories_share.h + media/stories/media_stories_sibling.cpp + media/stories/media_stories_sibling.h + media/stories/media_stories_slider.cpp + media/stories/media_stories_slider.h + media/stories/media_stories_view.cpp + media/stories/media_stories_view.h media/streaming/media_streaming_audio_track.cpp media/streaming/media_streaming_audio_track.h media/streaming/media_streaming_common.h @@ -1051,6 +1088,8 @@ PRIVATE media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h media/view/media_view_open_common.h + media/system_media_controls_manager.h + media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp menu/menu_antispam_validator.h menu/menu_item_download_files.cpp diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index 456d59f46..79b680cc2 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -111,6 +111,11 @@ pre { border-radius: 50%; overflow: hidden; } +.story { + display: block; + border-radius: 4px; + overflow: hidden; +} .userpic .initials { display: block; color: #fff; @@ -194,6 +199,10 @@ a.block_link:hover { text-decoration: none !important; background-color: #f5f7f8; } +a.expanded { + padding: 2px 8px; + margin: -2px -8px; +} .sections { padding: 11px 0; } @@ -428,6 +437,9 @@ div.toast_shown { .section.sessions { background-image: url(../images/section_sessions.png); } +.section.stories { + background-image: url(../images/section_stories.png); +} .section.web { background-image: url(../images/section_web.png); } @@ -489,6 +501,9 @@ div.toast_shown { .section.sessions { background-image: url(../images/section_sessions@2x.png); } +.section.stories { + background-image: url(../images/section_stories@2x.png); +} .section.web { background-image: url(../images/section_web@2x.png); } diff --git a/Telegram/Resources/export_html/images/section_stories.png b/Telegram/Resources/export_html/images/section_stories.png new file mode 100644 index 000000000..650c69c91 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories.png differ diff --git a/Telegram/Resources/export_html/images/section_stories@2x.png b/Telegram/Resources/export_html/images/section_stories@2x.png new file mode 100644 index 000000000..429245138 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock.png b/Telegram/Resources/icons/dialogs/dialogs_lock.png deleted file mode 100644 index cd953f753..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png deleted file mode 100644 index d29e0579f..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png deleted file mode 100644 index 796427920..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off.png new file mode 100644 index 000000000..2459e1137 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png new file mode 100644 index 000000000..33b77eb3f Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png new file mode 100644 index 000000000..7653aa431 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on.png new file mode 100644 index 000000000..8209991f3 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png new file mode 100644 index 000000000..c4e3d6b25 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png new file mode 100644 index 000000000..67c7d7c71 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock.png b/Telegram/Resources/icons/dialogs/dialogs_unlock.png deleted file mode 100644 index 207f93bb9..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png deleted file mode 100644 index 64359ff67..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png deleted file mode 100644 index c62942302..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add.png b/Telegram/Resources/icons/emoji/stickers_add.png deleted file mode 100644 index d4c77a202..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add@2x.png b/Telegram/Resources/icons/emoji/stickers_add@2x.png deleted file mode 100644 index f188f440d..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add@3x.png b/Telegram/Resources/icons/emoji/stickers_add@3x.png deleted file mode 100644 index 48de8115e..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot.png b/Telegram/Resources/icons/emoji/stickers_add_dot.png deleted file mode 100644 index 1bb4078f4..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png deleted file mode 100644 index 1723a7f36..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png deleted file mode 100644 index ce7f1aa76..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread.png b/Telegram/Resources/icons/emoji/stickers_add_unread.png deleted file mode 100644 index d8172e22b..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png deleted file mode 100644 index c48562529..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png deleted file mode 100644 index 46a03b994..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/info/info_media_stories.png b/Telegram/Resources/icons/info/info_media_stories.png new file mode 100644 index 000000000..5b4cb99be Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories.png differ diff --git a/Telegram/Resources/icons/info/info_media_stories@2x.png b/Telegram/Resources/icons/info/info_media_stories@2x.png new file mode 100644 index 000000000..fc0a2f6e3 Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories@2x.png differ diff --git a/Telegram/Resources/icons/info/info_media_stories@3x.png b/Telegram/Resources/icons/info/info_media_stories@3x.png new file mode 100644 index 000000000..df8a23bbd Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories@3x.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty.png b/Telegram/Resources/icons/info/info_media_story_empty.png new file mode 100644 index 000000000..6196956ba Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty@2x.png b/Telegram/Resources/icons/info/info_media_story_empty@2x.png new file mode 100644 index 000000000..c3e6997cd Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty@2x.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty@3x.png b/Telegram/Resources/icons/info/info_media_story_empty@3x.png new file mode 100644 index 000000000..b6422809d Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive.png b/Telegram/Resources/icons/info/info_stories_archive.png new file mode 100644 index 000000000..9b4b79ce4 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@2x.png b/Telegram/Resources/icons/info/info_stories_archive@2x.png new file mode 100644 index 000000000..831363fa5 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@3x.png b/Telegram/Resources/icons/info/info_stories_archive@3x.png new file mode 100644 index 000000000..e02e85c6c Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent.png b/Telegram/Resources/icons/info/info_stories_recent.png new file mode 100644 index 000000000..341ee2a06 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@2x.png b/Telegram/Resources/icons/info/info_stories_recent@2x.png new file mode 100644 index 000000000..ecb3fc72d Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@3x.png b/Telegram/Resources/icons/info/info_stories_recent@3x.png new file mode 100644 index 000000000..bacf1a3c1 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive.png b/Telegram/Resources/icons/info/info_stories_to_archive.png new file mode 100644 index 000000000..f9c81896e Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@2x.png b/Telegram/Resources/icons/info/info_stories_to_archive@2x.png new file mode 100644 index 000000000..ef905891c Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@3x.png b/Telegram/Resources/icons/info/info_stories_to_archive@3x.png new file mode 100644 index 000000000..bc2fc30af Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile.png b/Telegram/Resources/icons/info/info_stories_to_profile.png new file mode 100644 index 000000000..cfe465c5b Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@2x.png b/Telegram/Resources/icons/info/info_stories_to_profile@2x.png new file mode 100644 index 000000000..1e9990c2f Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@3x.png b/Telegram/Resources/icons/info/info_stories_to_profile@3x.png new file mode 100644 index 000000000..414fa60a2 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends.png b/Telegram/Resources/icons/mediaview/mini_close_friends.png new file mode 100644 index 000000000..5c3072447 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png new file mode 100644 index 000000000..82dd44af9 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png new file mode 100644 index 000000000..1f024ce73 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts.png b/Telegram/Resources/icons/mediaview/mini_contacts.png new file mode 100644 index 000000000..bc716cab4 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_contacts@2x.png new file mode 100644 index 000000000..a7df593e8 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_contacts@3x.png new file mode 100644 index 000000000..50c9ff55f Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts.png new file mode 100644 index 000000000..c3ca5b06d Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png new file mode 100644 index 000000000..d12f95496 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png new file mode 100644 index 000000000..90860348b Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next.png b/Telegram/Resources/icons/mediaview/stories_next.png new file mode 100644 index 000000000..dd997b2d0 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next@2x.png b/Telegram/Resources/icons/mediaview/stories_next@2x.png new file mode 100644 index 000000000..82b819e9e Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next@3x.png b/Telegram/Resources/icons/mediaview/stories_next@3x.png new file mode 100644 index 000000000..3550c8cce Binary files /dev/null and b/Telegram/Resources/icons/mediaview/stories_next@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share.png b/Telegram/Resources/icons/mediaview/viewer_share.png new file mode 100644 index 000000000..14861b1ee Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share@2x.png b/Telegram/Resources/icons/mediaview/viewer_share@2x.png new file mode 100644 index 000000000..7c6e9341e Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share@3x.png b/Telegram/Resources/icons/mediaview/viewer_share@3x.png new file mode 100644 index 000000000..43310c895 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share@3x.png differ diff --git a/Telegram/Resources/icons/menu/archive_open.png b/Telegram/Resources/icons/menu/archive_open.png new file mode 100644 index 000000000..2e3090130 Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open.png differ diff --git a/Telegram/Resources/icons/menu/archive_open@2x.png b/Telegram/Resources/icons/menu/archive_open@2x.png new file mode 100644 index 000000000..c95bd9f66 Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open@2x.png differ diff --git a/Telegram/Resources/icons/menu/archive_open@3x.png b/Telegram/Resources/icons/menu/archive_open@3x.png new file mode 100644 index 000000000..74687f9ec Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open@3x.png differ diff --git a/Telegram/Resources/icons/menu/channel.png b/Telegram/Resources/icons/menu/channel.png index df2823788..d6caf7131 100644 Binary files a/Telegram/Resources/icons/menu/channel.png and b/Telegram/Resources/icons/menu/channel.png differ diff --git a/Telegram/Resources/icons/menu/channel@2x.png b/Telegram/Resources/icons/menu/channel@2x.png index 68e65eade..c68e6a6b0 100644 Binary files a/Telegram/Resources/icons/menu/channel@2x.png and b/Telegram/Resources/icons/menu/channel@2x.png differ diff --git a/Telegram/Resources/icons/menu/channel@3x.png b/Telegram/Resources/icons/menu/channel@3x.png index e3f8e50d3..0ce3d46a9 100644 Binary files a/Telegram/Resources/icons/menu/channel@3x.png and b/Telegram/Resources/icons/menu/channel@3x.png differ diff --git a/Telegram/Resources/icons/menu/groups.png b/Telegram/Resources/icons/menu/groups.png new file mode 100644 index 000000000..2fc454a36 Binary files /dev/null and b/Telegram/Resources/icons/menu/groups.png differ diff --git a/Telegram/Resources/icons/menu/groups@2x.png b/Telegram/Resources/icons/menu/groups@2x.png new file mode 100644 index 000000000..986bc696d Binary files /dev/null and b/Telegram/Resources/icons/menu/groups@2x.png differ diff --git a/Telegram/Resources/icons/menu/groups@3x.png b/Telegram/Resources/icons/menu/groups@3x.png new file mode 100644 index 000000000..28ed4085f Binary files /dev/null and b/Telegram/Resources/icons/menu/groups@3x.png differ diff --git a/Telegram/Resources/icons/menu/night_mode.png b/Telegram/Resources/icons/menu/night_mode.png new file mode 100644 index 000000000..531195e73 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode.png differ diff --git a/Telegram/Resources/icons/menu/night_mode@2x.png b/Telegram/Resources/icons/menu/night_mode@2x.png new file mode 100644 index 000000000..1742270a3 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode@2x.png differ diff --git a/Telegram/Resources/icons/menu/night_mode@3x.png b/Telegram/Resources/icons/menu/night_mode@3x.png new file mode 100644 index 000000000..601217161 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode@3x.png differ diff --git a/Telegram/Resources/icons/menu/profile.png b/Telegram/Resources/icons/menu/profile.png index 962419484..99861b419 100644 Binary files a/Telegram/Resources/icons/menu/profile.png and b/Telegram/Resources/icons/menu/profile.png differ diff --git a/Telegram/Resources/icons/menu/profile@2x.png b/Telegram/Resources/icons/menu/profile@2x.png index fe742a412..14faac768 100644 Binary files a/Telegram/Resources/icons/menu/profile@2x.png and b/Telegram/Resources/icons/menu/profile@2x.png differ diff --git a/Telegram/Resources/icons/menu/profile@3x.png b/Telegram/Resources/icons/menu/profile@3x.png index e2cf03667..f760db92b 100644 Binary files a/Telegram/Resources/icons/menu/profile@3x.png and b/Telegram/Resources/icons/menu/profile@3x.png differ diff --git a/Telegram/Resources/icons/menu/saved_messages.png b/Telegram/Resources/icons/menu/saved_messages.png new file mode 100644 index 000000000..3b8ba1eb7 Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages.png differ diff --git a/Telegram/Resources/icons/menu/saved_messages@2x.png b/Telegram/Resources/icons/menu/saved_messages@2x.png new file mode 100644 index 000000000..b89a09231 Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages@2x.png differ diff --git a/Telegram/Resources/icons/menu/saved_messages@3x.png b/Telegram/Resources/icons/menu/saved_messages@3x.png new file mode 100644 index 000000000..6fa7627de Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages@3x.png differ diff --git a/Telegram/Resources/icons/menu/settings.png b/Telegram/Resources/icons/menu/settings.png index 4453866e9..27b0cbcd6 100644 Binary files a/Telegram/Resources/icons/menu/settings.png and b/Telegram/Resources/icons/menu/settings.png differ diff --git a/Telegram/Resources/icons/menu/settings@2x.png b/Telegram/Resources/icons/menu/settings@2x.png index e255fb46a..76e0fbbe4 100644 Binary files a/Telegram/Resources/icons/menu/settings@2x.png and b/Telegram/Resources/icons/menu/settings@2x.png differ diff --git a/Telegram/Resources/icons/menu/settings@3x.png b/Telegram/Resources/icons/menu/settings@3x.png index 79b2cf6d2..5135a603e 100644 Binary files a/Telegram/Resources/icons/menu/settings@3x.png and b/Telegram/Resources/icons/menu/settings@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive.png b/Telegram/Resources/icons/menu/stories_archive.png new file mode 100644 index 000000000..a98e8583c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive@2x.png b/Telegram/Resources/icons/menu/stories_archive@2x.png new file mode 100644 index 000000000..5c5602569 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive@3x.png b/Telegram/Resources/icons/menu/stories_archive@3x.png new file mode 100644 index 000000000..715f241ba Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section.png b/Telegram/Resources/icons/menu/stories_archive_section.png new file mode 100644 index 000000000..d86b5dc87 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section@2x.png b/Telegram/Resources/icons/menu/stories_archive_section@2x.png new file mode 100644 index 000000000..f2c59be33 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section@3x.png b/Telegram/Resources/icons/menu/stories_archive_section@3x.png new file mode 100644 index 000000000..f6369c74c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_save.png b/Telegram/Resources/icons/menu/stories_save.png new file mode 100644 index 000000000..68d2a12f4 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save.png differ diff --git a/Telegram/Resources/icons/menu/stories_save@2x.png b/Telegram/Resources/icons/menu/stories_save@2x.png new file mode 100644 index 000000000..327054159 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_save@3x.png b/Telegram/Resources/icons/menu/stories_save@3x.png new file mode 100644 index 000000000..e97dac0ab Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section.png b/Telegram/Resources/icons/menu/stories_saved_section.png new file mode 100644 index 000000000..31a29d96b Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section@2x.png b/Telegram/Resources/icons/menu/stories_saved_section@2x.png new file mode 100644 index 000000000..02a9f4687 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section@3x.png b/Telegram/Resources/icons/menu/stories_saved_section@3x.png new file mode 100644 index 000000000..8de09fb32 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats.png b/Telegram/Resources/icons/menu/stories_to_chats.png new file mode 100644 index 000000000..b5129521c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats@2x.png b/Telegram/Resources/icons/menu/stories_to_chats@2x.png new file mode 100644 index 000000000..575fcc811 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats@3x.png b/Telegram/Resources/icons/menu/stories_to_chats@3x.png new file mode 100644 index 000000000..00f6b959b Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats@3x.png differ diff --git a/Telegram/Resources/icons/settings/stories.png b/Telegram/Resources/icons/settings/stories.png new file mode 100644 index 000000000..c41ac06dd Binary files /dev/null and b/Telegram/Resources/icons/settings/stories.png differ diff --git a/Telegram/Resources/icons/settings/stories@2x.png b/Telegram/Resources/icons/settings/stories@2x.png new file mode 100644 index 000000000..82e24fc67 Binary files /dev/null and b/Telegram/Resources/icons/settings/stories@2x.png differ diff --git a/Telegram/Resources/icons/settings/stories@3x.png b/Telegram/Resources/icons/settings/stories@3x.png new file mode 100644 index 000000000..23c70a8cc Binary files /dev/null and b/Telegram/Resources/icons/settings/stories@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4b37c2697..5a0e5e847 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_activate" = "Activate"; "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; +"lng_menu_my_stories" = "My Stories"; "lng_disable_notifications_from_tray" = "Disable notifications"; "lng_enable_notifications_from_tray" = "Enable notifications"; @@ -285,6 +286,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_message_text" = "New message text..."; "lng_deleted" = "Deleted Account"; "lng_deleted_message" = "Deleted message"; +"lng_deleted_story" = "Deleted story"; "lng_pinned_message" = "Pinned message"; "lng_pinned_previous" = "Previous message"; "lng_pinned_unpin_sure" = "Would you like to unpin this message?"; @@ -583,6 +585,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_forwards_privacy" = "Forwarded messages"; "lng_settings_profile_photo_privacy" = "Profile photo"; "lng_settings_voices_privacy" = "Voice messages"; +"lng_settings_bio_privacy" = "Bio"; "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages."; "lng_settings_privacy_premium_link" = "Telegram Premium"; "lng_settings_passcode_disable" = "Disable Passcode"; @@ -754,6 +757,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_background_apply2" = "Enjoy the view."; "lng_background_apply_button" = "Apply For This Chat"; "lng_background_dimming" = "Background dimming"; +"lng_background_sure_reset_default" = "Are you sure you want to reset the wallpaper?"; +"lng_background_reset_default" = "Reset"; "lng_download_path_ask" = "Ask download path for each file"; "lng_download_path" = "Download path"; @@ -961,6 +966,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_everyone" = "Everybody"; "lng_edit_privacy_contacts" = "My contacts"; +"lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_nobody" = "Nobody"; "lng_edit_privacy_exceptions" = "Add exceptions"; @@ -997,6 +1003,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_groups_always_title" = "Always allow"; "lng_edit_privacy_groups_never_title" = "Never allow"; +"lng_edit_privacy_about_title" = "Bio privacy settings"; +"lng_edit_privacy_about_header" = "Who can see my bio"; +"lng_edit_privacy_about_always_empty" = "Always allow"; +"lng_edit_privacy_about_never_empty" = "Never allow"; +"lng_edit_privacy_about_exceptions" = "These users will or will not be able to see your profile bio regardless of the settings above."; +"lng_edit_privacy_about_always_title" = "Always allow"; +"lng_edit_privacy_about_never_title" = "Never allow"; + "lng_edit_privacy_calls_title" = "Voice calls privacy"; "lng_edit_privacy_calls_header" = "Who can call you"; "lng_edit_privacy_calls_always_empty" = "Always allow"; @@ -1111,6 +1125,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_sure_kick_channel" = "Remove {user} from the channel?"; "lng_profile_sure_remove_admin" = "Remove {user} from admins?"; "lng_profile_loading" = "Loading..."; +"lng_profile_saved_stories#one" = "{count} saved story"; +"lng_profile_saved_stories#other" = "{count} saved stories"; "lng_profile_photos#one" = "{count} photo"; "lng_profile_photos#other" = "{count} photos"; "lng_profile_gifs#one" = "{count} GIF"; @@ -1334,6 +1350,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_report_group_video_title" = "Report group video"; "lng_report_channel_photo_title" = "Report channel photo"; "lng_report_channel_video_title" = "Report channel video"; +"lng_report_story" = "Report story"; "lng_report_please_select_messages" = "Please select messages to report."; "lng_report_select_messages" = "Select messages"; "lng_report_messages_none" = "Select Messages"; @@ -1491,6 +1508,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_pinned_media_sticker" = "a sticker"; "lng_action_pinned_media_emoji_sticker" = "a {emoji} sticker"; "lng_action_pinned_media_game" = "the game «{game}»"; +"lng_action_pinned_media_story" = "a story"; "lng_action_game_score#one" = "{from} scored {count} in {game}"; "lng_action_game_score#other" = "{from} scored {count} in {game}"; "lng_action_game_you_scored#one" = "You scored {count} in {game}"; @@ -1556,6 +1574,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_topic_icon_changed" = "{from} changed the {link} icon to {emoji}"; "lng_action_topic_icon_removed" = "{from} removed the {link} icon"; "lng_action_shared_chat_with_bot" = "You shared {chat} with {bot}"; +"lng_action_story_mention_me" = "You mentioned {user} in a story"; +"lng_action_story_mention" = "{user} mentioned you in a story"; +"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_premium_gift_duration_months#one" = "for {count} month"; "lng_premium_gift_duration_months#other" = "for {count} months"; @@ -1686,6 +1709,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_about_private_link" = "This link will only work for members of this chat."; "lng_forwarded" = "Forwarded from {user}"; +"lng_forwarded_story" = "Story from {user}"; "lng_forwarded_date" = "Original: {date}"; "lng_forwarded_channel" = "Forwarded from {channel}"; "lng_forwarded_psa_default" = "Forwarded from {channel}"; @@ -1960,6 +1984,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_sticker" = "Sticker"; "lng_in_dlg_sticker_emoji" = "{emoji} Sticker"; "lng_in_dlg_poll" = "Poll"; +"lng_in_dlg_story" = "Story"; +"lng_in_dlg_story_expired" = "Expired story"; "lng_in_dlg_media_count#one" = "{count} media"; "lng_in_dlg_media_count#other" = "{count} media"; "lng_in_dlg_photo_count#one" = "{count} photo"; @@ -2018,6 +2044,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_broadcast_ph" = "Broadcast a message..."; "lng_broadcast_silent_ph" = "Silent broadcast..."; "lng_send_anonymous_ph" = "Send anonymously..."; +"lng_story_reply_ph" = "Reply privately..."; "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}"; @@ -2035,6 +2062,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_as_premium_required" = "Subscribe to {link} to be able to comment on behalf of your channels in group chats."; "lng_send_as_premium_required_link" = "Telegram Premium"; "lng_record_cancel" = "Release outside this field to cancel"; +"lng_record_cancel_stories" = "Release outside to cancel"; "lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?"; "lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?"; "lng_record_lock_discard" = "Discard"; @@ -2381,6 +2409,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_add_contact" = "Create"; "lng_add_contact_button" = "New contact"; "lng_contacts_header" = "Contacts"; +"lng_contacts_hidden_stories" = "Hidden Stories"; +"lng_contacts_stories_status#one" = "{count} story"; +"lng_contacts_stories_status#other" = "{count} stories"; +"lng_contacts_stories_status_new#one" = "{count} new story"; +"lng_contacts_stories_status_new#other" = "{count} new stories"; "lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram."; "lng_try_other_contact" = "Try someone else"; "lng_create_group_link" = "Link"; @@ -2451,6 +2484,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_copy" = "Copy"; "lng_mediaview_forward" = "Forward"; "lng_mediaview_delete" = "Delete"; +"lng_mediaview_save_to_profile" = "Save to Profile"; +"lng_mediaview_archive_story" = "Archive Story"; "lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_files_all" = "View all files"; "lng_mediaview_single_photo" = "Single Photo"; @@ -2464,6 +2499,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_doc_image" = "File"; "lng_mediaview_today" = "today at {time}"; "lng_mediaview_yesterday" = "yesterday at {time}"; +"lng_mediaview_just_now" = "just now"; +"lng_mediaview_minutes_ago#one" = "{count} minute ago"; +"lng_mediaview_minutes_ago#other" = "{count} minutes ago"; +"lng_mediaview_hours_ago#one" = "{count} hour ago"; +"lng_mediaview_hours_ago#other" = "{count} hours ago"; "lng_mediaview_date_time" = "{date} at {time}"; "lng_mediaview_set_userpic" = "Set as Main"; "lng_mediaview_report_profile_photo" = "Report"; @@ -2471,6 +2511,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_title" = "Media viewer"; "lng_mediaview_saved_to" = "Image was saved to your {downloads} folder"; "lng_mediaview_saved_images_to" = "Images were saved to your {downloads} folder"; +"lng_mediaview_video_saved_to" = "Video file was saved to your {downloads} folder"; "lng_mediaview_downloads" = "Downloads"; "lng_mediaview_playback_speed" = "Playback speed: {speed}"; "lng_mediaview_rotate_video" = "Rotate video"; @@ -3389,6 +3430,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_export_option_info_about" = "Your chosen screen name, username, phone number and profile pictures."; "lng_export_option_contacts" = "Contacts list"; "lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices."; +"lng_export_option_stories" = "Stories archive"; +"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps."; "lng_export_option_sessions" = "Active sessions"; "lng_export_option_sessions_about" = "We store this to display your connected devices in Settings > Privacy & Security > Active Sessions."; "lng_export_header_other" = "Other"; @@ -3675,9 +3718,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_background" = "View background"; "lng_view_button_theme" = "View theme"; "lng_view_button_message" = "View message"; +"lng_view_button_story" = "View story"; "lng_view_button_voice_chat" = "Voice chat"; "lng_view_button_voice_chat_channel" = "Live stream"; "lng_view_button_request_join" = "Request to Join"; +"lng_view_button_external_link" = "Open link"; "lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_title" = "What are sponsored messages?"; @@ -3777,6 +3822,44 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_userpic_builder_color_subtitle" = "Choose background"; "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; +"lng_stories_my_name" = "My Story"; +"lng_stories_archive" = "Hide Stories"; +"lng_stories_unarchive" = "Unhide Stories"; +"lng_stories_row_count#one" = "{count} Story"; +"lng_stories_row_count#other" = "{count} Stories"; +"lng_stories_views#one" = "{count} view"; +"lng_stories_views#other" = "{count} views"; +"lng_stories_no_views" = "No views"; +"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; +"lng_stories_cant_reply" = "You can't reply to this story."; + +"lng_stories_my_title" = "Saved Stories"; +"lng_stories_archive_button" = "Stories Archive"; +"lng_stories_recent_button" = "Recent Stories"; +"lng_stories_archive_title" = "Stories Archive"; +"lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile."; +"lng_stories_reply_sent" = "Message Sent"; +"lng_stories_hidden_to_contacts" = "Stories from {user} will now be shown in **Archived Chats**."; +"lng_stories_shown_in_chats" = "Stories from {user} will now be shown in the **Chats List**."; +"lng_stories_delete_one_sure" = "Are you sure you want to delete this story?"; +"lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?"; +"lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?"; +"lng_stories_save_sure" = "Do you want to save this story to your profile?"; +"lng_stories_save_sure_many#one" = "Do you want to save {count} story to your profile?"; +"lng_stories_save_sure_many#other" = "Do you want to save {count} stories to your profile?"; +"lng_stories_save_done" = "This story is saved to your profile."; +"lng_stories_save_done_many#one" = "{count} story is saved to your profile."; +"lng_stories_save_done_many#other" = "{count} stories are saved to your profile."; +"lng_stories_save_done_about" = "Saved stories can be viewed by others on your profile until you remove them."; +"lng_stories_archive_sure" = "Do you want to hide this story from your profile?"; +"lng_stories_archive_sure_many#one" = "Do you want to hide {count} story from your profile?"; +"lng_stories_archive_sure_many#other" = "Do you want to hide {count} stories from your profile?"; +"lng_stories_archive_done" = "This story is hidden from your profile."; +"lng_stories_archive_done_many#one" = "{count} story is hidden from your profile."; +"lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile."; + +"lng_stories_link_invalid" = "This link is broken or has expired."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc index 06ecc7eb7..290d24a30 100644 --- a/Telegram/Resources/qrc/telegram/export.qrc +++ b/Telegram/Resources/qrc/telegram/export.qrc @@ -37,6 +37,8 @@ ../../export_html/images/section_photos@2x.png ../../export_html/images/section_sessions.png ../../export_html/images/section_sessions@2x.png + ../../export_html/images/section_stories.png + ../../export_html/images/section_stories@2x.png ../../export_html/images/section_web.png ../../export_html/images/section_web@2x.png ../../export_html/js/script.js diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 475e3ee7e..898e980a2 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.8.7.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index cedf12840..68328ffe8 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,8,4,0 - PRODUCTVERSION 4,8,4,0 + FILEVERSION 4,8,7,0 + PRODUCTVERSION 4,8,7,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.8.4.0" + VALUE "FileVersion", "4.8.7.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.8.4.0" + VALUE "ProductVersion", "4.8.7.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index f6f63f644..86b7ee28f 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,8,4,0 - PRODUCTVERSION 4,8,4,0 + FILEVERSION 4,8,7,0 + PRODUCTVERSION 4,8,7,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.8.4.0" + VALUE "FileVersion", "4.8.7.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.8.4.0" + VALUE "ProductVersion", "4.8.7.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index e0a0162ef..2a28bb74c 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -376,8 +376,10 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { ShowAtTheEndMsgId); auto action = Api::SendAction(history); action.clearDraft = false; - action.replyTo = itemId; - action.topicRootId = topicRootId; + action.replyTo = { + .msgId = itemId, + .topicRootId = topicRootId, + }; history->session().api().shareContact( history->session().user(), action); diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index f4afa97f6..0a44a916f 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -8,7 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "base/qt/qt_key_modifiers.h" +#include "data/data_histories.h" #include "data/data_thread.h" +#include "history/history.h" namespace Api { @@ -17,8 +19,8 @@ SendAction::SendAction( SendOptions options) : history(thread->owningHistory()) , options(options) -, replyTo(thread->topicRootId()) -, topicRootId(replyTo) { +, replyTo({ .msgId = thread->topicRootId() }) { + replyTo.topicRootId = replyTo.msgId; } SendOptions DefaultSendWhenOnlineOptions() { @@ -28,4 +30,8 @@ SendOptions DefaultSendWhenOnlineOptions() { }; } +MTPInputReplyTo SendAction::mtpReplyTo() const { + return Data::ReplyToForMTP(&history->owner(), replyTo); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 1bdb97ee5..08ee59ca1 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -40,11 +40,12 @@ struct SendAction { not_null history; SendOptions options; - MsgId replyTo = 0; - MsgId topicRootId = 0; + FullReplyTo replyTo; bool clearDraft = true; bool generateLocal = true; MsgId replaceMediaOf = 0; + + [[nodiscard]] MTPInputReplyTo mtpReplyTo() const; }; struct MessageToSend { diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index c940dbe86..89e1d57da 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -56,6 +56,15 @@ rpl::producer GlobalPrivacy::archiveAndMute() const { return _archiveAndMute.value(); } +UnarchiveOnNewMessage GlobalPrivacy::unarchiveOnNewMessageCurrent() const { + return _unarchiveOnNewMessage.current(); +} + +auto GlobalPrivacy::unarchiveOnNewMessage() const +-> rpl::producer { + return _unarchiveOnNewMessage.value(); +} + rpl::producer GlobalPrivacy::showArchiveAndMute() const { using namespace rpl::mappers; @@ -78,11 +87,20 @@ void GlobalPrivacy::dismissArchiveAndMuteSuggestion() { void GlobalPrivacy::update(bool archiveAndMute) { using Flag = MTPDglobalPrivacySettings::Flag; + const auto unarchive = unarchiveOnNewMessageCurrent(); _api.request(_requestId).cancel(); + const auto flags = Flag() + | (archiveAndMute + ? Flag::f_archive_and_mute_new_noncontact_peers + : Flag()) + | (unarchive == UnarchiveOnNewMessage::AnyUnmuted + ? Flag::f_keep_archived_unmuted + : Flag()) + | (unarchive != UnarchiveOnNewMessage::None + ? Flag::f_keep_archived_folders + : Flag()); _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( - MTP_globalPrivacySettings( - MTP_flags(Flag::f_archive_and_mute_new_noncontact_peers), - MTP_bool(archiveAndMute)) + MTP_globalPrivacySettings(MTP_flags(flags)) )).done([=](const MTPGlobalPrivacySettings &result) { _requestId = 0; apply(result); @@ -94,9 +112,12 @@ void GlobalPrivacy::update(bool archiveAndMute) { void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) { data.match([&](const MTPDglobalPrivacySettings &data) { - _archiveAndMute = data.varchive_and_mute_new_noncontact_peers() - ? mtpIsTrue(*data.varchive_and_mute_new_noncontact_peers()) - : false; + _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers(); + _unarchiveOnNewMessage = data.is_keep_archived_unmuted() + ? UnarchiveOnNewMessage::AnyUnmuted + : data.is_keep_archived_folders() + ? UnarchiveOnNewMessage::NotInFoldersUnmuted + : UnarchiveOnNewMessage::None; }); } diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index a1a693499..f569951ca 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -17,6 +17,12 @@ class Session; namespace Api { +enum class UnarchiveOnNewMessage { + None, + NotInFoldersUnmuted, + AnyUnmuted, +}; + class GlobalPrivacy final { public: explicit GlobalPrivacy(not_null api); @@ -26,6 +32,10 @@ public: [[nodiscard]] bool archiveAndMuteCurrent() const; [[nodiscard]] rpl::producer archiveAndMute() const; + [[nodiscard]] auto unarchiveOnNewMessageCurrent() const + -> UnarchiveOnNewMessage; + [[nodiscard]] auto unarchiveOnNewMessage() const + -> rpl::producer; [[nodiscard]] rpl::producer showArchiveAndMute() const; [[nodiscard]] rpl::producer<> suggestArchiveAndMute() const; void dismissArchiveAndMuteSuggestion(); @@ -37,6 +47,8 @@ private: MTP::Sender _api; mtpRequestId _requestId = 0; rpl::variable _archiveAndMute = false; + rpl::variable _unarchiveOnNewMessage + = UnarchiveOnNewMessage::None; rpl::variable _showArchiveAndMute = false; std::vector> _callbacks; diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index 7dd916140..ec8c1e4b2 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -22,8 +22,7 @@ MTPVector ComposeSendingDocumentAttributes( const auto dimensions = document->dimensions; auto attributes = QVector(1, filenameAttribute); if (dimensions.width() > 0 && dimensions.height() > 0) { - const auto duration = document->getDuration(); - if (duration >= 0 && !document->hasMimeType(u"image/gif"_q)) { + if (document->hasDuration() && !document->hasMimeType(u"image/gif"_q)) { auto flags = MTPDdocumentAttributeVideo::Flags(0); using VideoFlag = MTPDdocumentAttributeVideo::Flag; if (document->isVideoMessage()) { @@ -34,9 +33,10 @@ MTPVector ComposeSendingDocumentAttributes( } attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), - MTP_int(duration), + MTP_double(document->duration() / 1000.), MTP_int(dimensions.width()), - MTP_int(dimensions.height()))); + MTP_int(dimensions.height()), + MTPint())); // preload_prefix_size } else { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(dimensions.width()), @@ -56,7 +56,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_performer; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(song->duration), + MTP_int(document->duration() / 1000), MTP_string(song->title), MTP_string(song->performer), MTPstring())); @@ -65,7 +65,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_waveform; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(voice->duration), + MTP_int(document->duration() / 1000), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(voice->waveform)))); diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index a449b2200..71512172b 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -97,8 +97,7 @@ constexpr auto kSharedMediaLimit = 100; photo, photoThumbs, MTP_documentEmpty(MTP_long(0)), - jpeg, - 0); + jpeg); } [[nodiscard]] std::optional PrepareMtpMarkup( diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index d18c947c1..227b6e9db 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -43,13 +43,12 @@ void Polls::create( const auto history = action.history; const auto peer = history->peer; - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; auto sendFlags = MTPmessages_SendMedia::Flags(0); if (action.replyTo) { - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto clearCloudDraft = action.clearDraft; if (clearCloudDraft) { @@ -74,13 +73,11 @@ void Polls::create( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), PollDataToInputMedia(&data), MTP_string(), MTP_long(randomId), diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp index 7185b76f5..501870180 100644 --- a/Telegram/SourceFiles/api/api_report.cpp +++ b/Telegram/SourceFiles/api/api_report.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_peer.h" #include "data/data_photo.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/boxes/report_box.h" @@ -39,11 +40,15 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) { } // namespace void SendReport( - std::shared_ptr show, - not_null peer, - Ui::ReportReason reason, - const QString &comment, - std::variant> data) { + std::shared_ptr show, + not_null peer, + Ui::ReportReason reason, + const QString &comment, + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data) { auto done = [=] { show->showToast(tr::lng_report_thanks(tr::now)); }; @@ -72,6 +77,17 @@ void SendReport( ReasonToTL(reason), MTP_string(comment) )).done(std::move(done)).send(); + }, [&](StoryId id) { + const auto user = peer->asUser(); + if (!user) { + return; + } + peer->session().api().request(MTPstories_Report( + user->inputUser, + MTP_vector(1, MTP_int(id)), + ReasonToTL(reason), + MTP_string(comment) + )).done(std::move(done)).send(); }); } diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h index 08535c00b..14e9d4ef1 100644 --- a/Telegram/SourceFiles/api/api_report.h +++ b/Telegram/SourceFiles/api/api_report.h @@ -22,6 +22,10 @@ void SendReport( not_null peer, Ui::ReportReason reason, const QString &comment, - std::variant> data); + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data); } // namespace Api diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index 40b07e3f3..5b471851e 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -60,8 +60,7 @@ SendMediaReady PrepareRingtoneDocument( MTP_photoEmpty(MTP_long(0)), PreparedPhotoThumbs(), document, - QByteArray(), - 0); + QByteArray()); } } // namespace @@ -203,8 +202,8 @@ int Ringtones::maxSavedCount() const { 100); } -int Ringtones::maxDuration() const { - return _session->account().appConfig().get( +crl::time Ringtones::maxDuration() const { + return crl::time(1000) * _session->account().appConfig().get( "ringtone_duration_max", 5); } diff --git a/Telegram/SourceFiles/api/api_ringtones.h b/Telegram/SourceFiles/api/api_ringtones.h index 08918a89e..ea97db349 100644 --- a/Telegram/SourceFiles/api/api_ringtones.h +++ b/Telegram/SourceFiles/api/api_ringtones.h @@ -40,7 +40,7 @@ public: [[nodiscard]] int64 maxSize() const; [[nodiscard]] int maxSavedCount() const; - [[nodiscard]] int maxDuration() const; + [[nodiscard]] crl::time maxDuration() const; private: struct UploadedData { diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index dfba1d930..9cfe272f8 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -86,10 +86,7 @@ void SendExistingMedia( auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, message.action.options); @@ -150,13 +147,11 @@ void SendExistingMedia( histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), inputMedia(), MTP_string(captionText), MTP_long(randomId), @@ -273,10 +268,7 @@ bool SendDice(MessageToSend &message) { auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(message.action); const auto anonymousPost = peer->amAnonymous(); @@ -320,13 +312,11 @@ bool SendDice(MessageToSend &message) { histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_inputMediaDice(MTP_string(emoji)), MTP_string(), MTP_long(randomId), @@ -378,12 +368,12 @@ void SendConfirmedFile( if (!isEditing) { const auto histories = &session->data().histories(); - file->to.replyTo = histories->convertTopicReplyTo( + file->to.replyTo.msgId = histories->convertTopicReplyToId( history, - file->to.replyTo); - file->to.topicRootId = histories->convertTopicReplyTo( + file->to.replyTo.msgId); + file->to.replyTo.topicRootId = histories->convertTopicReplyToId( history, - file->to.topicRootId); + file->to.replyTo.topicRootId); } session->uploader().upload(newId, file); @@ -391,7 +381,6 @@ void SendConfirmedFile( auto action = SendAction(history, file->to.options); action.clearDraft = false; action.replyTo = file->to.replyTo; - action.topicRootId = file->to.topicRootId; action.generateLocal = true; action.replaceMediaOf = file->to.replaceMediaOf; session->api().sendAction(action); @@ -453,11 +442,13 @@ void SendConfirmedFile( MTP_flags(Flag::f_document | (file->spoiler ? Flag::f_spoiler : Flag())), file->document, + MTPDocument(), // alt_document MTPint()); } else if (file->type == SendMediaType::Audio) { return MTP_messageMediaDocument( MTP_flags(MTPDmessageMediaDocument::Flag::f_document), file->document, + MTPDocument(), // alt_document MTPint()); } else { Unexpected("Type in sendFilesConfirmed."); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index f78a5d9d1..31462da39 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_scheduled_messages.h" #include "data/data_send_action.h" +#include "data/data_stories.h" #include "data/data_message_reactions.h" #include "inline_bots/bot_attach_web_view.h" #include "chat_helpers/emoji_interactions.h" @@ -679,9 +680,11 @@ void Updates::getDifference() { api().request(MTPupdates_GetDifference( MTP_flags(0), MTP_int(_ptsWaiter.current()), - MTPint(), + MTPint(), // pts_limit + MTPint(), // pts_total_limit MTP_int(_updatesDate), - MTP_int(_updatesQts) + MTP_int(_updatesQts), + MTPint() // qts_limit )).done([=](const MTPupdates_Difference &result) { differenceDone(result); }).fail([=](const MTP::Error &error) { @@ -2545,7 +2548,15 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); - } + } break; + + case mtpc_updateStory: { + _session->data().stories().apply(update.c_updateStory()); + } break; + + case mtpc_updateReadStories: { + _session->data().stories().apply(update.c_updateReadStories()); + } break; } } diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index 8c97479c6..edaae7c97 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -82,6 +82,8 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) { switch (rule.option) { case Option::Everyone: return MTP_inputPrivacyValueAllowAll(); case Option::Contacts: return MTP_inputPrivacyValueAllowContacts(); + case Option::CloseFriends: + return MTP_inputPrivacyValueAllowCloseFriends(); case Option::Nobody: return MTP_inputPrivacyValueDisallowAll(); } Unexpected("Option value in Api::UserPrivacy::RulesToTL."); @@ -110,6 +112,8 @@ UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) { setOption(Option::Everyone); }, [&](const MTPDprivacyValueAllowContacts &) { setOption(Option::Contacts); + }, [&](const MTPDprivacyValueAllowCloseFriends &) { + setOption(Option::CloseFriends); }, [&](const MTPDprivacyValueAllowUsers &data) { const auto &users = data.vusers().v; always.reserve(always.size() + users.size()); @@ -177,18 +181,13 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) { case Key::Calls: return MTP_inputPrivacyKeyPhoneCall(); case Key::Invites: return MTP_inputPrivacyKeyChatInvite(); case Key::PhoneNumber: return MTP_inputPrivacyKeyPhoneNumber(); - case Key::AddedByPhone: - return MTP_inputPrivacyKeyAddedByPhone(); - case Key::LastSeen: - return MTP_inputPrivacyKeyStatusTimestamp(); - case Key::CallsPeer2Peer: - return MTP_inputPrivacyKeyPhoneP2P(); - case Key::Forwards: - return MTP_inputPrivacyKeyForwards(); - case Key::ProfilePhoto: - return MTP_inputPrivacyKeyProfilePhoto(); - case Key::Voices: - return MTP_inputPrivacyKeyVoiceMessages(); + case Key::AddedByPhone: return MTP_inputPrivacyKeyAddedByPhone(); + case Key::LastSeen: return MTP_inputPrivacyKeyStatusTimestamp(); + case Key::CallsPeer2Peer: return MTP_inputPrivacyKeyPhoneP2P(); + case Key::Forwards: return MTP_inputPrivacyKeyForwards(); + case Key::ProfilePhoto: return MTP_inputPrivacyKeyProfilePhoto(); + case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages(); + case Key::About: return MTP_inputPrivacyKeyAbout(); } Unexpected("Key in Api::UserPrivacy::KetToTL."); } @@ -214,6 +213,8 @@ std::optional TLToKey(mtpTypeId type) { case mtpc_inputPrivacyKeyProfilePhoto: return Key::ProfilePhoto; case mtpc_privacyKeyVoiceMessages: case mtpc_inputPrivacyKeyVoiceMessages: return Key::Voices; + case mtpc_privacyKeyAbout: + case mtpc_inputPrivacyKeyAbout: return Key::About; } return std::nullopt; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 39d8025b5..be8e452d9 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -29,10 +29,12 @@ public: Forwards, ProfilePhoto, Voices, + About, }; enum class Option { Everyone, Contacts, + CloseFriends, Nobody, }; struct Rule { diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 6fc8800b9..f32358c06 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/who_reacted_context_action.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Api { namespace { @@ -357,37 +358,6 @@ struct State { }); } -[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now) { - if (!date) { - return {}; - } - const auto parsed = base::unixtime::parse(date); - const auto readDate = parsed.date(); - const auto nowDate = now.date(); - if (readDate == nowDate) { - return tr::lng_mediaview_today( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } else if (readDate.addDays(1) == nowDate) { - return tr::lng_mediaview_yesterday( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } - return tr::lng_mediaview_date_time( - tr::now, - lt_date, - tr::lng_month_day( - tr::now, - lt_month, - Lang::MonthDay(readDate.month())(tr::now), - lt_day, - QString::number(readDate.day())), - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); -} - bool UpdateUserpics( not_null state, not_null item, @@ -614,6 +584,37 @@ rpl::producer WhoReacted( } // namespace +QString FormatReadDate(TimeId date, const QDateTime &now) { + if (!date) { + return {}; + } + const auto parsed = base::unixtime::parse(date); + const auto readDate = parsed.date(); + const auto nowDate = now.date(); + if (readDate == nowDate) { + return tr::lng_mediaview_today( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } else if (readDate.addDays(1) == nowDate) { + return tr::lng_mediaview_yesterday( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } + return tr::lng_mediaview_date_time( + tr::now, + lt_date, + tr::lng_month_day( + tr::now, + lt_month, + Lang::MonthDay(readDate.month())(tr::now), + lt_day, + QString::number(readDate.day())), + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); +} + bool WhoReadExists(not_null item) { if (!item->out()) { return false; diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index 3fdcd8a56..9a9100535 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -29,6 +29,7 @@ enum class WhoReactedList { One, }; +[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now); [[nodiscard]] bool WhoReadExists(not_null item); [[nodiscard]] bool WhoReactedExists( not_null item, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 6348f1718..a469e839e 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_scheduled_messages.h" #include "data/data_channel_admins.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" @@ -765,9 +766,7 @@ QString ApiWrap::exportDirectMessageLink( channel->inputChannel, MTP_int(item->id) )).done([=](const MTPExportedMessageLink &result) { - const auto link = result.match([&](const auto &data) { - return qs(data.vlink()); - }); + const auto link = qs(result.data().vlink()); if (current != link) { _unlikelyMessageLinks.emplace_or_assign(itemId, link); } @@ -775,6 +774,32 @@ QString ApiWrap::exportDirectMessageLink( return current; } +QString ApiWrap::exportDirectStoryLink(not_null story) { + const auto storyId = story->fullId(); + const auto user = story->peer()->asUser(); + Assert(user != nullptr); + const auto fallback = [&] { + const auto base = user->username(); + const auto story = QString::number(storyId.story); + const auto query = base + "/s/" + story; + return session().createInternalLinkFull(query); + }; + const auto i = _unlikelyStoryLinks.find(storyId); + const auto current = (i != end(_unlikelyStoryLinks)) + ? i->second + : fallback(); + request(MTPstories_ExportStoryLink( + story->peer()->asUser()->inputUser, + MTP_int(story->id()) + )).done([=](const MTPExportedStoryLink &result) { + const auto link = qs(result.data().vlink()); + if (current != link) { + _unlikelyStoryLinks.emplace_or_assign(storyId, link); + } + }).send(); + return current; +} + void ApiWrap::requestContacts() { if (_session->data().contactsLoaded().current() || _contactsRequestId) { return; @@ -1806,6 +1831,11 @@ void ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound())); _notifySettingRequests.erase(key); }).send(); @@ -2511,6 +2541,15 @@ void ApiWrap::refreshFileReference( request(MTPaccount_GetSavedRingtones(MTP_long(0))); }, [&](Data::FileOriginPremiumPreviews data) { request(MTPhelp_GetPremiumPromo()); + }, [&](Data::FileOriginStory data) { + const auto user = _session->data().peer(data.peerId)->asUser(); + if (user) { + request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(1, MTP_int(data.storyId)))); + } else { + fail(); + } }, [&](v::null_t) { fail(); }); @@ -3093,8 +3132,9 @@ void ApiWrap::sharedMediaDone( void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled && !action.replaceMediaOf) { - const auto topic = action.topicRootId - ? action.history->peer->forumTopicFor(action.topicRootId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topic = topicRootId + ? action.history->peer->forumTopicFor(topicRootId) : nullptr; if (topic) { topic->readTillEnd(); @@ -3108,12 +3148,13 @@ void ApiWrap::sendAction(const SendAction &action) { void ApiWrap::finishForwarding(const SendAction &action) { const auto history = action.history; - auto toForward = history->resolveForwardDraft(action.topicRootId); + const auto topicRootId = action.replyTo.topicRootId; + auto toForward = history->resolveForwardDraft(topicRootId); if (!toForward.items.empty()) { const auto error = GetErrorTextForSending( history->peer, { - .topicRootId = action.topicRootId, + .topicRootId = topicRootId, .forward = &toForward.items, }); if (!error.isEmpty()) { @@ -3121,7 +3162,7 @@ void ApiWrap::finishForwarding(const SendAction &action) { } forwardMessages(std::move(toForward), action); - history->setForwardDraft(action.topicRootId, {}); + history->setForwardDraft(topicRootId, {}); } _session->data().sendHistoryChangeNotifications(); @@ -3165,31 +3206,33 @@ void ApiWrap::forwardMessages( const auto silentPost = ShouldSendSilent(peer, action.options); const auto sendAs = action.options.sendAs; + using SendFlag = MTPmessages_ForwardMessages::Flag; auto flags = MessageFlags(); - auto sendFlags = MTPmessages_ForwardMessages::Flags(0); + auto sendFlags = SendFlag() | SendFlag(); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_ForwardMessages::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (draft.options != Data::ForwardOptions::PreserveInfo) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_author; + sendFlags |= SendFlag::f_drop_author; } if (draft.options == Data::ForwardOptions::NoNamesAndCaptions) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_media_captions; + sendFlags |= SendFlag::f_drop_media_captions; } if (sendAs) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_send_as; + sendFlags |= SendFlag::f_send_as; } const auto kGeneralId = Data::ForumTopic::kGeneralId; - const auto topMsgId = (action.topicRootId == kGeneralId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topMsgId = (topicRootId == kGeneralId) ? MsgId(0) - : action.topicRootId; + : topicRootId; if (topMsgId) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_top_msg_id; + sendFlags |= SendFlag::f_top_msg_id; } auto forwardFrom = draft.items.front()->history()->peer; @@ -3550,14 +3593,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { action.generateLocal = true; sendAction(action); - const auto replyToId = action.replyTo; + const auto replyToId = action.replyTo.msgId; const auto replyTo = replyToId ? peer->owner().message(peer, replyToId) : nullptr; const auto topicRootId = replyTo ? replyTo->topicRootId() - : action.topicRootId - ? action.topicRootId + : action.replyTo.topicRootId + ? action.replyTo.topicRootId : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) @@ -3596,10 +3639,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { auto sendFlags = MTPmessages_SendMessage::Flags(0); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id; - if (action.topicRootId) { - sendFlags |= MTPmessages_SendMessage::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(action); MTPMessageMedia media = MTP_messageMediaEmpty(); @@ -3626,7 +3666,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_entities; } const auto clearCloudDraft = action.clearDraft; - const auto topicRootId = action.topicRootId; + const auto topicRootId = action.replyTo.topicRootId; if (clearCloudDraft) { sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft; history->clearCloudDraft(topicRootId); @@ -3663,13 +3703,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) { histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), msgText, MTP_long(randomId), MTPReplyMarkup(), @@ -3758,29 +3796,29 @@ void ApiWrap::sendInlineResult( ? (*localMessageId) : _session->data().nextLocalMessageId()); const auto randomId = base::RandomValue(); - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; + using SendFlag = MTPmessages_SendInlineBotResult::Flag; auto flags = NewMessageFlags(peer); - auto sendFlags = MTPmessages_SendInlineBotResult::Flag::f_clear_draft | 0; + auto sendFlags = SendFlag::f_clear_draft | SendFlag(); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_top_msg_id; - } + sendFlags |= SendFlag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, action.options); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (action.options.hideViaBot) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_hide_via; + sendFlags |= SendFlag::f_hide_via; } const auto sendAs = action.options.sendAs; @@ -3814,13 +3852,11 @@ void ApiWrap::sendInlineResult( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_long(randomId), MTP_long(data->getQueryId()), MTP_string(data->getId()), @@ -3942,8 +3978,7 @@ void ApiWrap::sendMediaWithRandomId( } const auto history = item->history(); - const auto replyTo = item->replyToId(); - const auto topicRootId = item->topicRootId(); + const auto replyTo = item->replyTo(); auto caption = item->originalText(); TextUtilities::Trim(caption); @@ -3956,8 +3991,7 @@ void ApiWrap::sendMediaWithRandomId( using Flag = MTPmessages_SendMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, options) ? Flag::f_silent : Flag(0)) @@ -3971,13 +4005,11 @@ void ApiWrap::sendMediaWithRandomId( histories.sendPreparedMessage( history, replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), media, MTP_string(caption.text), MTP_long(randomId), @@ -4068,13 +4100,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { return; } const auto history = sample->history(); - const auto replyTo = sample->replyToId(); - const auto topicRootId = sample->topicRootId(); + const auto replyTo = sample->replyTo(); const auto sendAs = album->options.sendAs; using Flag = MTPmessages_SendMultiMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, album->options) ? Flag::f_silent : Flag(0)) @@ -4085,13 +4115,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { histories.sendPreparedMessage( history, replyTo, - topicRootId, uint64(0), // randomId Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_vector(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()) @@ -4114,7 +4142,6 @@ FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const { peer->id, action.options, action.replyTo, - action.topicRootId, action.replaceMediaOf); } diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index e747f0f07..dcab7cde8 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -35,6 +35,7 @@ enum class StickersType : uchar; class Forum; class ForumTopic; class Thread; +class Story; } // namespace Data namespace InlineBots { @@ -160,6 +161,7 @@ public: QString exportDirectMessageLink( not_null item, bool inRepliesContext); + QString exportDirectStoryLink(not_null item); void requestContacts(); void requestDialogs(Data::Folder *folder = nullptr); @@ -707,5 +709,6 @@ private: base::flat_map, Fn> _botCommonGroupsRequests; base::flat_map _unlikelyMessageLinks; + base::flat_map _unlikelyStoryLinks; }; diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp index e1262b4c9..a7b92aa32 100644 --- a/Telegram/SourceFiles/boxes/background_box.cpp +++ b/Telegram/SourceFiles/boxes/background_box.cpp @@ -282,9 +282,9 @@ void BackgroundBox::chosen(const Data::WallPaper &paper) { close(); }); _controller->show(Ui::MakeConfirmBox({ - .text = u"Are you sure you want to reset the wallpaper?"_q, + .text = tr::lng_background_sure_reset_default(), .confirmed = reset, - .confirmText = u"Reset"_q, + .confirmText = tr::lng_background_reset_default(), })); } else { closeBox(); diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 738a88165..c83af953d 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -95,7 +95,7 @@ constexpr auto kDefaultDimming = 50; const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto groupedId = uint64(); const auto item = history->makeMessage( diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 54f915a45..3758d8a61 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -24,6 +24,11 @@ UserpicButton { uploadIcon: icon; uploadIconPosition: point; } +ShortInfoBox { + label: FlatLabel; + labeled: FlatLabel; + labeledOneLine: FlatLabel; +} countryRowHeight: 36px; countryRowNameFont: semiboldFont; @@ -344,11 +349,6 @@ autoDownloadLimitSlider: MediaSlider(defaultContinuousSlider) { } autoDownloadLimitPadding: margins(22px, 8px, 22px, 8px); -confirmCaptionArea: InputField(defaultInputField) { - textMargins: margins(1px, 26px, 31px, 4px); - heightMax: 158px; -} -confirmBg: windowBgOver; confirmMaxHeight: 245px; supportInfoField: InputField(defaultInputField) { @@ -391,51 +391,11 @@ sendMediaPreviewSize: 308px; sendMediaPreviewHeightMax: 1280; sendMediaRowSkip: 10px; -editMediaButtonSize: 32px; - -editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; -editMediaButton: IconButton(defaultIconButton) { - width: editMediaButtonSize; - height: editMediaButtonSize; - - icon: editMediaButtonIconFile; - - rippleAreaSize: editMediaButtonSize; - ripple: defaultRippleAnimation; -} - editMediaHintLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; minWidth: sendMediaPreviewSize; } -// SendFilesBox - -sendBoxAlbumGroupEditInternalSkip: 8px; -sendBoxAlbumGroupSkipRight: 5px; -sendBoxAlbumGroupSkipTop: 5px; -sendBoxAlbumGroupRadius: 4px; -sendBoxAlbumGroupSize: size(62px, 25px); -sendBoxAlbumSmallGroupSize: size(30px, 25px); - -sendBoxFileGroupSkipTop: 2px; -sendBoxFileGroupSkipRight: 5px; -sendBoxFileGroupEditInternalSkip: -1px; - -sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgRipple; - } -} -sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; -sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; - -sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; -sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; -sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; - -// End of SendFilesBox - calendarTitleHeight: boxTitleHeight; calendarPrevious: IconButton { width: calendarTitleHeight; @@ -982,6 +942,27 @@ requestsBoxList: PeerList(peerListBox) { padding: margins(0px, 12px, 0px, 12px); item: requestsBoxItem; } +contactsWithStories: PeerList(peerListBox) { + padding: margins(0px, 0px, 0px, 0px); + item: PeerListItem(peerListBoxItem) { + height: 52px; + photoPosition: point(18px, 5px); + namePosition: point(70px, 7px); + statusPosition: point(70px, 27px); + + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; + } +} +storiesReadLineTwice: 2px; +storiesUnreadLineTwice: 4px; requestsAcceptButton: RoundButton(defaultActiveButton) { width: -28px; height: 30px; @@ -1006,3 +987,26 @@ ringtonesBoxSkip: 7px; gradientButtonGlareDuration: 700; gradientButtonGlareTimeout: 2000; gradientButtonGlareWidth: 100px; + +infoLabeledOneLine: FlatLabel(defaultFlatLabel) { + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + lineHeight: 19px; + } + margin: margins(5px, 5px, 5px, 5px); +} +infoLabelSkip: 2px; +infoLabeled: FlatLabel(infoLabeledOneLine) { + minWidth: 180px; + maxHeight: 0px; + margin: margins(5px, 5px, 5px, 5px); +} +infoLabel: FlatLabel(infoLabeled) { + textFg: windowSubTextFg; +} + +shortInfoBox: ShortInfoBox { + label: infoLabel; + labeled: infoLabeled; + labeledOneLine: infoLabeledOneLine; +} diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 41f19a0b8..c9779b1f1 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -246,12 +246,12 @@ EditCaptionBox::EditCaptionBox( , _scroll(base::make_unique_q(this, st::boxScroll)) , _field(base::make_unique_q( this, - st::confirmCaptionArea, + st::defaultComposeFiles.caption, Ui::InputField::Mode::MultiLine, tr::lng_photo_caption())) , _emojiToggle(base::make_unique_q( this, - st::boxAttachEmoji)) + st::defaultComposeFiles.emoji)) , _initialText(std::move(text)) , _initialList(std::move(list)) , _saved(std::move(saved)) { @@ -402,6 +402,7 @@ void EditCaptionBox::rebuildPreview() { if (photo || document->isVideoFile() || document->isAnimation()) { const auto media = Ui::CreateChild( this, + st::defaultComposeControls, gifPaused, _historyItem, Ui::AttachControls::Type::EditOnly); @@ -410,6 +411,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, _historyItem, Ui::AttachControls::Type::EditOnly)); } @@ -418,6 +420,7 @@ void EditCaptionBox::rebuildPreview() { const auto media = Ui::SingleMediaPreview::Create( this, + st::defaultComposeControls, gifPaused, file, Ui::AttachControls::Type::EditOnly); @@ -429,6 +432,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, file, Ui::AttachControls::Type::EditOnly)); } @@ -482,7 +486,7 @@ void EditCaptionBox::setupField() { _field->setSubmitSettings( Core::App().settings().sendSubmitWay()); - _field->setMaxHeight(st::confirmCaptionArea.heightMax); + _field->setMaxHeight(st::defaultComposeFiles.caption.heightMax); connect(_field, &Ui::InputField::submitted, [=] { save(); }); connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); }); @@ -596,7 +600,7 @@ void EditCaptionBox::setupPhotoEditorEventHandler() { if (!_preparedList.files.empty()) { Editor::OpenWithPreparedFile( this, - controller, + controller->uiShow(), &_preparedList.files.front(), st::sendMediaPreviewSize, [=] { rebuildPreview(); }); @@ -845,7 +849,8 @@ bool EditCaptionBox::validateLength(const QString &text) const { if (remove <= 0) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _controller->show( + Box(CaptionLimitReachedBox, session, remove, nullptr)); return false; } diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index dfa6b3707..51be4cd11 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -112,10 +112,16 @@ std::unique_ptr PrivacyExceptionsBoxControl } // namespace +bool EditPrivacyController::hasOption(Option option) const { + return (option != Option::CloseFriends); +} + QString EditPrivacyController::optionLabel(Option option) const { switch (option) { case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now); case Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now); + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); case Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now); } Unexpected("Option value in optionsLabelKey."); @@ -182,10 +188,12 @@ bool EditPrivacyBox::showExceptionLink(Exception exception) const { switch (exception) { case Exception::Always: return (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends) || (_value.option == Option::Nobody); case Exception::Never: return (_value.option == Option::Everyone) - || (_value.option == Option::Contacts); + || (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends); } Unexpected("Invalid exception value."); } @@ -326,6 +334,7 @@ void EditPrivacyBox::setupContent() { { 0, st::settingsPrivacySkipTop, 0, 0 }); addOptionRow(Option::Everyone); addOptionRow(Option::Contacts); + addOptionRow(Option::CloseFriends); addOptionRow(Option::Nobody); const auto warning = addLabelOrDivider( content, diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 6a78c41ae..d9ba7dab9 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -41,9 +41,7 @@ public: [[nodiscard]] virtual Key key() const = 0; [[nodiscard]] virtual rpl::producer title() const = 0; - [[nodiscard]] virtual bool hasOption(Option option) const { - return true; - } + [[nodiscard]] virtual bool hasOption(Option option) const; [[nodiscard]] virtual rpl::producer optionsTitleKey() const = 0; [[nodiscard]] virtual QString optionLabel(Option option) const; [[nodiscard]] virtual rpl::producer warning() const { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 8e393c583..6cce77d60 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -309,7 +309,7 @@ object_ptr CreatePeerListSectionSubtitle( rpl::producer text) { auto result = object_ptr( parent, - st::searchedBarHeight); + st::windowFilterChatsSectionSubtitleHeight); const auto raw = result.data(); raw->paintRequest( diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 66f10501a..6f27553d0 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -10,13 +10,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/session_show.h" #include "main/main_session.h" #include "mainwidget.h" +#include "ui/effects/loading_element.h" +#include "ui/effects/outline_segments.h" +#include "ui/effects/round_checkbox.h" +#include "ui/effects/ripple_animation.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" -#include "ui/effects/loading_element.h" -#include "ui/effects/round_checkbox.h" -#include "ui/effects/ripple_animation.h" #include "ui/empty_userpic.h" #include "ui/wrap/slide_wrap.h" #include "ui/text/text_options.h" @@ -33,8 +34,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" #include "styles/style_widgets.h" -#include - PaintRoundImageCallback PaintUserpicCallback( not_null peer, bool respectSavedMessagesChat) { @@ -263,15 +262,23 @@ void PeerListBox::peerListSetRowChecked( not_null row, bool checked) { if (checked) { - addSelectItem(row, anim::type::normal); + if (_controller->trackSelectedList()) { + addSelectItem(row, anim::type::normal); + } PeerListContentDelegate::peerListSetRowChecked(row, checked); peerListUpdateRow(row); // This call deletes row from _searchRows. - _select->entity()->clearQuery(); + if (_select) { + _select->entity()->clearQuery(); + } } else { // The itemRemovedCallback will call changeCheckState() here. - _select->entity()->removeItem(row->id()); + if (_select) { + _select->entity()->removeItem(row->id()); + } else { + PeerListContentDelegate::peerListSetRowChecked(row, checked); + } peerListUpdateRow(row); } } @@ -882,9 +889,18 @@ void PeerListRow::createCheckbox( } void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { + Expects(!checked || _checkbox != nullptr); + + if (_checkbox) { + _checkbox->setChecked(checked, animated); + } +} + +void PeerListRow::setCustomizedCheckSegments( + std::vector segments) { Expects(_checkbox != nullptr); - _checkbox->setChecked(checked, animated); + _checkbox->setCustomizedSegments(std::move(segments)); } void PeerListRow::finishCheckedAnimation() { @@ -1126,6 +1142,24 @@ PeerListRow *PeerListContent::findRow(PeerListRowId id) { return (it == _rowsById.cend()) ? nullptr : it->second.get(); } +std::optional PeerListContent::lastRowMousePosition() const { + if (!_lastMousePosition) { + return std::nullopt; + } + const auto point = mapFromGlobal(*_lastMousePosition); + auto in = parentWidget()->rect().contains( + parentWidget()->mapFromGlobal(*_lastMousePosition)); + auto rowsPointY = point.y() - rowsTop(); + const auto index = (in + && rowsPointY >= 0 + && rowsPointY < shownRowsCount() * _rowHeight) + ? (rowsPointY / _rowHeight) + : -1; + return (index >= 0 && index == _selected.index.value) + ? QPoint(point.x(), rowsPointY) + : std::optional(); +} + void PeerListContent::removeRow(not_null row) { auto index = row->absoluteIndex(); auto isSearchResult = row->isSearchResult(); @@ -1254,6 +1288,9 @@ void PeerListContent::initDecorateWidget(Ui::RpWidget *widget) { }) | rpl::start_with_next([=] { mouseLeftGeometry(); }, widget->lifetime()); + widget->heightValue() | rpl::skip(1) | rpl::start_with_next([=] { + resizeToWidth(width()); + }, widget->lifetime()); } } @@ -1990,10 +2027,12 @@ void PeerListContent::setSearchQuery( bool PeerListContent::submitted() { if (const auto row = getRow(_selected.index)) { + _lastMousePosition = std::nullopt; _controller->rowClicked(row); return true; } else if (showingSearch()) { if (const auto row = getRow(RowIndex(0))) { + _lastMousePosition = std::nullopt; _controller->rowClicked(row); return true; } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 28cb69534..ac8ab554b 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -36,6 +36,7 @@ class SlideWrap; class FlatLabel; struct ScrollToRequest; class PopupMenu; +struct OutlineSegment; } // namespace Ui using PaintRoundImageCallback = Fn segments); void setHidden(bool hidden) { _hidden = hidden; } @@ -324,6 +327,7 @@ public: virtual void peerListScrollToTop() = 0; virtual int peerListFullRowsCount() = 0; virtual PeerListRow *peerListFindRow(PeerListRowId id) = 0; + virtual std::optional peerListLastRowMousePosition() = 0; virtual void peerListSortRows(Fn compare) = 0; virtual int peerListPartitionRows(Fn border) = 0; virtual void peerListShowBox( @@ -500,6 +504,9 @@ public: return delegate()->peerListIsRowChecked(row); } + virtual bool trackSelectedList() { + return true; + } virtual bool searchInLocal() { return true; } @@ -609,6 +616,7 @@ public: void prependRow(std::unique_ptr row); void prependRowFromSearchResult(not_null row); PeerListRow *findRow(PeerListRowId id); + std::optional lastRowMousePosition() const; void updateRow(not_null row) { updateRow(row, RowIndex()); } @@ -863,6 +871,9 @@ public: PeerListRow *peerListFindRow(PeerListRowId id) override { return _content->findRow(id); } + std::optional peerListLastRowMousePosition() override { + return _content->lastRowMousePosition(); + } void peerListUpdateRow(not_null row) override { _content->updateRow(row); } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 0679d4dc9..5831ec0b5 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -9,12 +9,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "base/random.h" +#include "boxes/filters/edit_filter_chats_list.h" #include "ui/boxes/confirm_box.h" +#include "ui/effects/round_checkbox.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/padding_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" @@ -31,12 +37,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" +#include "ui/effects/outline_segments.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" // showAddContact() #include "base/unixtime.h" #include "styles/style_boxes.h" #include "styles/style_profile.h" #include "styles/style_dialogs.h" +#include "data/data_stories.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" + namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); @@ -49,21 +61,26 @@ object_ptr PrepareContactsBox( using Mode = ContactsBoxController::SortMode; auto controller = std::make_unique( &sessionController->session()); + controller->setStyleOverrides(&st::contactsWithStories); + controller->setStoriesShown(true); const auto raw = controller.get(); auto init = [=](not_null box) { struct State { - QPointer toggleSort; - Mode mode = ContactsBoxController::SortMode::Online; + QPointer<::Ui::IconButton> toggleSort; + rpl::variable mode = Mode::Online; + ::Ui::Animations::Simple scrollAnimation; }; + const auto state = box->lifetime().make_state(); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); box->addLeftButton( tr::lng_profile_add_contact(), [=] { sessionController->showAddContact(); }); state->toggleSort = box->addTopButton(st::contactsSortButton, [=] { - const auto online = (state->mode == Mode::Online); - state->mode = online ? Mode::Alphabet : Mode::Online; - raw->setSortMode(state->mode); + const auto online = (state->mode.current() == Mode::Online); + const auto mode = online ? Mode::Alphabet : Mode::Online; + state->mode = mode; + raw->setSortMode(mode); state->toggleSort->setIconOverride( online ? &st::contactsSortOnlineIcon : nullptr, online ? &st::contactsSortOnlineIconOver : nullptr); @@ -73,6 +90,39 @@ object_ptr PrepareContactsBox( return Box(std::move(controller), std::move(init)); } +QBrush PeerListStoriesGradient(const style::PeerList &st) { + const auto left = st.item.photoPosition.x(); + const auto top = st.item.photoPosition.y(); + const auto size = st.item.photoSize; + return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size)); +} + +std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush) { + Expects(unread <= count); + Expects(count > 0); + + auto result = std::vector(); + const auto add = [&](bool unread) { + result.push_back({ + .brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b, + .width = (unread + ? st::dialogsStoriesFull.lineTwice / 2. + : st::dialogsStoriesFull.lineReadTwice / 2.), + }); + }; + result.reserve(count); + for (auto i = 0, till = count - unread; i != till; ++i) { + add(false); + } + for (auto i = 0; i != unread; ++i) { + add(true); + } + return result; +} + void PeerListRowWithLink::setActionLink(const QString &action) { _action = action; refreshActionLink(); @@ -308,6 +358,115 @@ bool ChatsListBoxController::appendRow(not_null history) { return false; } +PeerListStories::PeerListStories( + not_null controller, + not_null session) +: _controller(controller) +, _session(session) { +} + +void PeerListStories::updateColors() { + for (auto i = begin(_counts); i != end(_counts); ++i) { + if (const auto row = _delegate->peerListFindRow(i->first)) { + if (i->second.count >= 0 && i->second.unread >= 0) { + applyForRow(row, i->second.count, i->second.unread, true); + } + } + } +} + +void PeerListStories::updateFor( + uint64 id, + int count, + int unread) { + if (const auto row = _delegate->peerListFindRow(id)) { + applyForRow(row, count, unread); + _delegate->peerListUpdateRow(row); + } +} + +void PeerListStories::process(not_null row) { + const auto user = row->peer()->asUser(); + if (!user) { + return; + } + const auto stories = &_session->data().stories(); + const auto source = stories->source(user->id); + const auto count = source + ? int(source->ids.size()) + : user->hasActiveStories() + ? 1 + : 0; + const auto unread = source + ? source->info().unreadCount + : user->hasUnreadStories() + ? 1 + : 0; + applyForRow(row, count, unread, true); +} + +bool PeerListStories::handleClick(not_null peer) { + const auto point = _delegate->peerListLastRowMousePosition(); + const auto &st = _controller->listSt()->item; + if (point && point->x() < st.photoPosition.x() + st.photoSize) { + if (const auto window = peer->session().tryResolveWindow()) { + if (const auto user = peer->asUser()) { + if (user->hasActiveStories()) { + window->openPeerStories(peer->id); + return true; + } + } + } + } + return false; +} + +void PeerListStories::prepare(not_null delegate) { + _delegate = delegate; + + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + style::PaletteChanged() | rpl::start_with_next([=] { + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + updateColors(); + }, _lifetime); + + _session->changes().peerUpdates( + Data::PeerUpdate::Flag::StoriesState + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + const auto id = update.peer->id.value; + if (const auto row = _delegate->peerListFindRow(id)) { + process(row); + } + }, _lifetime); + + const auto stories = &_session->data().stories(); + stories->sourceChanged() | rpl::start_with_next([=](PeerId id) { + const auto source = stories->source(id); + const auto info = source + ? source->info() + : Data::StoriesSourceInfo(); + updateFor(id.value, info.count, info.unreadCount); + }, _lifetime); +} + +void PeerListStories::applyForRow( + not_null row, + int count, + int unread, + bool force) { + auto &counts = _counts[row->id()]; + if (!force && counts.count == count && counts.unread == unread) { + return; + } + counts.count = count; + counts.unread = unread; + _delegate->peerListSetRowChecked(row, count > 0); + if (count > 0) { + row->setCustomizedCheckSegments( + PeerListStoriesSegments(count, unread, _unreadBrush)); + } +} + ContactsBoxController::ContactsBoxController( not_null session) : ContactsBoxController( @@ -334,6 +493,10 @@ void ContactsBoxController::prepare() { prepareViewHook(); + if (_stories) { + _stories->prepare(delegate()); + } + session().data().contactsLoaded().value( ) | rpl::start_with_next([=] { rebuildRows(); @@ -378,8 +541,10 @@ std::unique_ptr ContactsBoxController::createSearchRow( void ContactsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); - if (const auto window = peer->session().tryResolveWindow()) { - window->showPeerHistory(row->peer()); + if (_stories && _stories->handleClick(peer)) { + return; + } else if (const auto window = peer->session().tryResolveWindow()) { + window->showPeerHistory(peer); } } @@ -404,6 +569,10 @@ void ContactsBoxController::setSortMode(SortMode mode) { } } +void ContactsBoxController::setStoriesShown(bool shown) { + _stories = std::make_unique(this, _session); +} + void ContactsBoxController::sort() { switch (_sortMode) { case SortMode::Alphabet: sortByName(); break; @@ -449,7 +618,11 @@ bool ContactsBoxController::appendRow(not_null user) { return false; } if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } return true; } return false; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 00106c392..0ba108f01 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -20,12 +20,21 @@ class Forum; class ForumTopic; } // namespace Data +namespace Ui { +struct OutlineSegment; +} // namespace Ui + namespace Window { class SessionController; } // namespace Window [[nodiscard]] object_ptr PrepareContactsBox( not_null sessionController); +[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st); +[[nodiscard]] std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush); class PeerListRowWithLink : public PeerListRow { public: @@ -116,6 +125,41 @@ private: }; +class PeerListStories final { +public: + PeerListStories( + not_null controller, + not_null session); + + void prepare(not_null delegate); + + void process(not_null row); + bool handleClick(not_null peer); + +private: + struct Counts { + int count = 0; + int unread = 0; + }; + + void updateColors(); + void updateFor(uint64 id, int count, int unread); + void applyForRow( + not_null row, + int count, + int unread, + bool force = false); + + const not_null _controller; + const not_null _session; + PeerListDelegate *_delegate = nullptr; + + QBrush _unreadBrush; + base::flat_map _counts; + rpl::lifetime _lifetime; + +}; + class ContactsBoxController : public PeerListController { public: explicit ContactsBoxController(not_null session); @@ -128,12 +172,16 @@ public: [[nodiscard]] std::unique_ptr createSearchRow( not_null peer) override final; void rowClicked(not_null row) override; + bool trackSelectedList() override { + return !_stories; + } enum class SortMode { Alphabet, Online, }; void setSortMode(SortMode mode); + void setStoriesShown(bool shown); protected: virtual std::unique_ptr createRow(not_null user); @@ -155,6 +203,8 @@ private: base::Timer _sortByOnlineTimer; rpl::lifetime _sortByOnlineLifetime; + std::unique_ptr _stories; + }; class ChooseRecipientBoxController diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 94caec150..e1d09efec 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -25,11 +25,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_indexed_list.h" #include "data/data_peer_values.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_changes.h" #include "base/unixtime.h" +#include "ui/effects/outline_segments.h" #include "ui/widgets/popup_menu.h" #include "ui/ui_utility.h" #include "info/profile/info_profile_values.h" @@ -900,7 +902,11 @@ void ParticipantsBoxController::setupListChangeViewers() { return (row.peer() == user); }); } else if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } refreshRows(); if (_onlineSorter) { _onlineSorter->sort(); @@ -1159,9 +1165,19 @@ void ParticipantsBoxController::restoreState( if (my->wasLoading) { loadMoreRows(); } + const auto was = _fullCountValue.current(); PeerListController::restoreState(std::move(state)); - if (delegate()->peerListFullRowsCount() > 0 || _allLoaded) { + const auto now = delegate()->peerListFullRowsCount(); + if (now > 0 || _allLoaded) { refreshDescription(); + if (_stories) { + for (auto i = 0; i != now; ++i) { + _stories->process(delegate()->peerListRowAt(i)); + } + } + if (now != was) { + refreshRows(); + } } if (_onlineSorter) { _onlineSorter->sort(); @@ -1177,14 +1193,21 @@ rpl::producer ParticipantsBoxController::fullCountValue() const { return _fullCountValue.value(); } +void ParticipantsBoxController::setStoriesShown(bool shown) { + _stories = std::make_unique( + this, + &_navigation->session()); +} + void ParticipantsBoxController::prepare() { auto title = [&] { switch (_role) { case Role::Admins: return tr::lng_channel_admins(); case Role::Profile: - case Role::Members: return (_peer->isChannel() && !_peer->isMegagroup() - ? tr::lng_profile_subscribers_section() - : tr::lng_profile_participants_section()); + case Role::Members: + return ((_peer->isChannel() && !_peer->isMegagroup()) + ? tr::lng_profile_subscribers_section() + : tr::lng_profile_participants_section()); case Role::Restricted: return tr::lng_exceptions_list_title(); case Role::Kicked: return tr::lng_removed_list_title(); } @@ -1207,6 +1230,10 @@ void ParticipantsBoxController::prepare() { setDescriptionText(tr::lng_contacts_loading(tr::now)); setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); + if (_stories) { + _stories->prepare(delegate()); + } + if (_role == Role::Profile) { auto visible = _peer->isMegagroup() ? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup()) @@ -1318,8 +1345,14 @@ void ParticipantsBoxController::rebuildChatParticipants( } } for (const auto &user : participants) { - if (auto row = createRow(user)) { - delegate()->peerListAppendRow(std::move(row)); + if (!delegate()->peerListFindRow(user->id.value)) { + if (auto row = createRow(user)) { + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } + } } } _onlineSorter->sort(); @@ -1373,7 +1406,11 @@ void ParticipantsBoxController::rebuildChatAdmins( } for (const auto user : list) { if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } } } @@ -1543,6 +1580,11 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() { void ParticipantsBoxController::rowClicked(not_null row) { const auto participant = row->peer(); const auto user = participant->asUser(); + + if (_stories && _stories->handleClick(participant)) { + return; + } + if (_role == Role::Admins) { Assert(user != nullptr); showAdmin(user); @@ -1890,7 +1932,11 @@ bool ParticipantsBoxController::appendRow(not_null participant) { recomputeTypeFor(participant); return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } @@ -1906,10 +1952,17 @@ bool ParticipantsBoxController::prependRow(not_null participant) { if (_role == Role::Admins) { // Perhaps we've added a new admin from search. delegate()->peerListPrependRowFromSearchResult(row); + if (_stories) { + _stories->process(row); + } } return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 30b445813..0882f214a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "info/profile/info_profile_members_controllers.h" +class PeerListStories; struct ChatAdminRightsInfo; struct ChatRestrictionsInfo; @@ -174,6 +175,9 @@ public: QWidget *parent, not_null row) override; void loadMoreRows() override; + bool trackSelectedList() override { + return !_stories; + } void peerListSearchAddRow(not_null peer) override; std::unique_ptr createSearchRow( @@ -187,6 +191,8 @@ public: [[nodiscard]] rpl::producer onlineCountValue() const; [[nodiscard]] rpl::producer fullCountValue() const; + void setStoriesShown(bool shown); + protected: // Allow child controllers not providing navigation. // This is their responsibility to override all methods that use it. @@ -288,6 +294,8 @@ private: Ui::BoxPointer _addBox; QPointer _editParticipantBox; + std::unique_ptr _stories; + }; // Members, banned and restricted users server side search. diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index 619542edb..1bf236db1 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_player.h" #include "base/event_filter.h" #include "lang/lang_keys.h" +#include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_info.h" @@ -614,8 +615,10 @@ PeerShortInfoBox::PeerShortInfoBox( rpl::producer fields, rpl::producer status, rpl::producer userpic, - Fn videoPaused) -: _type(type) + Fn videoPaused, + const style::ShortInfoBox *stOverride) +: _st(stOverride ? *stOverride : st::shortInfoBox) +, _type(type) , _fields(std::move(fields)) , _topRoundBackground(this) , _scroll(this, st::shortInfoScroll) @@ -691,11 +694,12 @@ void PeerShortInfoBox::prepareRows() { auto addInfoLineGeneric = [&]( rpl::producer &&label, rpl::producer &&text, - const style::FlatLabel &textSt = st::infoLabeled) { + const style::FlatLabel &textSt) { auto line = CreateTextWithLabel( _rows, rpl::duplicate(label) | Ui::Text::ToWithEntities(), rpl::duplicate(text), + _st.label, textSt, st::shortInfoLabeledPadding); _rows->add(object_ptr( @@ -715,7 +719,7 @@ void PeerShortInfoBox::prepareRows() { auto addInfoLine = [&]( rpl::producer &&label, rpl::producer &&text, - const style::FlatLabel &textSt = st::infoLabeled) { + const style::FlatLabel &textSt) { return addInfoLineGeneric( std::move(label), std::move(text), @@ -728,7 +732,7 @@ void PeerShortInfoBox::prepareRows() { auto result = addInfoLine( std::move(label), std::move(text), - st::infoLabeledOneLine); + _st.labeledOneLine); result->setDoubleClickSelectsParagraph(true); result->setContextCopyText(contextCopyText); return result; @@ -744,7 +748,7 @@ void PeerShortInfoBox::prepareRows() { auto label = _fields.current().isBio ? tr::lng_info_bio_label() : tr::lng_info_about_label(); - addInfoLine(std::move(label), aboutValue()); + addInfoLine(std::move(label), aboutValue(), _st.labeled); addInfoOneLine( tr::lng_info_username_label(), usernameValue() | Ui::Text::ToWithEntities(), diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index fc29b7062..07e60132a 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct ShortInfoCover; +struct ShortInfoBox; } // namespace style namespace Media::Streaming { @@ -144,7 +145,8 @@ public: rpl::producer fields, rpl::producer status, rpl::producer userpic, - Fn videoPaused); + Fn videoPaused, + const style::ShortInfoBox *stOverride); ~PeerShortInfoBox(); [[nodiscard]] rpl::producer<> openRequests() const; @@ -166,6 +168,7 @@ private: [[nodiscard]] rpl::producer usernameValue() const; [[nodiscard]] rpl::producer aboutValue() const; + const style::ShortInfoBox &_st; const PeerShortInfoType _type = PeerShortInfoType::User; rpl::variable _fields; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index a39b7eb47..8baf378b5 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -420,7 +420,8 @@ bool ProcessCurrent( object_ptr PrepareShortInfoBox( not_null peer, Fn open, - Fn videoPaused) { + Fn videoPaused, + const style::ShortInfoBox *stOverride) { const auto type = peer->isUser() ? PeerShortInfoType::User : peer->isBroadcast() @@ -432,7 +433,8 @@ object_ptr PrepareShortInfoBox( FieldsValue(peer), StatusValue(peer), std::move(userpic.value), - std::move(videoPaused)); + std::move(videoPaused), + stOverride); result->openRequests( ) | rpl::start_with_next(open, result->lifetime()); @@ -445,7 +447,8 @@ object_ptr PrepareShortInfoBox( object_ptr PrepareShortInfoBox( not_null peer, - not_null navigation) { + not_null navigation, + const style::ShortInfoBox *stOverride) { const auto open = [=] { navigation->showPeerHistory(peer); }; const auto videoIsPaused = [=] { return navigation->parentController()->isGifPausedAtLeastFor( @@ -454,7 +457,8 @@ object_ptr PrepareShortInfoBox( return PrepareShortInfoBox( peer, open, - videoIsPaused); + videoIsPaused, + stOverride); } rpl::producer PrepareShortInfoStatus(not_null peer) { diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h index f50a23bf7..327ce373b 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h @@ -13,6 +13,7 @@ class PeerData; namespace style { struct ShortInfoCover; +struct ShortInfoBox; } // namespace style namespace Ui { @@ -33,11 +34,13 @@ struct PreparedShortInfoUserpic { [[nodiscard]] object_ptr PrepareShortInfoBox( not_null peer, Fn open, - Fn videoPaused); + Fn videoPaused, + const style::ShortInfoBox *stOverride = nullptr); [[nodiscard]] object_ptr PrepareShortInfoBox( not_null peer, - not_null navigation); + not_null navigation, + const style::ShortInfoBox *stOverride = nullptr); [[nodiscard]] rpl::producer PrepareShortInfoStatus( not_null peer); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 55fbe0a35..3fbdc34c1 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -404,6 +404,7 @@ std::unique_ptr PublicsController::createRow( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, bool premiumPossible, rpl::producer title, @@ -411,6 +412,8 @@ void SimpleLimitBox( const QString &refAddition, const InfographicDescriptor &descriptor, bool fixed = false) { + const auto &st = stOverride ? *stOverride : st::defaultPremiumLimits; + box->setWidth(st::boxWideWidth); const auto top = fixed @@ -431,6 +434,7 @@ void SimpleLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st, descriptor.premiumLimit, descriptor.phrase, 0, @@ -473,6 +477,7 @@ void SimpleLimitBox( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, rpl::producer title, rpl::producer text, @@ -481,6 +486,7 @@ void SimpleLimitBox( bool fixed = false) { SimpleLimitBox( box, + stOverride, session, session->premiumPossible(), std::move(title), @@ -524,6 +530,7 @@ void SimplePinsLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filter_pin_limit_title(), std::move(text), @@ -561,6 +568,7 @@ void ChannelsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_channels_limit_title(), std::move(text), @@ -650,6 +658,7 @@ void PublicLinksLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_links_limit_title(), std::move(text), @@ -716,6 +725,7 @@ void FilterChatsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_filter_chats_limit_title(), std::move(text), @@ -753,6 +763,7 @@ void FilterLinksLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_filter_links_limit_title(), std::move(text), @@ -798,6 +809,7 @@ void FiltersLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filters_limit_title(), std::move(text), @@ -836,6 +848,7 @@ void ShareableFiltersLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filter_shared_limit_title(), std::move(text), @@ -900,6 +913,7 @@ void ForumPinsLimitBox( Ui::Text::RichLangValue); SimpleLimitBox( box, + nullptr, &forum->session(), false, tr::lng_filter_pin_limit_title(), @@ -911,7 +925,8 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); @@ -943,6 +958,7 @@ void CaptionLimitBox( SimpleLimitBox( box, + stOverride, session, tr::lng_caption_limit_title(), std::move(text), @@ -953,15 +969,17 @@ void CaptionLimitBox( void CaptionLimitReachedBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ .text = tr::lng_caption_limit_reached(tr::now, lt_count, remove), + .labelStyle = stOverride ? &stOverride->boxLabel : nullptr, .inform = true, }); if (!session->premium()) { box->addLeftButton(tr::lng_limits_increase(), [=] { box->getDelegate()->showBox( - Box(CaptionLimitBox, session, remove), + Box(CaptionLimitBox, session, remove, stOverride), Ui::LayerOption::KeepOther, anim::type::normal); box->closeBox(); @@ -972,7 +990,8 @@ void CaptionLimitReachedBox( void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes) { + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride) { const auto limits = Data::PremiumLimits(session); const auto defaultLimit = float64(limits.uploadMaxDefault()); const auto premiumLimit = float64(limits.uploadMaxPremium()); @@ -1011,6 +1030,7 @@ void FileSizeLimitBox( SimpleLimitBox( box, + stOverride, session, premiumPossible, tr::lng_file_size_limit_title(), @@ -1084,6 +1104,7 @@ void AccountsLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st::defaultPremiumLimits, (QString::number(std::max(current, defaultLimit) + 1) + ((current + 1 == premiumLimit) ? "" : "+")), QString::number(defaultLimit)); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h index 55be7e40f..bb9115a3e 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.h +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" +namespace style { +struct PremiumLimits; +} // namespace style + namespace Data { class Forum; } // namespace Data @@ -57,15 +61,18 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void CaptionLimitReachedBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes); + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride = nullptr); void AccountsLimitBox( not_null box, not_null session); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index ffc5bc709..20581a726 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -76,7 +76,7 @@ bool operator==(const Descriptor &a, const Descriptor &b) { struct Preload { Descriptor descriptor; std::shared_ptr media; - base::weak_ptr controller; + std::weak_ptr show; }; [[nodiscard]] std::vector &Preloads() { @@ -168,7 +168,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickerPreview( not_null parent, - not_null controller, + std::shared_ptr show, const std::shared_ptr &media, Fn readyCallback = nullptr) { using namespace HistoryView; @@ -194,6 +194,8 @@ void PreloadSticker(const std::shared_ptr &media) { struct State { std::unique_ptr lottie; std::unique_ptr effect; + style::owned_color pathFg = style::owned_color( + QColor(255, 255, 255, 64)); std::unique_ptr pathGradient; bool readyInvoked = false; }; @@ -239,15 +241,17 @@ void PreloadSticker(const std::shared_ptr &media) { }; createLottieIfReady(); if (!state->lottie || !state->effect) { - controller->session().downloaderTaskFinished( + show->session().downloaderTaskFinished( ) | rpl::take_while([=] { createLottieIfReady(); return !state->lottie || !state->effect; }) | rpl::start(result->lifetime()); } - state->pathGradient = MakePathShiftGradient( - controller->chatStyle(), - [=] { result->update(); }); + state->pathGradient = std::make_unique( + st::shadowFg, + state->pathFg.color(), + [=] { result->update(); }, + rpl::never<>()); result->paintRequest( ) | rpl::start_with_next([=] { @@ -262,7 +266,7 @@ void PreloadSticker(const std::shared_ptr &media) { if (!state->lottie || !state->lottie->ready() || !state->effect->ready()) { - p.setBrush(controller->chatStyle()->msgServiceBg()); + p.setBrush(st::shadowFg); ChatHelpers::PaintStickerThumbnailPath( p, media.get(), @@ -302,7 +306,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickersPreview( not_null parent, - not_null controller, + std::shared_ptr show, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); @@ -327,7 +331,7 @@ void PreloadSticker(const std::shared_ptr &media) { bool nextReady = false; int index = 0; }; - const auto premium = &controller->session().api().premium(); + const auto premium = &show->session().api().premium(); const auto state = lifetime.make_state(); const auto create = [=](std::shared_ptr media) { const auto outer = Ui::CreateChild(result); @@ -340,7 +344,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[maybe_unused]] const auto sticker = StickerPreview( outer, - controller, + show, media, state->singleReadyCallback); @@ -520,7 +524,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null VideoPreview( not_null parent, - not_null controller, + std::shared_ptr show, not_null document, bool alignToBottom, Fn readyCallback) { @@ -683,7 +687,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenericPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); @@ -699,7 +703,7 @@ struct VideoPreviewDocument { std::vector> medias; Ui::RpWidget *single = nullptr; }; - const auto session = &controller->session(); + const auto session = &show->session(); const auto state = lifetime.make_state(); const auto create = [=] { const auto document = LookupVideo(session, section); @@ -708,7 +712,7 @@ struct VideoPreviewDocument { } state->single = VideoPreview( result, - controller, + show, document, !VideoAlignToTop(section), readyCallback); @@ -724,14 +728,18 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { switch (section) { case PremiumPreview::Stickers: - return StickersPreview(parent, controller, readyCallback); + return StickersPreview(parent, std::move(show), readyCallback); default: - return GenericPreview(parent, controller, section, readyCallback); + return GenericPreview( + parent, + std::move(show), + section, + readyCallback); } } @@ -792,7 +800,7 @@ struct VideoPreviewDocument { void PreviewBox( not_null box, - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, const QImage &back) { @@ -825,7 +833,7 @@ void PreviewBox( }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; - state->order = Settings::PremiumPreviewOrder(&controller->session()); + state->order = Settings::PremiumPreviewOrder(&show->session()); const auto index = [=](PremiumPreview section) { const auto it = ranges::find(state->order, section); @@ -880,7 +888,7 @@ void PreviewBox( }; state->stickersPreload = GenerateDefaultPreview( outer, - controller, + show, PremiumPreview::Stickers, ready); state->stickersPreload->hide(); @@ -890,13 +898,13 @@ void PreviewBox( switch (descriptor.section) { case PremiumPreview::Stickers: state->content = media - ? StickerPreview(outer, controller, media, state->preload) - : StickersPreview(outer, controller, state->preload); + ? StickerPreview(outer, show, media, state->preload) + : StickersPreview(outer, show, state->preload); break; default: state->content = GenericPreview( outer, - controller, + show, descriptor.section, state->preload); break; @@ -955,7 +963,7 @@ void PreviewBox( } else { state->content = GenerateDefaultPreview( outer, - controller, + show, now, state->preload); } @@ -1003,7 +1011,7 @@ void PreviewBox( state->preload(); } }; - if (descriptor.fromSettings && controller->session().premium()) { + if (descriptor.fromSettings && show->session().premium()) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { @@ -1030,16 +1038,21 @@ void PreviewBox( auto button = descriptor.fromSettings ? object_ptr::fromRaw( Settings::CreateSubscribeButton({ - controller, - box, - computeRef, + .parent = box, + .computeRef = computeRef, + .show = show, })) : CreateUnlockButton(box, std::move(unlock)); button->resizeToWidth(width); if (!descriptor.fromSettings) { button->setClickedCallback([=] { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (!window) { + return; + } Settings::ShowPremium( - controller, + window, Settings::LookupPremiumRef(state->selected.current())); }); } @@ -1052,7 +1065,7 @@ void PreviewBox( if (descriptor.fromSettings) { Data::AmPremiumValue( - &controller->session() + &show->session() ) | rpl::skip(1) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); @@ -1076,25 +1089,26 @@ void PreviewBox( } void Show( - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, QImage back) { - const auto box = controller->show( - Box(PreviewBox, controller, descriptor, media, back)); + auto box = Box(PreviewBox, show, descriptor, media, back); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + descriptor.shownCallback(raw); } } -void Show(not_null controller, QImage back) { +void Show(std::shared_ptr show, QImage back) { auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { - Show(controller, i->descriptor, i->media, back); + } else if (already == show) { + Show(std::move(show), i->descriptor, i->media, back); i = list.erase(i); return; } else { @@ -1104,21 +1118,23 @@ void Show(not_null controller, QImage back) { } void Show( - not_null controller, + std::shared_ptr show, Descriptor &&descriptor) { - if (!controller->session().premiumPossible()) { - const auto box = controller->show(Box(PremiumUnavailableBox)); + if (!show->session().premiumPossible()) { + auto box = Box(PremiumUnavailableBox); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + descriptor.shownCallback(raw); } return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { + } else if (already == show) { if (i->descriptor == descriptor) { return; } @@ -1135,13 +1151,13 @@ void Show( } } - const auto weak = base::make_weak(controller); + const auto weak = std::weak_ptr(show); list.push_back({ .descriptor = descriptor, .media = (descriptor.requestedSticker ? descriptor.requestedSticker->createMediaView() : nullptr), - .controller = weak, + .show = weak, }); if (const auto &media = list.back().media) { PreloadSticker(media); @@ -1166,8 +1182,8 @@ void Show( Images::CornersMask(st::boxRadius), RectPart::TopLeft | RectPart::TopRight); crl::on_main([=] { - if (const auto strong = weak.get()) { - Show(strong, result); + if (auto strong = weak.lock()) { + Show(std::move(strong), result); } }); }); @@ -1176,9 +1192,9 @@ void Show( } // namespace void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document) { - Show(controller, Descriptor{ + Show(std::move(show), Descriptor{ .section = PremiumPreview::Stickers, .requestedSticker = document, }); @@ -1188,7 +1204,14 @@ void ShowPremiumPreviewBox( not_null controller, PremiumPreview section, Fn)> shown) { - Show(controller, Descriptor{ + ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); +} + +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown) { + Show(std::move(show), Descriptor{ .section = section, .shownCallback = std::move(shown), }); @@ -1198,7 +1221,7 @@ void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, Fn hiddenCallback) { - Show(controller, Descriptor{ + Show(controller->uiShow(), Descriptor{ .section = section, .fromSettings = true, .hiddenCallback = std::move(hiddenCallback), @@ -1337,7 +1360,10 @@ void DoubledLimitsPreviewBox( Main::Domain::kPremiumMaxAccounts, till, }); - Ui::Premium::ShowListBox(box, std::move(entries)); + Ui::Premium::ShowListBox( + box, + st::defaultPremiumLimits, + std::move(entries)); } object_ptr CreateUnlockButton( diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 0bf439d29..51f9bcfbd 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class DocumentData; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct ReactionId; } // namespace Data @@ -30,7 +34,7 @@ class Session; } // namespace Main void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document); void DoubledLimitsPreviewBox( @@ -59,6 +63,11 @@ void ShowPremiumPreviewBox( PremiumPreview section, Fn)> shown = nullptr); +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown = nullptr); + void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 8c3845efe..f0282306b 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -62,7 +62,8 @@ PeerId GenerateUser(not_null history, const QString &name) { MTPstring(), // bot placeholder MTPstring(), // lang code MTPEmojiStatus(), - MTPVector())); + MTPVector(), + MTPint())); // stories_max_id return peerId; } @@ -76,11 +77,11 @@ AdminLog::OwnedItem GenerateItem( const auto item = history->addNewLocalMessage( history->nextNonHistoryEntryId(), - MessageFlag::FakeHistoryItem + (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId - | MessageFlag::HasReplyInfo, + | MessageFlag::HasReplyInfo), UserId(), // via - replyTo, + FullReplyTo{ .msgId = replyTo }, base::unixtime::now(), // date from, QString(), // postAuthor diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp index 291e0617b..5c8d59133 100644 --- a/Telegram/SourceFiles/boxes/report_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp @@ -14,12 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" namespace { [[nodiscard]] object_ptr Report( not_null peer, - std::variant> data) { + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data, + const style::ReportBox *stOverride) { const auto source = v::match(data, [](const MessageIdsList &ids) { return Ui::ReportSource::Message; }, [&](not_null photo) { @@ -34,15 +40,18 @@ namespace { : (photo->hasVideo() ? Ui::ReportSource::ChannelVideo : Ui::ReportSource::ChannelPhoto); + }, [&](StoryId id) { + return Ui::ReportSource::Story; }, [](v::null_t) { Unexpected("Bad source report."); return Ui::ReportSource::Bot; }); + const auto st = stOverride ? stOverride : &st::defaultReportBox; return Box([=](not_null box) { const auto show = box->uiShow(); - Ui::ReportReasonBox(box, source, [=](Ui::ReportReason reason) { + Ui::ReportReasonBox(box, *st, source, [=](Ui::ReportReason reason) { show->showBox(Box([=](not_null box) { - Ui::ReportDetailsBox(box, [=](const QString &text) { + Ui::ReportDetailsBox(box, *st, [=](const QString &text) { Api::SendReport(show, peer, reason, text, data); show->hideLayer(); }); @@ -56,13 +65,13 @@ namespace { object_ptr ReportItemsBox( not_null peer, MessageIdsList ids) { - return Report(peer, ids); + return Report(peer, ids, nullptr); } object_ptr ReportProfilePhotoBox( not_null peer, not_null photo) { - return Report(peer, photo); + return Report(peer, photo, nullptr); } void ShowReportPeerBox( @@ -93,17 +102,20 @@ void ShowReportPeerBox( if (reason == Ui::ReportReason::Fake || reason == Ui::ReportReason::Other) { state->ids = {}; - state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send)); + state->detailsBox = window->show( + Box(Ui::ReportDetailsBox, st::defaultReportBox, send)); return; } window->showChooseReportMessages(peer, reason, [=]( MessageIdsList ids) { state->ids = std::move(ids); - state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send)); + state->detailsBox = window->show( + Box(Ui::ReportDetailsBox, st::defaultReportBox, send)); }); }; state->reasonBox = window->show(Box( Ui::ReportReasonBox, + st::defaultReportBox, (peer->isBroadcast() ? Ui::ReportSource::Channel : peer->isUser() diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index dd5a84e55..47cb78029 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -140,13 +140,19 @@ SendFilesLimits DefaultLimitsForPeer(not_null peer) { SendFilesCheck DefaultCheckForPeer( not_null controller, not_null peer) { + return DefaultCheckForPeer(controller->uiShow(), peer); +} + +SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer) { return [=]( const Ui::PreparedFile &file, bool compress, bool silent) { const auto error = Data::FileRestrictionError(peer, file, compress); if (error && !silent) { - controller->showToast(*error); + show->showToast(*error); } return !error.has_value(); }; @@ -154,6 +160,7 @@ SendFilesCheck DefaultCheckForPeer( SendFilesBox::Block::Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -173,20 +180,24 @@ SendFilesBox::Block::Block( if (_isAlbum) { const auto preview = Ui::CreateChild( parent.get(), + st, my, way); _preview.reset(preview); } else { const auto media = Ui::SingleMediaPreview::Create( parent, + st, gifPaused, first); if (media) { _isSingleMedia = true; _preview.reset(media); } else { - _preview.reset( - Ui::CreateChild(parent.get(), first)); + _preview.reset(Ui::CreateChild( + parent.get(), + st, + first)); } } _preview->show(); @@ -331,15 +342,32 @@ SendFilesBox::SendFilesBox( SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType) -: _controller(controller) -, _sendType(sendType) +: SendFilesBox(nullptr, { + .show = controller->uiShow(), + .list = std::move(list), + .caption = caption, + .limits = limits, + .check = check, + .sendType = sendType, + .sendMenuType = sendMenuType, +}) { +} + +SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) +: _show(std::move(descriptor.show)) +, _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _sendType(descriptor.sendType) , _titleHeight(st::boxTitleHeight) -, _list(std::move(list)) -, _limits(limits) -, _sendMenuType(sendMenuType) -, _check(std::move(check)) -, _caption(this, st::confirmCaptionArea, Ui::InputField::Mode::MultiLine) -, _prefilledCaptionText(std::move(caption)) +, _list(std::move(descriptor.list)) +, _limits(descriptor.limits) +, _sendMenuType(descriptor.sendMenuType) +, _check(std::move(descriptor.check)) +, _confirmedCallback(std::move(descriptor.confirmed)) +, _cancelledCallback(std::move(descriptor.cancelled)) +, _caption(this, _st.files.caption, Ui::InputField::Mode::MultiLine) +, _prefilledCaptionText(std::move(descriptor.caption)) , _scroll(this, st::boxScroll) , _inner( _scroll->setOwnedWidget( @@ -434,7 +462,7 @@ void SendFilesBox::setupDragArea() { const auto droppedCallback = [=](bool compress) { return [=](const QMimeData *data) { addFiles(data); - Window::ActivateWindow(_controller); + _show->activate(); }; }; areas.document->setDroppedCallback(droppedCallback(false)); @@ -482,7 +510,7 @@ void SendFilesBox::openDialogToAddFileToAlbum() { return true; }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, @@ -566,11 +594,11 @@ void SendFilesBox::addMenuButton() { return; } - const auto top = addTopButton(st::infoTopBarMenu); + const auto top = addTopButton(_st.files.menu); top->setClickedCallback([=] { - _menu = base::make_unique_q( - top, - st::popupMenuExpandedSeparator); + const auto &tabbed = _st.tabbed; + const auto &icons = tabbed.icons; + _menu = base::make_unique_q(top, tabbed.menu); if (hasSpoilerMenu()) { const auto spoilered = allWithSpoilers(); _menu->addAction( @@ -578,9 +606,9 @@ void SendFilesBox::addMenuButton() { ? tr::lng_context_disable_spoiler(tr::now) : tr::lng_context_spoiler_effect(tr::now)), [=] { toggleSpoilers(!spoilered); }, - spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler); + spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); if (hasSendMenu()) { - _menu->addSeparator(); + _menu->addSeparator(&tabbed.expandedSeparator); } } if (hasSendMenu()) { @@ -589,7 +617,8 @@ void SendFilesBox::addMenuButton() { _sendMenuType, [=] { sendSilent(); }, [=] { sendScheduled(); }, - [=] { sendWhenOnline(); }); + [=] { sendWhenOnline(); }, + &_st.tabbed.icons); } _menu->popup(QCursor::pos()); return true; @@ -714,12 +743,12 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) { } void SendFilesBox::pushBlock(int from, int till) { - const auto gifPaused = [controller = _controller] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); + const auto gifPaused = [show = _show] { + return show->paused(Window::GifPauseReason::Layer); }; _blocks.emplace_back( _inner.data(), + _st, &_list.files, from, till, @@ -810,7 +839,7 @@ void SendFilesBox::pushBlock(int from, int till) { return checkSlowmode(list) && checkRights(list); }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, @@ -828,15 +857,15 @@ void SendFilesBox::pushBlock(int from, int till) { const auto openedOnce = widget->lifetime().make_state(false); block.itemModifyRequest( - ) | rpl::start_with_next([=, controller = _controller](int index) { + ) | rpl::start_with_next([=, show = _show](int index) { if (!(*openedOnce)) { - controller->session().settings().incrementPhotoEditorHintShown(); - controller->session().saveSettings(); + show->session().settings().incrementPhotoEditorHintShown(); + show->session().saveSettings(); } *openedOnce = true; Editor::OpenWithPreparedFile( this, - controller, + show, &_list.files[index], st::sendMediaPreviewSize, [=] { refreshAllAfterChanges(from); }); @@ -859,12 +888,14 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_send_grouped(tr::now), groupFilesFirst, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendImagesAsPhotos.create( this, tr::lng_send_compressed(tr::now), _sendWay.current().sendImagesAsPhotos(), - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendWay.changes( ) | rpl::start_with_next([=](SendFilesWay value) { @@ -908,7 +939,8 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_remember(tr::now), false, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _wayRemember->hide(); rpl::combine( _groupFiles->checkedValue(), @@ -956,25 +988,32 @@ void SendFilesBox::updateSendWayControls() { : tr::lng_send_compressed_one(tr::now)); _hintLabel->setVisible( - _controller->session().settings().photoEditorHintShown() + _show->session().settings().photoEditorHintShown() ? _list.canHaveEditorHintLabel() : false); } void SendFilesBox::setupCaption() { - const auto allow = [=](const auto&) { + const auto allow = [=](const auto &) { return (_limits & SendFilesAllow::EmojiWithoutPremium); }; + const auto show = _show; InitMessageFieldHandlers( - _controller, + &show->session(), + show, _caption.data(), - Window::GifPauseReason::Layer, - allow); + [=] { return show->paused(Window::GifPauseReason::Layer); }, + allow, + &_st.files.caption); Ui::Emoji::SuggestionsController::Init( getDelegate()->outerContainer(), _caption, - &_controller->session(), - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + &_show->session(), + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); if (!_prefilledCaptionText.text.isEmpty()) { _caption->setTextWithTags( @@ -1022,12 +1061,21 @@ void SendFilesBox::setupEmojiPanel() { using Selector = ChatHelpers::TabbedSelector; _emojiPanel = base::make_unique_q( container, - _controller, - object_ptr( - nullptr, - _controller->uiShow(), - Window::GifPauseReason::Layer, - Selector::Mode::EmojiOnly)); + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + }); _emojiPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -1044,11 +1092,9 @@ void SendFilesBox::setupEmojiPanel() { const auto info = data.document->sticker(); if (info && info->setType == Data::StickersType::Emoji - && !_controller->session().premium() + && !_show->session().premium() && !(_limits & SendFilesAllow::EmojiWithoutPremium)) { - ShowPremiumPreviewBox( - _controller, - PremiumPreview::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); } @@ -1060,7 +1106,7 @@ void SendFilesBox::setupEmojiPanel() { }; _emojiFilter.reset(base::install_event_filter(container, filterCallback)); - _emojiToggle.create(this, st::boxAttachEmoji); + _emojiToggle.create(this, _st.files.emoji); _emojiToggle->setVisible(!_caption->isHidden()); _emojiToggle->installEventFilter(_emojiPanel); _emojiToggle->addClickHandler([=] { @@ -1098,7 +1144,7 @@ bool SendFilesBox::canAddFiles(not_null data) const { } bool SendFilesBox::addFiles(not_null data) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); auto list = [&] { const auto urls = Core::ReadMimeUrls(data); auto result = CanAddUrls(urls) @@ -1245,7 +1291,7 @@ void SendFilesBox::paintEvent(QPaintEvent *e) { Painter p(this); p.setFont(st::boxTitleFont); - p.setPen(st::boxTitleFg); + p.setPen(getDelegate()->style().title.textFg); p.drawTextLeft( st::boxPhotoTitlePosition.x(), st::boxTitlePosition.y() - st::boxTopMargin, @@ -1321,7 +1367,7 @@ void SendFilesBox::saveSendWaySettings() { } bool SendFilesBox::validateLength(const QString &text) const { - const auto session = &_controller->session(); + const auto session = &_show->session(); const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); const auto remove = int(text.size()) - limit; const auto way = _sendWay.current(); @@ -1331,7 +1377,8 @@ bool SendFilesBox::validateLength(const QString &text) const { way.sendImagesAsPhotos())) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _show->showBox( + Box(CaptionLimitReachedBox, session, remove, &_st.premium)); return false; } @@ -1397,8 +1444,7 @@ void SendFilesBox::sendScheduled() { ? SendMenu::Type::ScheduledToUser : _sendMenuType; const auto callback = [=](Api::SendOptions options) { send(options); }; - _controller->show( - HistoryView::PrepareScheduleBox(this, type, callback)); + _show->showBox(HistoryView::PrepareScheduleBox(this, type, callback)); } void SendFilesBox::sendWhenOnline() { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 8e93673f0..6e6c6fa82 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localimageloader.h" #include "storage/storage_media_prepare.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Window { class SessionController; } // namespace Window @@ -26,6 +30,7 @@ enum class SendType; namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { @@ -71,6 +76,29 @@ using SendFilesCheck = Fn controller, not_null peer); +[[nodiscard]] SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer); + +using SendFilesConfirmed = Fn; + +struct SendFilesBoxDescriptor { + std::shared_ptr show; + Ui::PreparedList list; + TextWithTags caption; + SendFilesLimits limits = {}; + SendFilesCheck check; + Api::SendType sendType = {}; + SendMenu::Type sendMenuType = {}; + const style::ComposeControls *stOverride = nullptr; + SendFilesConfirmed confirmed; + Fn cancelled; +}; class SendFilesBox : public Ui::BoxContent { public: @@ -87,14 +115,9 @@ public: SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType); + SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor); - void setConfirmedCallback( - Fn callback) { + void setConfirmedCallback(SendFilesConfirmed callback) { _confirmedCallback = std::move(callback); } void setCancelledCallback(Fn callback) { @@ -116,6 +139,7 @@ private: public: Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -201,7 +225,8 @@ private: void enqueueNextPrepare(); void addPreparedAsyncFile(Ui::PreparedFile &&file); - const not_null _controller; + const std::shared_ptr _show; + const style::ComposeControls &_st; const Api::SendType _sendType = Api::SendType(); QString _titleText; @@ -211,15 +236,10 @@ private: std::optional _removingIndex; SendFilesLimits _limits = {}; - SendMenu::Type _sendMenuType = SendMenu::Type(); + SendMenu::Type _sendMenuType = {}; SendFilesCheck _check; - Fn _confirmedCallback; + SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; bool _confirmed = false; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index d23e6142c..7cba7daba 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1414,15 +1414,16 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( ? MsgId(0) : topicRootId; const auto peer = thread->peer(); - histories.sendRequest(history, requestType, [=]( + const auto threadHistory = thread->owningHistory(); + histories.sendRequest(threadHistory, requestType, [=]( Fn finish) { - auto &api = history->session().api(); + auto &api = threadHistory->session().api(); const auto sendFlags = commonSendFlags | (topMsgId ? Flag::f_top_msg_id : Flag(0)) | (ShouldSendSilent(peer, options) ? Flag::f_silent : Flag(0)); - history->sendRequestId = api.request( + threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), history->peer->input, @@ -1433,7 +1434,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(options.scheduled), MTP_inputPeerEmpty() // send_as )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { - history->session().api().applyUpdates(updates); + threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); if (state->requests.empty()) { if (show->valid()) { @@ -1451,10 +1452,10 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( peer->name())); } finish(); - }).afterRequest(history->sendRequestId).send(); - return history->sendRequestId; + }).afterRequest(threadHistory->sendRequestId).send(); + return threadHistory->sendRequestId; }); - state->requests.insert(history->sendRequestId); + state->requests.insert(threadHistory->sendRequestId); } }; } diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 0f661181a..82e4e3deb 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -606,6 +606,7 @@ void StickerSetBox::updateButtons() { const auto session = &_show->session(); auto box = ChatHelpers::MakeConfirmRemoveSetBox( session, + st::boxLabel, _inner->setId()); if (box) { _show->showBox(std::move(box)); diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index 662cf51e5..b21cf229c 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" +#include "ui/power_saving.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/multi_select.h" @@ -63,7 +64,7 @@ ShowButton::ShowButton(not_null parent) _button.sizeValue( ) | rpl::start_with_next([=](const QSize &s) { resize( - s.width() + st::emojiSuggestionsFadeRight.width(), + s.width() + st::defaultEmojiSuggestions.fadeRight.width(), s.height()); _button.moveToRight(0, 0); }, lifetime()); @@ -74,7 +75,7 @@ void ShowButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto clip = e->rect(); - const auto &icon = st::emojiSuggestionsFadeRight; + const auto &icon = st::defaultEmojiSuggestions.fadeRight; const auto fade = QRect(0, 0, icon.width(), height()); if (fade.intersects(clip)) { icon.fill(p, fade); @@ -131,6 +132,14 @@ void TranslateBox( // container, // tr::lng_translate_box_original()); + const auto animationsPaused = [] { + using Which = FlatLabel::WhichAnimationsPaused; + const auto emoji = On(PowerSaving::kEmojiChat); + const auto spoiler = On(PowerSaving::kChatSpoiler); + return emoji + ? (spoiler ? Which::All : Which::CustomEmoji) + : (spoiler ? Which::Spoiler : Which::None); + }; const auto original = box->addRow(object_ptr>( box, object_ptr(box, stLabel))); @@ -139,6 +148,7 @@ void TranslateBox( original->entity()->setContextMenuHook([](auto&&) { }); } + original->entity()->setAnimationsPausedCallback(animationsPaused); original->entity()->setMarkedText( text, Core::MarkedTextContext{ @@ -194,6 +204,7 @@ void TranslateBox( box, object_ptr(box, stLabel))); translated->entity()->setSelectable(!hasCopyRestriction); + translated->entity()->setAnimationsPausedCallback(animationsPaused); constexpr auto kMaxLines = 3; container->resizeToWidth(box->width()); diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index e43af9373..f06f78cfd 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "base/timer.h" #include "styles/style_calls.h" -#include "styles/style_chat.h" // style::GroupCallUserpics +#include "styles/style_chat_helpers.h" // style::GroupCallUserpics #include "styles/style_layers.h" namespace Calls { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 454d658ed..8af7d3f0d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -124,7 +124,7 @@ void Show::showOrHideBoxOrLayer( } else if (const auto panel = _panel.get()) { panel->hideLayer(animated); } - } +} not_null Show::toastParent() const { const auto panel = _panel.get(); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 97f94cb19..5c72090e0 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -10,10 +10,20 @@ using "ui/basic.style"; using "boxes/boxes.style"; using "ui/layers/layers.style"; using "ui/widgets/widgets.style"; +using "ui/menu_icons.style"; +using "ui/effects/premium.style"; + +GroupCallUserpics { + size: pixels; + shift: pixels; + stroke: pixels; + align: align; +} TabbedSearch { outer: color; bg: color; + bgActive: color; fg: color; fgActive: color; fadeLeft: icon; @@ -28,9 +38,57 @@ TabbedSearch { height: pixels; } +ComposeIcons { + settings: icon; + + recent: icon; + recentActive: icon; + people: icon; + peopleActive: icon; + nature: icon; + natureActive: icon; + food: icon; + foodActive: icon; + activity: icon; + activityActive: icon; + travel: icon; + travelActive: icon; + objects: icon; + objectsActive: icon; + symbols: icon; + symbolsActive: icon; + + menuFave: icon; + menuUnfave: icon; + menuStickerSet: icon; + menuRecentRemove: icon; + menuGifAdd: icon; + menuGifRemove: icon; + menuMute: icon; + menuSchedule: icon; + menuWhenOnline: icon; + menuSpoiler: icon; + menuSpoilerOff: icon; + + stripBubble: icon; + stripPremiumLocked: icon; + stripExpandPanel: icon; + stripExpandDropdown: icon; +} + +EmojiSuggestions { + dropdown: InnerDropdown; + bg: color; + overBg: color; + textFg: color; + fadeLeft: icon; + fadeRight: icon; +} + EmojiPan { margin: margins; padding: margins; + showAnimation: PanelAnimation; desiredSize: pixels; verticalSizeSub: pixels; header: pixels; @@ -43,13 +101,149 @@ EmojiPan { iconWidth: pixels; iconArea: pixels; bg: color; + headerFg: color; + trendingHeaderFg: color; + trendingSubheaderFg: color; + trendingUnreadFg: color; + trendingInstalled: icon; overBg: color; + pathBg: color; + pathFg: color; + textFg: color; categoriesBg: color; categoriesBgOver: color; fadeLeft: icon; fadeRight: icon; + menu: PopupMenu; + expandedSeparator: MenuSeparator; + tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + removeSet: IconButton; + boxLabel: FlatLabel; + icons: ComposeIcons; + autocompleteBottomSkip: pixels; +} + +MessageBar { + title: TextStyle; + titleFg: color; + text: TextStyle; + textFg: color; + textPalette: TextPalette; + duration: int; +} + +EmojiButton { + inner: IconButton; + bg: color; + lineFg: color; + lineFgOver: color; +} + +SendButton { + inner: IconButton; + record: icon; + recordOver: icon; + sendDisabledFg: color; +} + +RecordBarLock { + ripple: RippleAnimation; + originTop: icon; + originBottom: icon; + originBody: icon; + shadowTop: icon; + shadowBottom: icon; + shadowBody: icon; + arrow: icon; + fg: color; +} + +RecordBar { + radius: pixels; + bg: color; + durationFg: color; + cancel: color; + cancelActive: color; + cancelRipple: RippleAnimation; + lock: RecordBarLock; + remove: IconButton; +} + +ComposeFiles { + check: Check; + checkbox: Checkbox; + menu: IconButton; + caption: InputField; + emoji: EmojiButton; + confirmBg: color; + buttonFile: IconButton; + buttonFileEdit: icon; + buttonFileDelete: icon; + iconBg: color; + iconPlay: icon; + iconImage: icon; + iconDocument: icon; + nameFg: color; + statusFg: color; +} + +ComposeControls { + bg: color; + radius: pixels; + + field: InputField; + send: SendButton; + attach: IconButton; + emoji: EmojiButton; + suggestions: EmojiSuggestions; + tabbed: EmojiPan; + tabbedHeightMin: pixels; + tabbedHeightMax: pixels; + record: RecordBar; + files: ComposeFiles; + premium: PremiumLimits; +} + +ReportBox { + button: SettingsButton; + label: FlatLabel; + field: InputField; + spam: icon; + fake: icon; + violence: icon; + children: icon; + pornography: icon; + copyright: icon; + drugs: icon; + personal: icon; + other: icon; +} + +WhoRead { + userpics: GroupCallUserpics; + photoLeft: pixels; + photoSize: pixels; + photoSkip: pixels; + nameLeft: pixels; + iconPosition: point; + itemPadding: margins; +} + +defaultWhoRead: WhoRead { + userpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(right); + } + photoLeft: 13px; + photoSize: 30px; + photoSkip: 5px; + nameLeft: 57px; + iconPosition: point(15px, 7px); + itemPadding: margins(44px, 9px, 17px, 7px); } switchPmButton: RoundButton(defaultBoxButton) { @@ -144,12 +338,6 @@ stickersScroll: ScrollArea(boxScroll) { stickersRowDisabledOpacity: 0.4; stickersRowDuration: 200; -stickersSettings: icon {{ "emoji/emoji_settings", emojiIconFg }}; -stickersTrending: icon {{ "emoji/stickers_add", emojiIconFg }}; -stickersTrendingUnread: icon { - { "emoji/stickers_add_unread", emojiIconFg }, - { "emoji/stickers_add_dot", dialogsUnreadBg } -}; emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }}; filtersRemove: IconButton(stickersRemove) { @@ -163,22 +351,6 @@ emojiTabs: SettingsSlider(defaultTabsSlider) { barTop: 40px; labelTop: 12px; } -emojiRecent: icon {{ "emoji/emoji_recent", emojiIconFg }}; -emojiRecentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; -emojiPeople: icon {{ "emoji/emoji_smile", emojiIconFg }}; -emojiPeopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; -emojiNature: icon {{ "emoji/emoji_nature", emojiIconFg }}; -emojiNatureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; -emojiFood: icon {{ "emoji/emoji_food", emojiIconFg }}; -emojiFoodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; -emojiActivity: icon {{ "emoji/emoji_activities", emojiIconFg }}; -emojiActivityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; -emojiTravel: icon {{ "emoji/emoji_travel", emojiIconFg }}; -emojiTravelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; -emojiObjects: icon {{ "emoji/emoji_objects", emojiIconFg }}; -emojiObjectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; -emojiSymbols: icon {{ "emoji/emoji_love", emojiIconFg }}; -emojiSymbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; emojiCategoryIconTop: 6px; emojiPanAnimation: PanelAnimation(defaultPanelAnimation) { @@ -196,84 +368,60 @@ emojiPanSlideDuration: 200; emojiPanArea: size(34px, 32px); emojiPanRadius: 8px; +defaultTabbedSearchCancel: CrossButton { + width: 33px; + height: 33px; + + cross: CrossAnimation { + size: 27px; + skip: 8px; + stroke: 1.; + minScale: 0.3; + } + crossFg: menuIconFg; + crossFgOver: menuIconFg; + crossPosition: point(1px, 3px); + + duration: 150; + loadingPeriod: 1000; + ripple: emptyRippleAnimation; +} +defaultTabbedSearchField: InputField(defaultMultiSelectSearchField) { + textMargins: margins(2px, 7px, 2px, 0px); +} +defaultTabbedSearchButton: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} +defaultTabbedSearchBack: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_back", menuIconFg }}; + iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} defaultTabbedSearch: TabbedSearch { outer: emojiPanBg; bg: emojiPanHover; + bgActive: windowBgRipple; fg: emojiIconFg; fgActive: emojiSubIconFgActive; fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanHover }}; fadeRight: icon {{ "fade_horizontal", emojiPanHover }}; - field: InputField(defaultMultiSelectSearchField) { - textMargins: margins(2px, 7px, 2px, 0px); - } - search: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - back: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_back", menuIconFg }}; - iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - cancel: CrossButton { - width: 33px; - height: 33px; - - cross: CrossAnimation { - size: 27px; - skip: 8px; - stroke: 1.; - minScale: 0.3; - } - crossFg: menuIconFg; - crossFgOver: menuIconFg; - crossPosition: point(1px, 3px); - - duration: 150; - loadingPeriod: 1000; - ripple: emptyRippleAnimation; - } + field: defaultTabbedSearchField; + search: defaultTabbedSearchButton; + back: defaultTabbedSearchBack; + cancel: defaultTabbedSearchCancel; defaultFieldWidth: 103px; groupWidth: 30px; groupSkip: 2px; height: 33px; } -defaultEmojiPan: EmojiPan { - margin: margins(7px, 0px, 7px, 0px); - padding: margins(7px, 0px, 4px, 7px); - desiredSize: 37px; - verticalSizeSub: 1px; - header: 33px; - headerLeft: 14px; - headerLockLeft: 7px; - headerLockedLeft: 26px; - headerTop: 10px; - footer: 36px; - iconSkip: 3px; - iconWidth: 30px; - iconArea: 28px; - bg: emojiPanBg; - overBg: emojiPanHover; - categoriesBg: emojiPanCategories; - categoriesBgOver: windowBgRipple; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; - fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; - search: defaultTabbedSearch; - searchMargin: margins(1px, 11px, 2px, 5px); -} -statusEmojiPan: EmojiPan(defaultEmojiPan) { - categoriesBg: windowBg; - categoriesBgOver: windowBgOver; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; - fadeRight: icon {{ "fade_horizontal", windowBg }}; -} inlineResultsMinHeight: 278px; inlineResultsMaxHeight: 640px; @@ -337,6 +485,140 @@ stickersToast: Toast(defaultToast) { stickersEmpty: icon {{ "stickers_empty", windowSubTextFg }}; emojiEmpty: icon {{ "emoji_empty", windowSubTextFg }}; +editMediaButtonSize: 32px; + +editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; +editMediaButton: IconButton(defaultIconButton) { + width: editMediaButtonSize; + height: editMediaButtonSize; + + icon: editMediaButtonIconFile; + + rippleAreaSize: editMediaButtonSize; + ripple: defaultRippleAnimation; +} + +sendBoxAlbumGroupEditInternalSkip: 8px; +sendBoxAlbumGroupSkipRight: 5px; +sendBoxAlbumGroupSkipTop: 5px; +sendBoxAlbumGroupRadius: 4px; +sendBoxAlbumGroupSize: size(62px, 25px); +sendBoxAlbumSmallGroupSize: size(30px, 25px); + +sendBoxFileGroupSkipTop: 2px; +sendBoxFileGroupSkipRight: 5px; +sendBoxFileGroupEditInternalSkip: -1px; + +sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgRipple; + } +} +sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; +sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; + +sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; +sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; +sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; + +defaultComposeIcons: ComposeIcons { + settings: icon {{ "emoji/emoji_settings", emojiIconFg }}; + + recent: icon {{ "emoji/emoji_recent", emojiIconFg }}; + recentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; + people: icon {{ "emoji/emoji_smile", emojiIconFg }}; + peopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; + nature: icon {{ "emoji/emoji_nature", emojiIconFg }}; + natureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; + food: icon {{ "emoji/emoji_food", emojiIconFg }}; + foodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; + activity: icon {{ "emoji/emoji_activities", emojiIconFg }}; + activityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; + travel: icon {{ "emoji/emoji_travel", emojiIconFg }}; + travelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; + objects: icon {{ "emoji/emoji_objects", emojiIconFg }}; + objectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; + symbols: icon {{ "emoji/emoji_love", emojiIconFg }}; + symbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; + + menuFave: menuIconFave; + menuUnfave: menuIconUnfave; + menuStickerSet: menuIconStickers; + menuRecentRemove: menuIconDelete; + menuGifAdd: menuIconGif; + menuGifRemove: menuIconDelete; + menuMute: menuIconMute; + menuSchedule: menuIconSchedule; + menuWhenOnline: menuIconWhenOnline; + menuSpoiler: menuIconSpoiler; + menuSpoilerOff: menuIconSpoilerOff; + + stripBubble: icon{ + { "chat/reactions_bubble_shadow", windowShadowFg }, + { "chat/reactions_bubble", windowBg }, + }; + stripPremiumLocked: icon{ + { "chat/reactions_premium_bg", historyPeerArchiveUserpicBg }, + { "chat/reactions_premium_star", historyPeerUserpicFg }, + }; + stripExpandPanel: icon{ + { "chat/reactions_round_big", windowBgRipple }, + { "chat/reactions_expand_panel", windowSubTextFg }, + }; + stripExpandDropdown: icon{ + { "chat/reactions_round_small", windowBgRipple }, + { "chat/reactions_expand_panel", windowSubTextFg }, + }; +} +defaultEmojiPan: EmojiPan { + margin: margins(7px, 0px, 7px, 0px); + padding: margins(7px, 0px, 4px, 7px); + showAnimation: emojiPanAnimation; + desiredSize: 37px; + verticalSizeSub: 1px; + header: 33px; + headerLeft: 14px; + headerLockLeft: 7px; + headerLockedLeft: 26px; + headerTop: 10px; + footer: 36px; + iconSkip: 3px; + iconWidth: 30px; + iconArea: 28px; + bg: emojiPanBg; + headerFg: emojiPanHeaderFg; + trendingHeaderFg: stickersTrendingHeaderFg; + trendingSubheaderFg: stickersTrendingSubheaderFg; + trendingUnreadFg: stickersFeaturedUnreadBg; + trendingInstalled: stickersFeaturedInstalled; + overBg: emojiPanHover; + pathBg: windowBgRipple; + pathFg: windowBgOver; + textFg: windowFg; + categoriesBg: emojiPanCategories; + categoriesBgOver: windowBgRipple; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; + fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; + menu: popupMenuWithIcons; + expandedSeparator: MenuSeparator(defaultMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + tabs: emojiTabs; + search: defaultTabbedSearch; + searchMargin: margins(1px, 11px, 2px, 5px); + removeSet: stickerPanRemoveSet; + boxLabel: boxLabel; + icons: defaultComposeIcons; + autocompleteBottomSkip: 0px; +} +statusEmojiPan: EmojiPan(defaultEmojiPan) { + categoriesBg: windowBg; + categoriesBgOver: windowBgOver; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; + fadeRight: icon {{ "fade_horizontal", windowBg }}; +} + inlineBotsScroll: ScrollArea(defaultSolidScroll) { deltat: stickerPanPadding; deltab: stickerPanPadding; @@ -353,6 +635,15 @@ emojiSuggestionsScrolledWidth: 240px; emojiSuggestionsPadding: margins(emojiColorsPadding, 0px, emojiColorsPadding, 0px); emojiSuggestionsFadeAfter: 20px; +defaultEmojiSuggestions: EmojiSuggestions { + dropdown: emojiSuggestionsDropdown; + bg: menuBg; + overBg: emojiPanHover; + textFg: windowFg; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; + fadeRight: icon {{ "fade_horizontal", boxBg }}; +} + mentionHeight: 40px; mentionPadding: margins(8px, 5px, 8px, 5px); mentionTop: 11px; @@ -392,10 +683,6 @@ reactStripSize: 32px; reactStripMinWidth: 60px; reactStripImage: 26px; reactStripSkip: 7px; -reactStripBubble: icon{ - { "chat/reactions_bubble_shadow", windowShadowFg }, - { "chat/reactions_bubble", windowBg }, -}; reactStripBubbleRight: 20px; userpicBuilderEmojiPan: EmojiPan(statusEmojiPan) { margin: margins(reactStripSkip, 0px, reactStripSkip, 0px); @@ -422,9 +709,483 @@ reactPanelScroll: ScrollArea(emojiScroll) { deltab: 7px; } -emojiSuggestionsFadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; -emojiSuggestionsFadeRight: icon {{ "fade_horizontal", boxBg }}; - choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }}; choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }}; choosePeerCreateIconLeft: 25px; + +historyRequestsUserpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(left); +} +historyRequestsHeight: 33px; + +historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); + +historyComposeAreaPalette: TextPalette(defaultTextPalette) { + linkFg: historyComposeAreaFgService; +} + +defaultMessageBar: MessageBar { + title: semiboldTextStyle; + titleFg: windowActiveTextFg; + text: defaultTextStyle; + textFg: historyComposeAreaFg; + textPalette: historyComposeAreaPalette; + duration: 160; +} + +historyComposeButton: FlatButton { + color: windowActiveTextFg; + overColor: windowActiveTextFg; + + bgColor: historyComposeButtonBg; + overBgColor: historyComposeButtonBgOver; + + width: -32px; + height: 46px; + + textTop: 14px; + + font: semiboldFont; + overFont: semiboldFont; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgRipple; + } +} +historyUnblock: FlatButton(historyComposeButton) { + color: attentionButtonFg; + overColor: attentionButtonFgOver; +} +historyContactStatusButton: FlatButton(historyComposeButton) { + height: 49px; + textTop: 16px; + overBgColor: historyComposeButtonBg; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgOver; + } +} +historyContactStatusBlock: FlatButton(historyContactStatusButton) { + color: attentionButtonFg; + overColor: attentionButtonFg; +} +historyContactStatusLabel: FlatLabel(defaultFlatLabel) { + minWidth: 240px; +} +historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { + align: align(top); + textFg: windowSubTextFg; +} +historyContactStatusMinSkip: 16px; + +historyReplySkip: 51px; +historyReplyNameFg: windowActiveTextFg; +historyReplyHeight: 49px; +historyReplyIconPosition: point(5px, 5px); +historyReplyIcon: icon {{ "chat/input_reply", historyReplyIconFg }}; +historyForwardIcon: icon {{ "chat/input_forward", historyReplyIconFg }}; +historyEditIcon: icon {{ "chat/input_edit", historyReplyIconFg }}; +historyReplyCancel: IconButton { + width: 49px; + height: 49px; + + icon: historyReplyCancelIcon; + iconOver: historyReplyCancelIconOver; + iconPosition: point(-1px, -1px); + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +historyPinnedShowAll: IconButton(historyReplyCancel) { + icon: icon {{ "pinned_show_all", historyReplyCancelFg }}; + iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }}; +} +historyPinnedBotButton: RoundButton(defaultActiveButton) { + width: -34px; + height: 30px; + textTop: 6px; + padding: margins(2px, 10px, 10px, 9px); +} +historyPinnedBotButtonMaxWidth: 150px; + +historyToDownPosition: point(12px, 10px); +historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; +historyToDownAboveOver: icon {{ "history_down_arrow", historyToDownFgOver }}; +historyToDownPaddingTop: 10px; +historyToDownBelow: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBg }, +}; +historyToDownBelowOver: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBgOver }, +}; +historyToDown: TwoIconButton { + width: 52px; + height: 62px; + + iconBelow: historyToDownBelow; + iconBelowOver: historyToDownBelowOver; + iconAbove: historyToDownAbove; + iconAboveOver: historyToDownAboveOver; + iconPosition: point(0px, historyToDownPaddingTop); + + rippleAreaPosition: point(5px, 15px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyToDownBgRipple; + } +} +historyToDownBadgeFont: semiboldFont; +historyToDownBadgeSize: 22px; + +historyToDownShownAfter: 480px; +historyToDownDuration: 150; + +dialogsToUpAbove: icon {{ "history_down_arrow-flip_vertical", historyToDownFg, point(0px, 1px) }}; +dialogsToUpAboveOver: icon {{ "history_down_arrow-flip_vertical", historyToDownFgOver, point(0px, 1px) }}; + +dialogsToUp: TwoIconButton(historyToDown) { + iconAbove: dialogsToUpAbove; + iconAboveOver: dialogsToUpAboveOver; +} + +historyUnreadMentions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_mention", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_mention", historyToDownFgOver }}; +} +historyUnreadReactions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_reaction", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_reaction", historyToDownFgOver }}; +} +historyUnreadThingsSkip: 4px; + +historyComposeField: InputField(defaultInputField) { + font: normalFont; + textMargins: margins(0px, 0px, 0px, 0px); + textAlign: align(left); + textFg: historyComposeAreaFg; + textBg: historyComposeAreaBg; + heightMin: 36px; + heightMax: 72px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(7px, 5px, 7px, 5px); + placeholderAlign: align(topleft); + placeholderScale: 0.; + placeholderFont: normalFont; + placeholderShift: -50px; + border: 0px; + borderActive: 0px; + duration: 100; +} +historyComposeFieldMaxHeight: 224px; +// historyMinHeight: 56px; + +historyAttach: IconButton(defaultIconButton) { + width: 44px; + height: 46px; + + icon: icon {{ "chat/input_attach", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + + rippleAreaPosition: point(2px, 3px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +historyMessagesTTL: IconButtonWithText { + iconButton: IconButton(historyAttach) { + icon: icon {{ "chat/input_autodelete", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_autodelete", historyComposeIconFgOver }}; + } + textFg: historyComposeIconFg; + textFgOver: historyComposeIconFgOver; + textPadding: margins(21px, 20px, 3px, 7px); + textAlign: align(left); + + font: font(10px semibold); +} +historyReplaceMedia: IconButton(historyAttach) { + icon: icon {{ "chat/input_replace", windowBgActive }}; + iconOver: icon {{ "chat/input_replace", windowBgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgOver; + } +} + +historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; +historyEmojiCircle: size(20px, 20px); +historyEmojiCircleLine: 1.5; +historyEmojiCircleFg: historyComposeIconFg; +historyEmojiCircleFgOver: historyComposeIconFgOver; +historyBotKeyboardShow: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard", historyComposeIconFgOver }}; +} +historyBotKeyboardHide: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFgOver }}; +} +historyBotCommandStart: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_command", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_command", historyComposeIconFgOver }}; +} +historyScheduledToggle: IconButton(historyAttach) { + icon: icon { + { "chat/input_scheduled", historyComposeIconFg }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; + iconOver: icon { + { "chat/input_scheduled", historyComposeIconFgOver }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; +} + +historyAttachEmojiInner: IconButton(historyAttach) { + icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }}; +} +historyAttachEmoji: EmojiButton { + inner: historyAttachEmojiInner; + bg: historyComposeAreaBg; + lineFg: historyEmojiCircleFg; + lineFgOver: historyEmojiCircleFgOver; +} +boxAttachEmoji: EmojiButton(historyAttachEmoji) { + inner: IconButton(historyAttachEmojiInner) { + width: 30px; + height: 30px; + rippleAreaSize: 0px; + } +} +boxAttachEmojiTop: 20px; + +historySendIcon: icon {{ "chat/input_send", historySendIconFg }}; +historySendIconOver: icon {{ "chat/input_send", historySendIconFgOver }}; +historySendIconPosition: point(10px, 11px); +historySendSize: size(44px, 46px); +historyScheduleIcon: icon {{ "chat/input_schedule", historyComposeAreaBg }}; +historyScheduleIconPosition: point(7px, 8px); +historyEditSaveIcon: icon {{ "chat/input_save", historySendIconFg }}; +historyEditSaveIconOver: icon {{ "chat/input_save", historySendIconFgOver }}; + +historyEditMediaBg: videoPlayIconBg; +historyEditMedia: icon{{ "chat/input_draw", videoPlayIconFg }}; +historyMessagesTTLPickerHeight: 200px; +historyMessagesTTLPickerItemHeight: 40px; +historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + align: align(topleft); + textFg: windowSubTextFg; +} + +historyRecordVoiceFg: historyComposeIconFg; +historyRecordVoiceFgOver: historyComposeIconFgOver; +historyRecordVoiceFgInactive: attentionButtonFg; +historyRecordVoiceFgActive: windowBgActive; +historyRecordVoiceFgActiveIcon: windowFgActive; +historyRecordVoiceShowDuration: 120; +historyRecordVoiceDuration: 120; +historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; +historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; +historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; +historyRecordSendIconPosition: point(2px, 0px); +historyRecordVoiceRippleBgActive: lightButtonBgOver; +historyRecordSignalRadius: 5px; +historyRecordCancel: windowSubTextFg; +historyRecordCancelActive: windowActiveTextFg; +historyRecordFont: font(13px); +historyRecordDurationSkip: 12px; +historyRecordDurationFg: historyComposeAreaFg; + +historyRecordMainBlobMinRadius: 23px; +historyRecordMainBlobMaxRadius: 37px; +historyRecordMinorBlobMinRadius: 40px; +historyRecordMinorBlobMaxRadius: 47px; +historyRecordMajorBlobMinRadius: 43px; +historyRecordMajorBlobMaxRadius: 50px; + +historyRecordTextStyle: TextStyle(defaultTextStyle) { + font: historyRecordFont; +} + +historyRecordTextWidthForWrap: 210px; +historyRecordTextLeft: 15px; +historyRecordTextRight: 25px; + +historyRecordLockShowDuration: historyToDownDuration; +historyRecordLockSize: size(75px, 133px); + +historyRecordLockIconSize: size(14px, 17px); +historyRecordLockIconBottomHeight: 9px; +historyRecordLockIconLineHeight: 2px; +historyRecordLockIconLineSkip: 3px; +historyRecordLockIconLineWidth: 2px; +historyRecordLockIconArcHeight: 4px; +historyRecordStopIconWidth: 12px; + +historyRecordLockTopShadow: icon {{ "voice_lock/record_lock_top_shadow", historyToDownShadow }}; +historyRecordLockTop: icon {{ "voice_lock/record_lock_top", historyToDownBg }}; +historyRecordLockBottomShadow: icon {{ "voice_lock/record_lock_bottom_shadow", historyToDownShadow }}; +historyRecordLockBottom: icon {{ "voice_lock/record_lock_bottom", historyToDownBg }}; +historyRecordLockBodyShadow: icon {{ "voice_lock/record_lock_body_shadow", historyToDownShadow }}; +historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}; +historyRecordLockMargin: margins(4px, 4px, 4px, 4px); +historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; +historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); + +historyRecordDelete: IconButton(historyAttach) { + icon: icon {{ "info/info_media_delete", historyComposeIconFg }}; + iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }}; + iconPosition: point(10px, 11px); +} +historyRecordWaveformRightSkip: 10px; +historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px); + +historyRecordWaveformBar: 3px; + +historyRecordLockPosition: point(1px, 35px); + +historyRecordCancelButtonWidth: 100px; +historyRecordCancelButtonFg: lightButtonFg; + +historySilentToggle: IconButton(historyBotKeyboardShow) { + icon: icon {{ "chat/input_silent", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }}; +} +historySilentToggleOn: icon {{ "chat/input_silent_on", historyComposeIconFg }}; +historySilentToggleOnOver: icon {{ "chat/input_silent_on", historyComposeIconFgOver }}; + +defaultRecordBarLock: RecordBarLock { + ripple: defaultRippleAnimation; + originTop: historyRecordLockTop; + originBottom: historyRecordLockBottom; + originBody: historyRecordLockBody; + shadowTop: historyRecordLockTopShadow; + shadowBottom: historyRecordLockBottomShadow; + shadowBody: historyRecordLockBodyShadow; + arrow: historyRecordLockArrow; + fg: historyToDownFg; +} +defaultRecordBar: RecordBar { + bg: historyComposeAreaBg; + durationFg: historyRecordDurationFg; + cancel: historyRecordCancel; + cancelActive: historyRecordCancelActive; + cancelRipple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgRipple; + } + lock: defaultRecordBarLock; + remove: historyRecordDelete; +} + +historySend: SendButton { + inner: IconButton(historyAttach) { + icon: historySendIcon; + iconOver: historySendIconOver; + } + record: historyRecordVoice; + recordOver: historyRecordVoiceOver; + sendDisabledFg: historyComposeIconFg; +} + +defaultComposeFilesMenu: IconButton(defaultIconButton) { + width: 48px; + height: 54px; + + icon: icon {{ "title_menu_dots", boxTitleCloseFg }}; + iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }}; + iconPosition: point(18px, -1px); + + rippleAreaPosition: point(1px, 6px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +defaultComposeFilesField: InputField(defaultInputField) { + textMargins: margins(1px, 26px, 31px, 4px); + heightMax: 158px; +} +defaultComposeFiles: ComposeFiles { + check: defaultCheck; + checkbox: defaultBoxCheckbox; + menu: defaultComposeFilesMenu; + caption: defaultComposeFilesField; + emoji: boxAttachEmoji; + confirmBg: windowBgOver; + buttonFile: sendBoxAlbumGroupButtonFile; + buttonFileEdit: sendBoxAlbumGroupEditButtonIconFile; + buttonFileDelete: sendBoxAlbumGroupDeleteButtonIconFile; + iconBg: msgFileInBg; + iconPlay: icon {{ "history_file_play", historyFileInIconFg }}; + iconImage: icon {{ "history_file_image", historyFileInIconFg }}; + iconDocument: icon {{ "history_file_document", historyFileInIconFg }}; + nameFg: historyFileNameInFg; + statusFg: mediaInFg; +} +defaultComposeControls: ComposeControls { + bg: historyComposeAreaBg; + radius: 0px; + + field: historyComposeField; + send: historySend; + attach: historyAttach; + emoji: historyAttachEmoji; + suggestions: defaultEmojiSuggestions; + tabbed: defaultEmojiPan; + tabbedHeightMin: emojiPanMinHeight; + tabbedHeightMax: emojiPanMaxHeight; + record: defaultRecordBar; + files: defaultComposeFiles; + premium: defaultPremiumLimits; +} + +moreChatsBarHeight: 48px; +moreChatsBarTextPosition: point(12px, 4px); +moreChatsBarStatusPosition: point(12px, 24px); +moreChatsBarClose: IconButton(defaultIconButton) { + width: 48px; + height: 48px; + + icon: boxTitleCloseIcon; + iconOver: boxTitleCloseIconOver; + iconPosition: point(12px, -1px); + + rippleAreaPosition: point(0px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +reportReasonTopSkip: 8px; +reportReasonButton: SettingsButton(defaultSettingsButton) { + style: boxTextStyle; + padding: margins(62px, 7px, 8px, 7px); + iconLeft: 22px; +} + +defaultReportBox: ReportBox { + button: reportReasonButton; + label: boxLabel; + field: newGroupDescription; + spam: menuIconDelete; + fake: menuIconFake; + violence: menuIconViolence; + children: menuIconBlock; + pornography: menuIconPorn; + copyright: menuIconCopyright; + drugs: menuIconDrugs; + personal: menuIconPersonal; + other: menuIconReport; +} diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h new file mode 100644 index 000000000..2f9915679 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace ChatHelpers { + +struct ComposeFeatures { + bool sendAs = true; + bool ttlInfo = true; + bool botCommandSend = true; + bool silentBroadcastToggle = true; + bool attachBotsMenu = true; + bool inlineBots = true; + bool megagroupSet = true; + bool stickersSettings = true; + bool openStickerSets = true; + bool autocompleteHashtags = true; + bool autocompleteMentions = true; + bool autocompleteCommands = true; +}; + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h index ba2f391df..4a418c042 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h @@ -46,6 +46,8 @@ enum class WindowUsage { class Show : public Main::SessionShow { public: + virtual void activate() = 0; + [[nodiscard]] virtual bool paused(PauseReason reason) const = 0; [[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0; @@ -59,7 +61,7 @@ public: Data::FileOrigin origin, not_null photo) const = 0; - virtual void processChosenSticker(FileChosen chosen) const = 0; + virtual void processChosenSticker(FileChosen &&chosen) const = 0; [[nodiscard]] virtual Window::SessionController *resolveWindow( WindowUsage) const; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 19dd9c827..f41482c19 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -56,7 +56,7 @@ using Core::RecentEmojiDocument; class EmojiColorPicker final : public Ui::RpWidget { public: - EmojiColorPicker(QWidget *parent); + EmojiColorPicker(QWidget *parent, const style::EmojiPan &st); void showEmoji(EmojiPtr emoji); @@ -87,6 +87,8 @@ private: void updateSelected(); void setSelected(int newSelected); + const style::EmojiPan &_st; + bool _ignoreShow = false; QVector _variants; @@ -97,6 +99,7 @@ private: QSize _singleSize; QPoint _areaPosition; QPoint _innerPosition; + Ui::RoundRect _backgroundRect; Ui::RoundRect _overBg; bool _hiding = false; @@ -118,9 +121,13 @@ struct EmojiListWidget::RecentOne { RecentEmojiId id; }; -EmojiColorPicker::EmojiColorPicker(QWidget *parent) +EmojiColorPicker::EmojiColorPicker( + QWidget *parent, + const style::EmojiPan &st) : RpWidget(parent) -, _overBg(st::emojiPanRadius, st::emojiPanHover) { +, _st(st) +, _backgroundRect(st::emojiPanRadius, _st.bg) +, _overBg(st::emojiPanRadius, _st.overBg) { setMouseTracking(true); } @@ -176,8 +183,8 @@ void EmojiColorPicker::paintEvent(QPaintEvent *e) { p.drawPixmap(0, 0, _cache); return; } - Ui::Shadow::paint(p, inner, width(), st::emojiPanAnimation.shadow); - Ui::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners); + Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow); + _backgroundRect.paint(p, inner); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; @@ -388,6 +395,7 @@ EmojiListWidget::EmojiListWidget( descriptor.show, std::move(descriptor.paused)) , _show(std::move(descriptor.show)) +, _features(descriptor.features) , _mode(descriptor.mode) , _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1) , _premiumIcon(_mode == Mode::EmojiStatus @@ -397,8 +405,8 @@ EmojiListWidget::EmojiListWidget( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) , _overBg(st::emojiPanRadius, st().overBg) -, _collapsedBg(st::emojiPanExpand.height / 2, st::emojiPanHeaderFg) -, _picker(this) +, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) +, _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) { setMouseTracking(true); if (st().bg->c.alpha() > 0) { @@ -615,6 +623,10 @@ rpl::producer<> EmojiListWidget::jumpedToPremium() const { return _jumpedToPremium.events(); } +rpl::producer<> EmojiListWidget::escapes() const { + return _search ? _search->escapes() : rpl::never<>(); +} + void EmojiListWidget::prepareExpanding() { if (_search) { _searchExpandCache = _search->grab(); @@ -625,13 +637,14 @@ void EmojiListWidget::paintExpanding( Painter &p, QRect clip, int finalBottom, - float64 progress, + float64 geometryProgress, + float64 fullProgress, RectPart origin) { const auto searchShift = _search ? anim::interpolate( st().padding.top() - _search->height(), 0, - progress) + geometryProgress) : 0; const auto shift = clip.topLeft() + QPoint(0, searchShift); const auto adjusted = clip.translated(-shift); @@ -646,7 +659,7 @@ void EmojiListWidget::paintExpanding( p.translate(shift); p.setClipRect(adjusted); paint(p, ExpandingContext{ - .progress = progress, + .progress = fullProgress, .finalHeight = finalHeight, .expanding = true, }, adjusted); @@ -718,6 +731,7 @@ object_ptr EmojiListWidget::createFooter() { .paused = footerPaused, .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; @@ -1006,7 +1020,7 @@ void EmojiListWidget::validateEmojiPaintContext( st::stickerPanPremium1, st::stickerPanPremium2, 0.5) - : st::windowFg->c), + : st().textFg->c), .size = QSize(_customSingleSize, _customSingleSize), .now = crl::now(), .scale = context.progress, @@ -1067,7 +1081,7 @@ void EmojiListWidget::paint( - paintButtonGetWidth(p, info, buttonSelected, clip); if (info.section > 0 && clip.top() < info.rowsTop) { p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto titleText = (info.section < _staticCount) ? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now) : _custom[info.section - _staticCount].title; @@ -1086,7 +1100,7 @@ void EmojiListWidget::paint( } const auto textBaseline = top + st::emojiPanHeaderFont->ascent; p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawText(titleLeft, textBaseline, titleText); } if (clip.top() + clip.height() > info.rowsTop) { @@ -1178,7 +1192,7 @@ void EmojiListWidget::drawCollapsedBadge( const auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2; const auto buttony = position.y() + (_singleSize.height() - buttonh) / 2; _collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh)); - p.setPen(st::emojiPanBg); + p.setPen(this->st().bg); p.setFont(st.font); p.drawText( buttonx + (buttonw - textWidth) / 2, @@ -1454,7 +1468,8 @@ void EmojiListWidget::displaySet(uint64 setId) { } void EmojiListWidget::removeSet(uint64 setId) { - if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { + const auto &labelSt = st().boxLabel; + if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) { checkHideWithBox(std::move(box)); } } @@ -1527,9 +1542,10 @@ QRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const { if (_mode != Mode::Full) { return QRect(); } - const auto buttonw = st::stickerPanRemoveSet.rippleAreaPosition.x() - + st::stickerPanRemoveSet.rippleAreaSize; - const auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + const auto buttonw = removeSt.rippleAreaPosition.x() + + removeSt.rippleAreaSize; + const auto buttonh = removeSt.height; const auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw; const auto buttony = info.top + st::emojiPanRemoveTop; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1961,19 +1977,18 @@ int EmojiListWidget::paintButtonGetWidth( if (remove.isEmpty()) { return 0; } else if (remove.intersects(clip)) { + const auto &removeSt = st().removeSet; if (custom.ripple) { custom.ripple->paint( p, - remove.x() + st::stickerPanRemoveSet.rippleAreaPosition.x(), - remove.y() + st::stickerPanRemoveSet.rippleAreaPosition.y(), + remove.x() + removeSt.rippleAreaPosition.x(), + remove.y() + removeSt.rippleAreaPosition.y(), width()); if (custom.ripple->empty()) { custom.ripple.reset(); } } - const auto &icon = selected - ? st::stickerPanRemoveSet.iconOver - : st::stickerPanRemoveSet.icon; + const auto &icon = selected ? removeSt.iconOver : removeSt.icon; icon.paint( p, (remove.topLeft() @@ -2040,7 +2055,9 @@ void EmojiListWidget::updateSelected() { if (hasButton(section) && myrtlrect(buttonRect(section)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (section >= _staticCount && _mode == Mode::Full) { + } else if (_features.openStickerSets + && section >= _staticCount + && _mode == Mode::Full) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) { @@ -2154,13 +2171,12 @@ std::unique_ptr EmojiListWidget::createButtonRipple( && section < _staticCount + _custom.size()); const auto remove = hasRemoveButton(section); - const auto &st = remove - ? st::stickerPanRemoveSet.ripple - : st::emojiPanButton.ripple; + const auto &removeSt = st().removeSet; + const auto &st = remove ? removeSt.ripple : st::emojiPanButton.ripple; auto mask = remove ? Ui::RippleAnimation::EllipseMask(QSize( - st::stickerPanRemoveSet.rippleAreaSize, - st::stickerPanRemoveSet.rippleAreaSize)) + removeSt.rippleAreaSize, + removeSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, @@ -2174,7 +2190,7 @@ QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { return myrtlrect(buttonRect(section)).topLeft() + (hasRemoveButton(section) - ? st::stickerPanRemoveSet.rippleAreaPosition + ? st().removeSet.rippleAreaPosition : QPoint()); } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index fd307eda8..374b6e7b3 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "ui/widgets/tooltip.h" #include "ui/round_rect.h" @@ -84,6 +85,7 @@ struct EmojiListDescriptor { DocumentId, Fn)> customRecentFactory; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; class EmojiListWidget final @@ -123,6 +125,7 @@ public: [[nodiscard]] rpl::producer chosen() const; [[nodiscard]] rpl::producer customChosen() const; [[nodiscard]] rpl::producer<> jumpedToPremium() const; + [[nodiscard]] rpl::producer<> escapes() const; void provideRecent(const std::vector &customRecentList); @@ -131,7 +134,8 @@ public: Painter &p, QRect clip, int finalBottom, - float64 progress, + float64 geometryProgress, + float64 fullProgress, RectPart origin); base::unique_qptr fillContextMenu( @@ -345,6 +349,7 @@ private: void applyNextSearchQuery(); const std::shared_ptr _show; + const ComposeFeatures _features; Mode _mode = Mode::Full; std::unique_ptr _search; const int _staticCount = 0; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index b74e37708..a97d1fe42 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" +#include "ui/round_rect.h" #include "platform/platform_specific.h" #include "core/application.h" #include "base/event_filter.h" @@ -41,15 +42,133 @@ constexpr auto kAnimationDuration = crl::time(120); } // namespace +class SuggestionsWidget final : public Ui::RpWidget { +public: + SuggestionsWidget( + QWidget *parent, + const style::EmojiSuggestions &st, + not_null session, + bool suggestCustomEmoji, + Fn)> allowCustomWithoutPremium); + ~SuggestionsWidget(); + + void showWithQuery(SuggestionsQuery query, bool force = false); + void selectFirstResult(); + bool handleKeyEvent(int key); + + [[nodiscard]] rpl::producer toggleAnimated() const; + + struct Chosen { + QString emoji; + QString customData; + }; + [[nodiscard]] rpl::producer triggered() const; + +private: + struct Row { + Row(not_null emoji, const QString &replacement); + + Ui::Text::CustomEmoji *custom = nullptr; + DocumentData *document = nullptr; + not_null emoji; + QString replacement; + }; + struct Custom { + not_null document; + not_null emoji; + QString replacement; + }; + + bool eventHook(QEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void enterEventHook(QEnterEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void scrollByWheelEvent(not_null e); + void paintFadings(QPainter &p) const; + + [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; + [[nodiscard]] base::flat_multi_map lookupCustom( + const std::vector &rows) const; + [[nodiscard]] std::vector appendCustom( + std::vector rows); + [[nodiscard]] std::vector appendCustom( + std::vector rows, + const base::flat_multi_map &custom); + void resizeToRows(); + void setSelected( + int selected, + anim::type animated = anim::type::instant); + void setPressed(int pressed); + void clearMouseSelection(); + void clearSelection(); + void updateSelectedItem(); + void updateItem(int index); + [[nodiscard]] QRect inner() const; + [[nodiscard]] QPoint innerShift() const; + [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; + void selectByMouse(QPoint globalPosition); + bool triggerSelectedRow() const; + void triggerRow(const Row &row) const; + + [[nodiscard]] int scrollCurrent() const; + void scrollTo(int value, anim::type animated = anim::type::instant); + void stopAnimations(); + + [[nodiscard]] not_null resolveCustomEmoji( + not_null document); + void customEmojiRepaint(); + + const style::EmojiSuggestions &_st; + const not_null _session; + SuggestionsQuery _query; + std::vector _rows; + bool _suggestCustomEmoji = false; + Fn)> _allowCustomWithoutPremium; + + Ui::RoundRect _overRect; + + base::flat_map< + not_null, + std::unique_ptr> _customEmoji; + bool _repaintScheduled = false; + + std::optional _lastMousePosition; + bool _mouseSelection = false; + int _selected = -1; + int _pressed = -1; + + int _scrollValue = 0; + Ui::Animations::Simple _scrollAnimation; + Ui::Animations::Simple _selectedAnimation; + int _scrollMax = 0; + int _oneWidth = 0; + QMargins _padding; + + QPoint _mousePressPosition; + int _dragScrollStart = -1; + + rpl::event_stream _toggleAnimated; + rpl::event_stream _triggered; + +}; + SuggestionsWidget::SuggestionsWidget( QWidget *parent, + const style::EmojiSuggestions &st, not_null session, bool suggestCustomEmoji, Fn)> allowCustomWithoutPremium) : RpWidget(parent) +, _st(st) , _session(session) , _suggestCustomEmoji(suggestCustomEmoji) , _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium)) +, _overRect(st::roundRadiusSmall, _st.overBg) , _oneWidth(st::emojiSuggestionSize) , _padding(st::emojiSuggestionsPadding) { resize( @@ -284,7 +403,7 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { _repaintScheduled = false; const auto clip = e->rect(); - p.fillRect(clip, st::boxBg); + p.fillRect(clip, _st.bg); const auto shift = innerShift(); p.translate(-shift); @@ -298,15 +417,13 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { ? _pressed : _selectedAnimation.value(_selected); if (selected > -1.) { - Ui::FillRoundRect( + _overRect.paint( p, - QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth), - st::emojiPanHover, - Ui::StickerHoverCorners); + QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth)); } auto context = Ui::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = _st.textFg->c, .now = crl::now(), }; for (auto i = from; i != till; ++i) { @@ -338,9 +455,9 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { const auto rect = myrtlrect( shift.x(), 0, - st::emojiSuggestionsFadeLeft.width(), + _st.fadeLeft.width(), height()); - st::emojiSuggestionsFadeLeft.fill(p, rect); + _st.fadeLeft.fill(p, rect); p.setOpacity(1.); } const auto o_right = std::clamp( @@ -350,11 +467,11 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { if (o_right > 0.) { p.setOpacity(o_right); const auto rect = myrtlrect( - shift.x() + width() - st::emojiSuggestionsFadeRight.width(), + shift.x() + width() - _st.fadeRight.width(), 0, - st::emojiSuggestionsFadeRight.width(), + _st.fadeRight.width(), height()); - st::emojiSuggestionsFadeRight.fill(p, rect); + _st.fadeRight.fill(p, rect); p.setOpacity(1.); } } @@ -601,17 +718,17 @@ SuggestionsController::SuggestionsController( not_null field, not_null session, const Options &options) -: _field(field) +: _st(options.st ? *options.st : st::defaultEmojiSuggestions) +, _field(field) , _session(session) , _showExactTimer([=] { showWithQuery(getEmojiQuery()); }) , _options(options) { - _container = base::make_unique_q( - outer, - st::emojiSuggestionsDropdown); + _container = base::make_unique_q(outer, _st.dropdown); _container->setAutoHiding(false); _suggestions = _container->setOwnedWidget( object_ptr( _container, + _st, session, _options.suggestCustomEmoji, _options.allowCustomWithoutPremium)); @@ -661,6 +778,13 @@ SuggestionsController::SuggestionsController( updateForceHidden(); + _container->shownValue( + ) | rpl::filter([=](bool shown) { + return shown && !_shown; + }) | rpl::start_with_next([=] { + _container->hide(); + }, _container->lifetime()); + handleTextChange(); } @@ -749,6 +873,7 @@ void SuggestionsController::showWithQuery(SuggestionsQuery query) { const auto force = base::take(_keywordsRefreshed); _lastShownQuery = query; _suggestions->showWithQuery(_lastShownQuery, force); + _container->resizeToContent(); } SuggestionsQuery SuggestionsController::getEmojiQuery() { @@ -910,7 +1035,7 @@ void SuggestionsController::updateGeometry() { auto boundingRect = _container->parentWidget()->rect(); auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft; auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft(); - const auto padding = st::emojiSuggestionsDropdown.padding; + const auto padding = _st.dropdown.padding; const auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2; point -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _container->height()); if (rtl()) { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index 513fb518d..b89de0ad9 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct EmojiSuggestions; +} // namespace style + namespace Main { class Session; } // namespace Main @@ -29,125 +33,17 @@ class CustomEmoji; namespace Ui::Emoji { +class SuggestionsWidget; + using SuggestionsQuery = std::variant; -class SuggestionsWidget final : public Ui::RpWidget { -public: - SuggestionsWidget( - QWidget *parent, - not_null session, - bool suggestCustomEmoji, - Fn)> allowCustomWithoutPremium); - ~SuggestionsWidget(); - - void showWithQuery(SuggestionsQuery query, bool force = false); - void selectFirstResult(); - bool handleKeyEvent(int key); - - [[nodiscard]] rpl::producer toggleAnimated() const; - - struct Chosen { - QString emoji; - QString customData; - }; - [[nodiscard]] rpl::producer triggered() const; - -private: - struct Row { - Row(not_null emoji, const QString &replacement); - - Ui::Text::CustomEmoji *custom = nullptr; - DocumentData *document = nullptr; - not_null emoji; - QString replacement; - }; - struct Custom { - not_null document; - not_null emoji; - QString replacement; - }; - - bool eventHook(QEvent *e) override; - void paintEvent(QPaintEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void enterEventHook(QEnterEvent *e) override; - void leaveEventHook(QEvent *e) override; - - void scrollByWheelEvent(not_null e); - void paintFadings(QPainter &p) const; - - [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; - [[nodiscard]] base::flat_multi_map lookupCustom( - const std::vector &rows) const; - [[nodiscard]] std::vector appendCustom( - std::vector rows); - [[nodiscard]] std::vector appendCustom( - std::vector rows, - const base::flat_multi_map &custom); - void resizeToRows(); - void setSelected( - int selected, - anim::type animated = anim::type::instant); - void setPressed(int pressed); - void clearMouseSelection(); - void clearSelection(); - void updateSelectedItem(); - void updateItem(int index); - [[nodiscard]] QRect inner() const; - [[nodiscard]] QPoint innerShift() const; - [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; - void selectByMouse(QPoint globalPosition); - bool triggerSelectedRow() const; - void triggerRow(const Row &row) const; - - [[nodiscard]] int scrollCurrent() const; - void scrollTo(int value, anim::type animated = anim::type::instant); - void stopAnimations(); - - [[nodiscard]] not_null resolveCustomEmoji( - not_null document); - void customEmojiRepaint(); - - const not_null _session; - SuggestionsQuery _query; - std::vector _rows; - bool _suggestCustomEmoji = false; - Fn)> _allowCustomWithoutPremium; - - base::flat_map< - not_null, - std::unique_ptr> _customEmoji; - bool _repaintScheduled = false; - - std::optional _lastMousePosition; - bool _mouseSelection = false; - int _selected = -1; - int _pressed = -1; - - int _scrollValue = 0; - Ui::Animations::Simple _scrollAnimation; - Ui::Animations::Simple _selectedAnimation; - int _scrollMax = 0; - int _oneWidth = 0; - QMargins _padding; - - QPoint _mousePressPosition; - int _dragScrollStart = -1; - - rpl::event_stream _toggleAnimated; - rpl::event_stream _triggered; - -}; - class SuggestionsController { public: struct Options { bool suggestExactFirstWord = true; bool suggestCustomEmoji = false; Fn)> allowCustomWithoutPremium; + const style::EmojiSuggestions *st = nullptr; }; SuggestionsController( @@ -189,6 +85,7 @@ private: bool fieldFilter(not_null event); bool outerFilter(not_null event); + const style::EmojiSuggestions &_st; bool _shown = false; bool _forceHidden = false; int _queryStartPosition = 0; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 8b24df18a..7f5c7a52e 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -69,6 +69,7 @@ public: Inner( std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, @@ -126,11 +127,13 @@ private: const std::shared_ptr _show; const not_null _session; + const style::EmojiPan &_st; const not_null _parent; const not_null _mrows; const not_null _hrows; const not_null _brows; const not_null _srows; + Ui::RoundRect _overBg; rpl::lifetime _stickersLifetime; std::weak_ptr _lottieRenderer; base::unique_qptr _menu; @@ -192,10 +195,12 @@ FieldAutocomplete::FieldAutocomplete( FieldAutocomplete::FieldAutocomplete( QWidget *parent, - std::shared_ptr show) + std::shared_ptr show, + const style::EmojiPan *stOverride) : RpWidget(parent) , _show(std::move(show)) , _session(&_show->session()) +, _st(stOverride ? *stOverride : st::defaultEmojiPan) , _scroll(this) { hide(); @@ -204,6 +209,7 @@ FieldAutocomplete::FieldAutocomplete( _inner = _scroll->setOwnedWidget( object_ptr( _show, + _st, this, &_mrows, &_hrows, @@ -285,7 +291,7 @@ void FieldAutocomplete::paintEvent(QPaintEvent *e) { return; } - p.fillRect(rect(), st::mentionBg); + p.fillRect(rect(), _st.bg); } void FieldAutocomplete::showFiltered( @@ -679,6 +685,7 @@ void FieldAutocomplete::recount(bool resetScroll) { } else if (!_brows.empty()) { h = _brows.size() * st::mentionHeight; } + h += _st.autocompleteBottomSkip; if (_inner->width() != _boundings.width() || _inner->height() != h) { _inner->resize(_boundings.width(), h); @@ -686,9 +693,14 @@ void FieldAutocomplete::recount(bool resetScroll) { if (h > _boundings.height()) h = _boundings.height(); if (h > maxh) h = maxh; if (width() != _boundings.width() || height() != h) { - setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h); + setGeometry( + _boundings.x(), + _boundings.y() + _boundings.height() - h, + _boundings.width(), + h); _scroll->resize(_boundings.width(), h); - } else if (y() != _boundings.y() + _boundings.height() - h) { + } else if (x() != _boundings.x() + || y() != _boundings.y() + _boundings.height() - h) { move(_boundings.x(), _boundings.y() + _boundings.height() - h); } if (resetScroll) st = 0; @@ -817,6 +829,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { FieldAutocomplete::Inner::Inner( std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, @@ -824,14 +837,16 @@ FieldAutocomplete::Inner::Inner( not_null srows) : _show(std::move(show)) , _session(&_show->session()) +, _st(st) , _parent(parent) , _mrows(mrows) , _hrows(hrows) , _brows(brows) , _srows(srows) +, _overBg(st::roundRadiusSmall, _st.overBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + _st.pathBg, + _st.pathFg, [=] { update(); })) , _premiumMark(_session) , _previewTimer([=] { showPreview(); }) { @@ -900,7 +915,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { if (_sel == index) { QPoint tl(pos); if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width()); - Ui::FillRoundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, st::stickerPanSize)); } media->checkStickerSmall(); @@ -1366,8 +1381,10 @@ void FieldAutocomplete::Inner::setSel(int sel, bool scroll) { int32 row = _sel / _stickersPerRow; const auto padding = st::stickerPanPadding; _scrollToRequested.fire({ - padding + row * st::stickerPanSize.height(), - padding + (row + 1) * st::stickerPanSize.height() }); + (row ? padding : 0) + row * st::stickerPanSize.height(), + (padding + + (row + 1) * st::stickerPanSize.height() + + _st.autocompleteBottomSkip) }); } } } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 1bb75f122..2606dc1f2 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/object_ptr.h" +namespace style { +struct EmojiPan; +} // namespace style + namespace Ui { class PopupMenu; class ScrollArea; @@ -53,7 +57,8 @@ public: not_null controller); FieldAutocomplete( QWidget *parent, - std::shared_ptr show); + std::shared_ptr show, + const style::EmojiPan *stOverride = nullptr); ~FieldAutocomplete(); [[nodiscard]] std::shared_ptr uiShow() const; @@ -153,6 +158,7 @@ private: const std::shared_ptr _show; const not_null _session; + const style::EmojiPan &_st; QPixmap _cache; MentionRows _mrows; HashtagRows _hrows; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index b772db1de..3fc379aa3 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -59,7 +59,8 @@ constexpr auto kMinAfterScrollDelay = crl::time(33); void AddGifAction( Fn &&, const style::icon*)> callback, std::shared_ptr show, - not_null document) { + not_null document, + const style::ComposeIcons *iconsOverride) { if (!document->isGifv()) { return; } @@ -69,6 +70,9 @@ void AddGifAction( const auto text = (saved ? tr::lng_context_delete_gif : tr::lng_context_save_gif)(tr::now); + const auto &icons = iconsOverride + ? *iconsOverride + : st::defaultComposeIcons; callback(text, [=] { Api::ToggleSavedGif( show, @@ -82,7 +86,7 @@ void AddGifAction( document->session().local().writeSavedGifs(); } data.stickers().notifySavedGifsUpdated(); - }, saved ? &st::menuIconDelete : &st::menuIconGif); + }, saved ? &icons.menuGifRemove : &icons.menuGifAdd); } GifsListWidget::GifsListWidget( @@ -100,7 +104,7 @@ GifsListWidget::GifsListWidget( GifsListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, + descriptor.st ? *descriptor.st : st::defaultEmojiPan, descriptor.show, descriptor.paused) , _show(std::move(descriptor.show)) @@ -170,6 +174,7 @@ object_ptr GifsListWidget::createFooter() { .paused = pausedMethod(), .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; _chosenSetId = Data::Stickers::RecentSetId; @@ -334,7 +339,7 @@ void GifsListWidget::inlineResultsDone(const MTPmessages_BotResults &result) { void GifsListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintInlineItems(p, clip); } @@ -382,18 +387,18 @@ base::unique_qptr GifsListWidget::fillContextMenu( return nullptr; } - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto send = [=, selected = _selected](Api::SendOptions options) { selectInlineResult(selected, options, true); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), SendMenu::DefaultScheduleCallback(this, type, send), - SendMenu::DefaultWhenOnlineCallback(send)); + SendMenu::DefaultWhenOnlineCallback(send), + icons); if (const auto item = _mosaic.maybeItemAt(_selected)) { const auto document = item->getDocument() @@ -406,7 +411,7 @@ base::unique_qptr GifsListWidget::fillContextMenu( const style::icon *icon) { menu->addAction(text, std::move(done), icon); }; - AddGifAction(std::move(callback), _show, document); + AddGifAction(std::move(callback), _show, document, icons); } } return menu; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index d21c8e531..84e20c864 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct ComposeIcons; +} // namespace style + namespace Api { struct SendOptions; } // namespace Api @@ -48,7 +52,8 @@ namespace ChatHelpers { void AddGifAction( Fn &&, const style::icon*)> callback, std::shared_ptr show, - not_null document); + not_null document, + const style::ComposeIcons *iconsOverride = nullptr); class StickersListFooter; struct StickerIcon; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 36696ccab..b0e263c4a 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "base/qt/qt_common_adapters.h" #include @@ -497,7 +498,8 @@ InlineBotQuery ParseInlineBotQuery( } AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field) { + not_null field, + ChatHelpers::ComposeFeatures features) { auto result = AutocompleteQuery(); const auto cursor = field->textCursor(); @@ -529,6 +531,9 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( const auto text = fragment.text(); for (auto i = position - fragmentPosition; i != 0; --i) { if (text[i - 1] == '@') { + if (!features.autocompleteMentions) { + return {}; + } if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); @@ -539,12 +544,18 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( } return result; } else if (text[i - 1] == '#') { + if (!features.autocompleteHashtags) { + return {}; + } if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); } return result; } else if (text[i - 1] == '/') { + if (!features.autocompleteCommands) { + return {}; + } if (i < 2 && !fragmentPosition) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 727e51a5b..13012557b 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "base/timer.h" #include "base/qt_connection.h" +#include "chat_helpers/compose/compose_features.h" #ifndef TDESKTOP_DISABLE_SPELLCHECK #include "boxes/dictionaries_manager.h" @@ -90,7 +91,8 @@ struct AutocompleteQuery { bool fromStart = false; }; AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field); + not_null field, + ChatHelpers::ComposeFeatures features); class MessageLinksParser : private QObject { public: diff --git a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp index fed572b75..dcebbfdc4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp @@ -128,7 +128,7 @@ void DicePack::generateLocal(int index, const QString &name) { QByteArray(), nullptr, SendMediaType::File, - FileLoadTo(0, {}, 0, 0, 0), + FileLoadTo(0, {}, {}, 0), {}, false); task.process({ .generateGoodThumbnail = false }); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index a846b5362..15e2e8c34 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -292,7 +292,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) descriptor.st ? *descriptor.st : st::defaultEmojiPan) , _session(descriptor.session) , _paused(descriptor.paused) -, _settingsButtonVisible(descriptor.settingsButtonVisible) +, _features(descriptor.features) , _iconState([=] { update(); }) , _subiconState([=] { update(); }) , _selectionBg(st::emojiPanRadius, st().categoriesBgOver) @@ -300,7 +300,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) setMouseTracking(true); _iconsLeft = st().iconSkip - + (_settingsButtonVisible ? st().iconWidth : 0); + + (_features.stickersSettings ? st().iconWidth : 0); _iconsRight = st().iconSkip; _session->downloaderTaskFinished( @@ -618,7 +618,7 @@ void StickersListFooter::paint( return; } - if (_settingsButtonVisible && !hasOnlyFeaturedSets()) { + if (_features.stickersSettings) { paintStickerSettingsIcon(p); } @@ -1012,12 +1012,12 @@ void StickersListFooter::updateSelected() { if (rtl()) x = width() - x; const auto settingsLeft = _iconsLeft - _singleWidth; auto newOver = OverState(SpecialOver::None); - if (_settingsButtonVisible + if (_features.stickersSettings && x >= settingsLeft && x < settingsLeft + _singleWidth && y >= _iconsTop && y < _iconsTop + st().footer) { - if (!_icons.empty() && !hasOnlyFeaturedSets()) { + if (!_icons.empty()) { newOver = SpecialOver::Settings; } } else if (!_icons.empty()) { @@ -1161,17 +1161,11 @@ void StickersListFooter::refreshSubiconsGeometry() { updateEmojiWidthCallback(); } -bool StickersListFooter::hasOnlyFeaturedSets() const { - return (_icons.size() == 1) - && (_icons[0].setId == Data::Stickers::FeaturedSetId); -} - void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const { const auto settingsLeft = _iconsLeft - _singleWidth; - st::stickersSettings.paint( + st().icons.settings.paint( p, - settingsLeft - + (_singleWidth - st::stickersSettings.width()) / 2, + (settingsLeft + (_singleWidth - st().icons.settings.width()) / 2), _iconsTop + st::emojiCategoryIconTop, width()); } @@ -1351,7 +1345,7 @@ void StickersListFooter::paintSetIconToCache( const auto y = (st().footer - icon.pixh) / 2; if (icon.custom) { icon.custom->paint(p, Ui::Text::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = st().textFg->c, .size = QSize(icon.pixw, icon.pixh), .now = now, .scale = context.progress, @@ -1411,22 +1405,22 @@ void StickersListFooter::paintSetIconToCache( using Section = Ui::Emoji::Section; const auto sectionIcon = [&](Section section, bool active) { const auto icons = std::array{ - &st::emojiRecent, - &st::emojiRecentActive, - &st::emojiPeople, - &st::emojiPeopleActive, - &st::emojiNature, - &st::emojiNatureActive, - &st::emojiFood, - &st::emojiFoodActive, - &st::emojiActivity, - &st::emojiActivityActive, - &st::emojiTravel, - &st::emojiTravelActive, - &st::emojiObjects, - &st::emojiObjectsActive, - &st::emojiSymbols, - &st::emojiSymbolsActive, + &st().icons.recent, + &st().icons.recentActive, + &st().icons.people, + &st().icons.peopleActive, + &st().icons.nature, + &st().icons.natureActive, + &st().icons.food, + &st().icons.foodActive, + &st().icons.activity, + &st().icons.activityActive, + &st().icons.travel, + &st().icons.travelActive, + &st().icons.objects, + &st().icons.objectsActive, + &st().icons.symbols, + &st().icons.symbolsActive, }; const auto index = int(section) * 2 + (active ? 1 : 0); @@ -1464,15 +1458,8 @@ void StickersListFooter::paintSetIconToCache( } else { paintOne(0, [&] { const auto selected = (info.index == _iconState.selected); - if (icon.setId == Data::Stickers::FeaturedSetId) { - const auto &stickers = _session->data().stickers(); - return stickers.featuredSetsUnreadCount() - ? &st::stickersTrendingUnread - : &st::stickersTrending; - //} else if (setId == Stickers::FavedSetId) { - // return &st::stickersFaved; - } else if (icon.setId == AllEmojiSectionSetId()) { - return &st::emojiPeople; + if (icon.setId == AllEmojiSectionSetId()) { + return &st().icons.people; } else if (const auto section = SetIdEmojiSection(icon.setId)) { return sectionIcon(*section, selected); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index 1a08f6853..f7a0afc1d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "media/clip/media_clip_reader.h" +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" +#include "media/clip/media_clip_reader.h" #include "mtproto/sender.h" #include "ui/dpr/dpr_image.h" #include "ui/round_rect.h" @@ -116,8 +117,8 @@ public: not_null session; Fn paused; not_null parent; - bool settingsButtonVisible = false; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; explicit StickersListFooter(Descriptor &&descriptor); @@ -130,7 +131,6 @@ public: uint64 activeSetId, Fn()> renderer, ValidateIconAnimations animations); - [[nodiscard]] bool hasOnlyFeaturedSets() const; void leaveToChildEvent(QEvent *e, QWidget *child) override; @@ -270,7 +270,7 @@ private: const not_null _session; const Fn _paused; - const bool _settingsButtonVisible = false; + const ComposeFeatures _features; static constexpr auto kVisibleIconsCount = 8; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index dc7194fee..c1ffda95c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/stickers_list_widget.h" +#include "core/application.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" @@ -181,11 +182,13 @@ StickersListWidget::StickersListWidget( StickersListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, + descriptor.st ? *descriptor.st : st::defaultEmojiPan, descriptor.show, descriptor.paused) , _mode(descriptor.mode) , _show(std::move(descriptor.show)) +, _features(descriptor.features) +, _overBg(st::roundRadiusSmall, st().overBg) , _api(&session().mtp()) , _localSetsManager(std::make_unique(&session())) , _section(Section::Stickers) @@ -203,8 +206,8 @@ StickersListWidget::StickersListWidget( ImageRoundRadius::Small, st::stickerGroupCategoryAdd.textBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + st().pathBg, + st().pathFg, [=] { update(); })) , _megagroupSetAbout(st::columnMinimalWidthThird - st::emojiScroll.width - st().headerLeft) , _addText(tr::lng_stickers_featured_add(tr::now).toUpper()) @@ -221,9 +224,15 @@ StickersListWidget::StickersListWidget( } _settings->addClickHandler([=] { - using Section = StickersBox::Section; - _show->showBox( - Box(_show, Section::Installed, _isMasks)); + if (const auto window = _show->resolveWindow( + WindowUsage::PremiumPromo)) { + // While media viewer can't show StickersBox. + using Section = StickersBox::Section; + window->show( + Box(_show, Section::Installed, _isMasks)); + Core::App().hideMediaView(); + Window::ActivateWindow(window); + } }); session().downloaderTaskFinished( @@ -286,7 +295,8 @@ object_ptr StickersListWidget::createFooter() { .session = &session(), .paused = footerPaused, .parent = this, - .settingsButtonVisible = true, + .st = &st(), + .features = _features, }); _footer = result; @@ -297,7 +307,7 @@ object_ptr StickersListWidget::createFooter() { _footer->openSettingsRequests( ) | rpl::start_with_next([=] { - const auto onlyFeatured = _footer->hasOnlyFeaturedSets(); + const auto onlyFeatured = !_isMasks && _mySets.empty(); _show->showBox(Box( _show, (onlyFeatured @@ -846,7 +856,7 @@ QRect StickersListWidget::stickerRect(int section, int sel) { void StickersListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintStickers(p, clip); } @@ -907,7 +917,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto add = featuredAddRect(info); int checkx = add.left() + (add.width() - st::stickersFeaturedInstalled.width()) / 2; int checky = add.top() + (add.height() - st::stickersFeaturedInstalled.height()) / 2; - st::stickersFeaturedInstalled.paint(p, QPoint(checkx, checky), width()); + st().trendingInstalled.paint(p, QPoint(checkx, checky), width()); } if (set.flags & SetFlag::Unread) { widthForTitle -= st::stickersFeaturedUnreadSize + st::stickersFeaturedUnreadSkip; @@ -920,12 +930,12 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::stickersTrendingHeaderFont); - p.setPen(st::stickersTrendingHeaderFg); + p.setPen(st().trendingHeaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingHeaderTop, width(), titleText, titleWidth); if (set.flags & SetFlag::Unread) { p.setPen(Qt::NoPen); - p.setBrush(st::stickersFeaturedUnreadBg); + p.setBrush(st().trendingUnreadFg); { PainterHighQualityEnabler hq(p); @@ -935,7 +945,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto statusText = (count > 0) ? tr::lng_stickers_count(tr::now, lt_count, count) : tr::lng_contacts_loading(tr::now); p.setFont(st::stickersTrendingSubheaderFont); - p.setPen(st::stickersTrendingSubheaderFg); + p.setPen(st().trendingSubheaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingSubheaderTop, width(), statusText); if (info.rowsTop >= clip.y() + clip.height()) { @@ -962,13 +972,14 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { if (hasRemoveButton(info.section)) { auto remove = removeButtonRect(info); auto selected = selectedButton ? (selectedButton->section == info.section) : false; + const auto &removeSt = st().removeSet; if (set.ripple) { - set.ripple->paint(p, remove.x() + st::stickerPanRemoveSet.rippleAreaPosition.x(), remove.y() + st::stickerPanRemoveSet.rippleAreaPosition.y(), width()); + set.ripple->paint(p, remove.x() + removeSt.rippleAreaPosition.x(), remove.y() + removeSt.rippleAreaPosition.y(), width()); if (set.ripple->empty()) { set.ripple.reset(); } } - const auto &icon = selected ? st::stickerPanRemoveSet.iconOver : st::stickerPanRemoveSet.icon; + const auto &icon = selected ? removeSt.iconOver : removeSt.icon; icon.paint( p, remove.x() + (remove.width() - icon.width()) / 2, @@ -982,7 +993,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st().headerTop, width(), titleText, titleWidth); } if (clip.top() + clip.height() <= info.rowsTop) { @@ -1107,7 +1118,7 @@ int StickersListWidget::megagroupSetInfoLeft() const { } void StickersListWidget::paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected) { - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto infoLeft = megagroupSetInfoLeft(); _megagroupSetAbout.drawLeft(p, infoLeft, y, width() - infoLeft, width()); @@ -1344,7 +1355,7 @@ void StickersListWidget::paintSticker( if (selected) { auto tl = pos; if (rtl()) tl.setX(width() - tl.x() - _singleSize.width()); - Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, _singleSize)); } media->checkStickerSmall(); @@ -1482,8 +1493,9 @@ QRect StickersListWidget::removeButtonRect(int index) const { } QRect StickersListWidget::removeButtonRect(const SectionInfo &info) const { - auto buttonw = st::stickerPanRemoveSet.width; - auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + auto buttonw = removeSt.width; + auto buttonh = removeSt.height; auto buttonx = stickersRight() - buttonw; auto buttony = info.top + (st().header - buttonh) / 2; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1560,10 +1572,11 @@ std::unique_ptr StickersListWidget::createButtonRipple(int std::move(mask), [this, section] { rtlupdate(featuredAddRect(section)); }); } - auto maskSize = QSize(st::stickerPanRemoveSet.rippleAreaSize, st::stickerPanRemoveSet.rippleAreaSize); + const auto &removeSt = st().removeSet; + auto maskSize = QSize(removeSt.rippleAreaSize, removeSt.rippleAreaSize); auto mask = Ui::RippleAnimation::EllipseMask(maskSize); return std::make_unique( - st::stickerPanRemoveSet.ripple, + removeSt.ripple, std::move(mask), [this, section] { rtlupdate(removeButtonRect(section)); }); } @@ -1574,7 +1587,8 @@ QPoint StickersListWidget::buttonRippleTopLeft(int section) const { if (shownSets()[section].externalLayout) { return myrtlrect(featuredAddRect(section)).topLeft(); } - return myrtlrect(removeButtonRect(section)).topLeft() + st::stickerPanRemoveSet.rippleAreaPosition; + return myrtlrect(removeButtonRect(section)).topLeft() + + st().removeSet.rippleAreaPosition; } void StickersListWidget::showStickerSetBox(not_null document) { @@ -1603,9 +1617,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( auto &set = sets[section]; Assert(index >= 0 && index < set.stickers.size()); - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto document = set.stickers[sticker->index].document; const auto send = [=](Api::SendOptions options) { @@ -1617,12 +1629,14 @@ base::unique_qptr StickersListWidget::fillContextMenu( : messageSentAnimationInfo(section, index, document), }); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), SendMenu::DefaultScheduleCallback(this, type, send), - SendMenu::DefaultWhenOnlineCallback(send)); + SendMenu::DefaultWhenOnlineCallback(send), + icons); const auto show = _show; const auto toggleFavedSticker = [=] { @@ -1637,11 +1651,13 @@ base::unique_qptr StickersListWidget::fillContextMenu( ? tr::lng_faved_stickers_remove : tr::lng_faved_stickers_add)(tr::now), toggleFavedSticker, - isFaved ? &st::menuIconUnfave : &st::menuIconFave); + isFaved ? &icons->menuUnfave : &icons->menuFave); - menu->addAction(tr::lng_context_pack_info(tr::now), [=] { - showStickerSetBox(document); - }, &st::menuIconStickers); + if (_features.openStickerSets) { + menu->addAction(tr::lng_context_pack_info(tr::now), [=] { + showStickerSetBox(document); + }, &icons->menuStickerSet); + } if (const auto id = set.id; id == Data::Stickers::RecentSetId) { menu->addAction(tr::lng_recent_stickers_remove(tr::now), [=] { @@ -1649,7 +1665,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( document, Data::FileOriginStickerSet(id, 0), false); - }, &st::menuIconDelete); + }, &icons->menuRecentRemove); } return menu; } @@ -1707,7 +1723,8 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { return; } const auto document = set.stickers[sticker->index].document; - if (e->modifiers() & Qt::ControlModifier) { + if (_features.openStickerSets + && (e->modifiers() & Qt::ControlModifier)) { showStickerSetBox(document); } else { auto settings = &AyuSettings::getInstance(); @@ -1884,12 +1901,6 @@ void StickersListWidget::processPanelHideFinished() { if (_footer) { _footer->clearHeavyData(); } - // Preserve panel state through visibility toggles. - //// Reset to the recent stickers section. - //if (_section == Section::Featured && (!_footer || !_footer->hasOnlyFeaturedSets())) { - // setSection(Section::Stickers); - // validateSelectedIcon(ValidateIconAnimations::None); - //} } void StickersListWidget::setSection(Section section) { @@ -2019,9 +2030,6 @@ void StickersListWidget::refreshSettingsVisibility() { void StickersListWidget::refreshFooterIcons() { refreshIcons(ValidateIconAnimations::None); - if (_footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } void StickersListWidget::preloadImages() { @@ -2089,9 +2097,6 @@ void StickersListWidget::refreshRecent() { if (_section == Section::Stickers) { refreshRecentStickers(); } - if (_footer && _footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } auto StickersListWidget::collectRecentStickers() -> std::vector { @@ -2228,7 +2233,7 @@ void StickersListWidget::refreshFavedStickers() { } void StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) { - if (!_megagroupSet || _isMasks) { + if (!_features.megagroupSet || !_megagroupSet || _isMasks) { return; } auto canEdit = _megagroupSet->canEditStickers(); @@ -2381,10 +2386,12 @@ void StickersListWidget::updateSelected() { newSelected = OverButton{ section }; } else if (featuredHasAddButton(section) && myrtlrect(featuredAddRect(info)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (!(sets[section].flags & SetFlag::Special)) { + } else if (_features.openStickerSets + && !(sets[section].flags & SetFlag::Special)) { newSelected = OverSet{ section }; - } else if (sets[section].id == Data::Stickers::MegagroupSetId - && (_megagroupSet->canEditStickers() || !sets[section].stickers.empty())) { + } else if ((sets[section].id == Data::Stickers::MegagroupSetId) + && (_megagroupSet->canEditStickers() + || !sets[section].stickers.empty())) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom && sx >= 0) { @@ -2651,10 +2658,12 @@ void StickersListWidget::removeMegagroupSet(bool locally) { close(); }), .cancelled = [](Fn &&close) { close(); }, + .labelStyle = &st().boxLabel, })); } void StickersListWidget::removeSet(uint64 setId) { + const auto &st = this->st().boxLabel; if (setId == Data::Stickers::MegagroupSetId) { const auto &sets = shownSets(); const auto i = ranges::find(sets, setId, &Set::id); @@ -2662,7 +2671,7 @@ void StickersListWidget::removeSet(uint64 setId) { const auto removeLocally = i->stickers.empty() || !_megagroupSet->canEditStickers(); removeMegagroupSet(removeLocally); - } else if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { + } else if (auto box = MakeConfirmRemoveSetBox(&session(), st, setId)) { checkHideWithBox(std::move(box)); } } @@ -2687,6 +2696,7 @@ StickersListWidget::~StickersListWidget() = default; object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId) { const auto &sets = session->data().stickers().sets(); const auto it = sets.find(setId); @@ -2753,6 +2763,7 @@ object_ptr MakeConfirmRemoveSetBox( } }, .confirmText = tr::lng_stickers_remove_pack_confirm(), + .labelStyle = &st, }); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index afde28b81..6e2c2fa04 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "data/stickers/data_stickers.h" #include "ui/round_rect.h" @@ -50,6 +51,7 @@ enum class Notification; namespace style { struct EmojiPan; +struct FlatLabel; } // namespace style namespace ChatHelpers { @@ -70,6 +72,7 @@ struct StickersListDescriptor { StickersListMode mode = StickersListMode::Full; Fn paused; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; class StickersListWidget final : public TabbedSelector::Inner { @@ -351,6 +354,8 @@ private: const Mode _mode; const std::shared_ptr _show; + const ComposeFeatures _features; + Ui::RoundRect _overBg; std::unique_ptr _search; MTP::Sender _api; std::unique_ptr _localSetsManager; @@ -423,6 +428,7 @@ private: [[nodiscard]] object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId); } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index 5321e649e..e8ec060fb 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -240,7 +240,7 @@ void TabbedPanel::paintEvent(QPaintEvent *e) { hideFinished(); } else { if (!_cache.isNull()) _cache = QPixmap(); - Ui::Shadow::paint(p, innerRect(), width(), st::emojiPanAnimation.shadow); + Ui::Shadow::paint(p, innerRect(), width(), _selector->st().showAnimation.shadow); } } @@ -362,7 +362,11 @@ void TabbedPanel::startShowAnimation() { if (!_a_show.animating()) { auto image = grabForAnimation(); - _showAnimation = std::make_unique(st::emojiPanAnimation, _dropDown ? Ui::PanelAnimation::Origin::TopRight : Ui::PanelAnimation::Origin::BottomRight); + _showAnimation = std::make_unique( + _selector->st().showAnimation, + (_dropDown + ? Ui::PanelAnimation::Origin::TopRight + : Ui::PanelAnimation::Origin::BottomRight)); auto inner = rect().marginsRemoved(st::emojiPanMargins); _showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor())); _showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius)); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h index 153c19581..fcedb5efc 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h @@ -30,7 +30,7 @@ struct TabbedPanelDescriptor { Window::SessionController *regularWindow = nullptr; object_ptr ownedSelector = { nullptr }; TabbedSelector *nonOwnedSelector = nullptr; -};; +}; class TabbedPanel : public Ui::RpWidget { public: diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 463b35f16..43b08c4a4 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -50,7 +50,7 @@ public: void setFinalImages(Direction direction, QImage &&left, QImage &&right, QRect inner, bool wasSectionIcons); void start(); - void paintFrame(QPainter &p, float64 dt, float64 opacity); + void paintFrame(QPainter &p, const style::EmojiPan &st, float64 dt, float64 opacity); private: Direction _direction = Direction::LeftToRight; @@ -131,7 +131,11 @@ void TabbedSelector::SlideAnimation::start() { _frameIntsPerLineAdd = (_width - _innerWidth) + _frameIntsPerLineAdded; } -void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 opacity) { +void TabbedSelector::SlideAnimation::paintFrame( + QPainter &p, + const style::EmojiPan &st, + float64 dt, + float64 opacity) { Expects(started()); Expects(dt >= 0.); @@ -168,8 +172,8 @@ void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 { auto p = QPainter(&_frame); p.setOpacity(opacity); - p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st::emojiPanBg); - p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st::emojiPanCategories : st::emojiPanBg); + p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st.bg); + p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st.categoriesBg : st.bg); p.setCompositionMode(QPainter::CompositionMode_SourceOver); if (leftTo > _innerLeft) { p.setOpacity(opacity * leftAlpha); @@ -325,11 +329,25 @@ TabbedSelector::TabbedSelector( std::shared_ptr show, PauseReason level, Mode mode) +: TabbedSelector(parent, { + .show = std::move(show), + .st = (mode == Mode::EmojiStatus + ? st::statusEmojiPan + : st::defaultEmojiPan), + .level = level, + .mode = mode, +}) { +} + +TabbedSelector::TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor) : RpWidget(parent) -, _st((mode == Mode::EmojiStatus) ? st::statusEmojiPan : st::defaultEmojiPan) -, _show(std::move(show)) -, _level(level) -, _mode(mode) +, _st(descriptor.st) +, _features(descriptor.features) +, _show(std::move(descriptor.show)) +, _level(descriptor.level) +, _mode(descriptor.mode) , _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg)) , _categoriesRounding( Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.categoriesBg)) @@ -445,10 +463,10 @@ TabbedSelector::TabbedSelector( ) | rpl::start_with_next([=] { _panelRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanBg); + _st.bg); _categoriesRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanCategories); + _st.categoriesBg); }, lifetime()); if (hasEmojiTab()) { @@ -469,6 +487,10 @@ TabbedSelector::TabbedSelector( TabbedSelector::~TabbedSelector() = default; +const style::EmojiPan &TabbedSelector::st() const { + return _st; +} + Main::Session &TabbedSelector::session() const { return _show->session(); } @@ -493,6 +515,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { : EmojiMode::Full), .paused = paused, .st = &_st, + .features = _features, }); } case SelectorTab::Stickers: { @@ -503,6 +526,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .mode = StickersMode::Full, .paused = paused, .st = &_st, + .features = _features, }); } case SelectorTab::Gifs: { @@ -521,6 +545,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .mode = StickersMode::Masks, .paused = paused, .st = &_st, + .features = _features, }); } } @@ -704,10 +729,10 @@ void TabbedSelector::paintSlideFrame(QPainter &p) { if (_roundRadius > 0) { paintBgRoundedPart(p); } else if (_tabsSlider) { - p.fillRect(0, 0, width(), _tabsSlider->height(), st::emojiPanBg); + p.fillRect(0, 0, width(), _tabsSlider->height(), _st.bg); } auto slideDt = _a_slide.value(1.); - _slideAnimation->paintFrame(p, slideDt, 1.); + _slideAnimation->paintFrame(p, _st, slideDt, 1.); } void TabbedSelector::paintBgRoundedPart(QPainter &p) { @@ -716,7 +741,7 @@ void TabbedSelector::paintBgRoundedPart(QPainter &p) { : _tabsSlider ? QRect(0, 0, width(), _tabsSlider->height()) : QRect(0, 0, width(), _roundRadius); - Ui::FillRoundRect(p, fill, st::emojiPanBg, { + Ui::FillRoundRect(p, fill, _st.bg, { .p = { _dropDown ? QPixmap() : _panelRounding.p[0], _dropDown ? QPixmap() : _panelRounding.p[1], @@ -765,10 +790,10 @@ void TabbedSelector::paintContent(QPainter &p) { sidesTop, st::emojiScroll.width, sidesHeight), - st::emojiPanBg); + _st.bg); p.fillRect( myrtlrect(0, sidesTop, st::emojiPanRadius, sidesHeight), - st::emojiPanBg); + _st.bg); } } @@ -1030,7 +1055,7 @@ void TabbedSelector::setAllowEmojiWithoutPremium(bool allow) { } void TabbedSelector::createTabsSlider() { - _tabsSlider.create(this, st::emojiTabs); + _tabsSlider.create(this, _st.tabs); fillTabsSliderSections(); @@ -1324,7 +1349,7 @@ void TabbedSelector::Inner::paintEmptySearchResults( iconTop + icon.height() - st::normalFont->height, height() - 2 * st::normalFont->height); p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); + p.setPen(_st.tabs.labelFg); p.drawTextLeft( (width() - textWidth) / 2, textTop, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index f7f65811a..d41aebb80 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "api/api_common.h" +#include "chat_helpers/compose/compose_features.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/effects/message_sending_animation_common.h" @@ -75,6 +76,21 @@ struct EmojiChosen { using InlineChosen = InlineBots::ResultSelected; +enum class TabbedSelectorMode { + Full, + EmojiOnly, + MediaEditor, + EmojiStatus, +}; + +struct TabbedSelectorDescriptor { + std::shared_ptr show; + const style::EmojiPan &st; + PauseReason level = {}; + TabbedSelectorMode mode = TabbedSelectorMode::Full; + ComposeFeatures features; +}; + [[nodiscard]] std::unique_ptr MakeSearch( not_null parent, const style::EmojiPan &st, @@ -86,12 +102,7 @@ using InlineChosen = InlineBots::ResultSelected; class TabbedSelector : public Ui::RpWidget { public: static constexpr auto kPickCustomTimeId = -1; - enum class Mode { - Full, - EmojiOnly, - MediaEditor, - EmojiStatus, - }; + using Mode = TabbedSelectorMode; enum class Action { Update, Cancel, @@ -102,8 +113,12 @@ public: std::shared_ptr show, PauseReason level, Mode mode = Mode::Full); + TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor); ~TabbedSelector(); + [[nodiscard]] const style::EmojiPan &st() const; [[nodiscard]] Main::Session &session() const; [[nodiscard]] PauseReason level() const; @@ -254,6 +269,7 @@ private: not_null masks() const; const style::EmojiPan &_st; + const ComposeFeatures _features; const std::shared_ptr _show; const PauseReason _level = {}; diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 0ec9ad55d..f124b2b3f 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_download_manager.h" @@ -1685,6 +1686,9 @@ bool Application::readyToQuit() { if (session->api().isQuitPrevent()) { prevented = true; } + if (session->data().stories().isQuitPrevent()) { + prevented = true; + } } } } diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index 6e3003893..7103e1bd6 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -371,6 +371,9 @@ bool GetDefault( ? parent->window() : Core::App().getFileDialogParent(); Core::App().notifyFileDialogShown(true); + const auto guard = gsl::finally([] { + Core::App().notifyFileDialogShown(false); + }); if (type == Type::ReadFiles) { files = QFileDialog::getOpenFileNames(resolvedParent, caption, startFile, filter); QString path = files.isEmpty() ? QString() : QFileInfo(files.back()).absoluteDir().absolutePath(); @@ -386,7 +389,6 @@ bool GetDefault( } else { file = QFileDialog::getOpenFileName(resolvedParent, caption, startFile, filter); } - Core::App().notifyFileDialogShown(false); if (file.isEmpty()) { files = QStringList(); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 1e6cda40f..d18b842c0 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -345,7 +345,13 @@ bool ResolveUsernameOrPhone( const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); - const auto domain = params.value(u"domain"_q); + const auto domainParam = params.value(u"domain"_q); + const auto appnameParam = params.value(u"appname"_q); + + // Fix t.me/s/username links. + const auto webChannelPreviewLink = (domainParam == u"s"_q) + && !appnameParam.isEmpty(); + const auto domain = webChannelPreviewLink ? appnameParam : domainParam; const auto phone = params.value(u"phone"_q); const auto validDomain = [](const QString &domain) { return qthelp::regex_match( @@ -383,7 +389,9 @@ bool ResolveUsernameOrPhone( if (const auto postId = postParam.toInt()) { post = postId; } - const auto appname = params.value(u"appname"_q); + const auto storyParam = params.value(u"story"_q); + const auto storyId = storyParam.toInt(); + const auto appname = webChannelPreviewLink ? QString() : appnameParam; const auto appstart = params.value(u"startapp"_q); const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); @@ -408,6 +416,7 @@ bool ResolveUsernameOrPhone( .usernameOrId = domain, .phone = phone, .messageId = post, + .storyId = storyId, .repliesInfo = commentId ? Navigation::RepliesByLinkInfo{ Navigation::CommentId{ commentId } @@ -421,7 +430,7 @@ bool ResolveUsernameOrPhone( .startToken = startToken, .startAdminRights = adminRights, .startAutoSubmit = myContext.botStartAutoSubmit, - .botAppName = appname.isEmpty() ? postParam : appname, + .botAppName = (appname.isEmpty() ? postParam : appname), .botAppForceConfirmation = myContext.mayShowConfirmation, .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) @@ -1027,6 +1036,7 @@ QString TryConvertUrlToLocal(QString url) { "/?$|" "/[a-zA-Z0-9\\.\\_]+/?(\\?|$)|" "/\\d+/?(\\?|$)|" + "/s/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { const auto params = query.mid(usernameMatch->captured(0).size()).toString(); @@ -1036,6 +1046,8 @@ QString TryConvertUrlToLocal(QString url) { added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); } else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&post="_q + postMatch->captured(1); + } else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + added = u"&story="_q + storyMatch->captured(1); } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&appname="_q + appNameMatch->captured(1); } diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp index e75a5fc94..8ba1b6494 100644 --- a/Telegram/SourceFiles/core/mime_type.cpp +++ b/Telegram/SourceFiles/core/mime_type.cpp @@ -229,4 +229,15 @@ QList ReadMimeUrls(not_null data) { : QList(); } +bool CanSendFiles(not_null data) { + if (data->hasImage()) { + return true; + } else if (const auto urls = ReadMimeUrls(data); !urls.empty()) { + if (ranges::all_of(urls, &QUrl::isLocalFile)) { + return true; + } + } + return false; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/mime_type.h b/Telegram/SourceFiles/core/mime_type.h index d5aaa9eb0..3271adafe 100644 --- a/Telegram/SourceFiles/core/mime_type.h +++ b/Telegram/SourceFiles/core/mime_type.h @@ -67,5 +67,6 @@ struct MimeImageData { [[nodiscard]] MimeImageData ReadMimeImage(not_null data); [[nodiscard]] QString ReadMimeText(not_null data); [[nodiscard]] QList ReadMimeUrls(not_null data); +[[nodiscard]] bool CanSendFiles(not_null data); } // namespace Core diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index 051ad5910..c051a4375 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_regex.h" #include "ui/ui_utility.h" #include "ui/effects/animations.h" +#include "ui/platform/ui_platform_utility.h" #include #include @@ -584,9 +585,18 @@ void Sandbox::registerEnterFromEventLoop() { } bool Sandbox::notifyOrInvoke(QObject *receiver, QEvent *e) { - if (e->type() == base::InvokeQueuedEvent::kType) { + const auto type = e->type(); + if (type == base::InvokeQueuedEvent::kType) { static_cast(e)->invoke(); return true; + } else if (receiver == this) { + if (type == QEvent::ApplicationDeactivate) { + if (Ui::Platform::SkipApplicationDeactivateEvent()) { + return true; + } + } else if (type == QEvent::ApplicationActivate) { + Ui::Platform::GotApplicationActivateEvent(); + } } return QApplication::notify(receiver, e); } diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index d0d7580cc..e7351e8fd 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_session.h" +#include "data/data_sponsored_messages.h" #include "ui/text/text_custom_emoji.h" #include "ui/basic_click_handlers.h" #include "ui/emoji_config.h" @@ -26,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "mtproto/mtproto_config.h" #include "window/window_controller.h" +#include "window/window_session_controller.h" #include "mainwindow.h" namespace Core { @@ -264,6 +266,16 @@ Fn UiIntegration::createSpoilerRepaint(const std::any &context) { return my ? my->customEmojiRepaint : nullptr; } +bool UiIntegration::allowClickHandlerActivation( + const std::shared_ptr &handler, + const ClickContext &context) { + const auto my = context.other.value(); + if (const auto window = my.sessionWindow.get()) { + window->session().data().sponsoredMessages().clicked(my.itemId); + } + return true; +} + rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { return Core::App().passcodeLockChanges() | rpl::to_empty; } diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index a71c9cfbe..65661ecfd 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -60,6 +60,9 @@ public: const QString &data, const std::any &context) override; Fn createSpoilerRepaint(const std::any &context) override; + bool allowClickHandlerActivation( + const std::shared_ptr &handler, + const ClickContext &context) override; QString phraseContextCopyText() override; QString phraseContextCopyEmail() override; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 9a71820ca..16c30b177 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 4008004; -constexpr auto AppVersionStr = "4.8.4"; +constexpr auto AppVersion = 4008007; +constexpr auto AppVersionStr = "4.8.7"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h index d48542d19..4a4d62d17 100644 --- a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h @@ -57,7 +57,7 @@ public: return std::nullopt; } [[nodiscard]] std::optional nearest(Id id) const { - static_assert(std::is_same_v>); + static_assert(std::is_same_v>); if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) { return *it; } else if (_ids.empty()) { @@ -70,6 +70,10 @@ public: std::swap(_skippedBefore, _skippedAfter); } + friend inline bool operator==( + const AbstractSparseIds&, + const AbstractSparseIds&) = default; + private: IdsContainer _ids; std::optional _fullCount; diff --git a/Telegram/SourceFiles/data/data_audio_msg_id.cpp b/Telegram/SourceFiles/data/data_audio_msg_id.cpp index 4c95989ac..d8365bee2 100644 --- a/Telegram/SourceFiles/data/data_audio_msg_id.cpp +++ b/Telegram/SourceFiles/data/data_audio_msg_id.cpp @@ -27,7 +27,7 @@ AudioMsgId::AudioMsgId( , _externalPlayId(externalPlayId) , _changeablePlaybackSpeed(_audio->isVoiceMessage() || _audio->isVideoMessage() - || (_audio->getDuration() >= kMinLengthForChangeablePlaybackSpeed)) { + || (_audio->duration() >= kMinLengthForChangeablePlaybackSpeed)) { setTypeFromAudio(); } diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 9f28a7af0..f20041565 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -272,6 +272,38 @@ void Changes::entryRemoved(not_null entry) { _entryChanges.drop(entry); } +void Changes::storyUpdated( + not_null story, + StoryUpdate::Flags flags) { + const auto drop = (flags & StoryUpdate::Flag::Destroyed); + _storyChanges.updated(story, flags, drop); + if (!drop) { + scheduleNotifications(); + } +} + +rpl::producer Changes::storyUpdates( + StoryUpdate::Flags flags) const { + return _storyChanges.updates(flags); +} + +rpl::producer Changes::storyUpdates( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.updates(story, flags); +} + +rpl::producer Changes::storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.flagsValue(story, flags); +} + +rpl::producer Changes::realtimeStoryUpdates( + StoryUpdate::Flag flag) const { + return _storyChanges.realtimeUpdates(flag); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; @@ -291,6 +323,7 @@ void Changes::sendNotifications() { _messageChanges.sendNotifications(); _entryChanges.sendNotifications(); _topicChanges.sendNotifications(); + _storyChanges.sendNotifications(); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index e5bbc48e4..d4fee22c8 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -38,6 +38,7 @@ inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) { namespace Data { class ForumTopic; +class Story; struct NameUpdate { NameUpdate( @@ -84,26 +85,27 @@ struct PeerUpdate { SupportInfo = (1ULL << 23), IsBot = (1ULL << 24), EmojiStatus = (1ULL << 25), + StoriesState = (1ULL << 26), // For chats and channels - InviteLinks = (1ULL << 26), - Members = (1ULL << 27), - Admins = (1ULL << 28), - BannedUsers = (1ULL << 29), - Rights = (1ULL << 30), - PendingRequests = (1ULL << 31), - Reactions = (1ULL << 32), + InviteLinks = (1ULL << 27), + Members = (1ULL << 28), + Admins = (1ULL << 29), + BannedUsers = (1ULL << 30), + Rights = (1ULL << 31), + PendingRequests = (1ULL << 32), + Reactions = (1ULL << 33), // For channels - ChannelAmIn = (1ULL << 33), - StickersSet = (1ULL << 34), - ChannelLinkedChat = (1ULL << 35), - ChannelLocation = (1ULL << 36), - Slowmode = (1ULL << 37), - GroupCall = (1ULL << 38), + ChannelAmIn = (1ULL << 34), + StickersSet = (1ULL << 35), + ChannelLinkedChat = (1ULL << 36), + ChannelLocation = (1ULL << 37), + Slowmode = (1ULL << 38), + GroupCall = (1ULL << 39), // For iteration - LastUsedBit = (1ULL << 38), + LastUsedBit = (1ULL << 39), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -147,19 +149,19 @@ struct TopicUpdate { enum class Flag : uint32 { None = 0, - UnreadView = (1U << 1), - UnreadMentions = (1U << 2), + UnreadView = (1U << 1), + UnreadMentions = (1U << 2), UnreadReactions = (1U << 3), - Notifications = (1U << 4), - Title = (1U << 5), - IconId = (1U << 6), - ColorId = (1U << 7), - CloudDraft = (1U << 8), - Closed = (1U << 9), - Creator = (1U << 10), - Destroyed = (1U << 11), + Notifications = (1U << 4), + Title = (1U << 5), + IconId = (1U << 6), + ColorId = (1U << 7), + CloudDraft = (1U << 8), + Closed = (1U << 9), + Creator = (1U << 10), + Destroyed = (1U << 11), - LastUsedBit = (1U << 11), + LastUsedBit = (1U << 11), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -198,14 +200,14 @@ struct EntryUpdate { enum class Flag : uint32 { None = 0, - Repaint = (1U << 0), + Repaint = (1U << 0), HasPinnedMessages = (1U << 1), - ForwardDraft = (1U << 2), - LocalDraftSet = (1U << 3), - Height = (1U << 4), - Destroyed = (1U << 5), + ForwardDraft = (1U << 2), + LocalDraftSet = (1U << 3), + Height = (1U << 4), + Destroyed = (1U << 5), - LastUsedBit = (1U << 5), + LastUsedBit = (1U << 5), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -215,6 +217,26 @@ struct EntryUpdate { }; +struct StoryUpdate { + enum class Flag : uint32 { + None = 0, + + Edited = (1U << 0), + Destroyed = (1U << 1), + NewAdded = (1U << 2), + ViewsAdded = (1U << 3), + MarkRead = (1U << 4), + + LastUsedBit = (1U << 4), + }; + using Flags = base::flags; + friend inline constexpr auto is_flag_type(Flag) { return true; } + + not_null story; + Flags flags = 0; + +}; + class Changes final { public: explicit Changes(not_null session); @@ -298,6 +320,20 @@ public: EntryUpdate::Flag flag) const; void entryRemoved(not_null entry); + void storyUpdated( + not_null story, + StoryUpdate::Flags flags); + [[nodiscard]] rpl::producer storyUpdates( + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyUpdates( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer realtimeStoryUpdates( + StoryUpdate::Flag flag) const; + void sendNotifications(); private: @@ -348,6 +384,7 @@ private: Manager _topicChanges; Manager _messageChanges; Manager _entryChanges; + Manager _storyChanges; bool _notify = false; diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 2adfe69e4..c9e66759b 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -326,14 +326,17 @@ Main::Session &DocumentData::session() const { void DocumentData::setattributes( const QVector &attributes) { + _duration = -1; _flags &= ~(Flag::ImageType | Flag::HasAttachedStickers | Flag::UseTextColor + | Flag::SilentVideo | kStreamingSupportedMask); _flags |= kStreamingSupportedUnknown; validateLottieSticker(); + _videoPreloadPrefix = 0; for (const auto &attribute : attributes) { attribute.match([&](const MTPDdocumentAttributeImageSize &data) { dimensions = QSize(data.vw().v, data.vh().v); @@ -389,13 +392,20 @@ void DocumentData::setattributes( : VideoDocument; if (data.is_round_message()) { _additional = std::make_unique(); - round()->duration = data.vduration().v; + } else if (const auto size = data.vpreload_prefix_size()) { + if (size->v > 0) { + _videoPreloadPrefix = size->v; + } } } else if (const auto info = sticker()) { info->type = StickerType::Webm; } - _duration = data.vduration().v; + _duration = crl::time( + base::SafeRound(data.vduration().v * 1000)); setMaybeSupportsStreaming(data.is_supports_streaming()); + if (data.is_nosound()) { + _flags |= Flag::SilentVideo; + } dimensions = QSize(data.vw().v, data.vh().v); }, [&](const MTPDdocumentAttributeAudio &data) { if (type == FileDocument) { @@ -408,14 +418,14 @@ void DocumentData::setattributes( } } if (const auto voiceData = voice() ? voice() : round()) { - voiceData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); voiceData->waveform = documentWaveformDecode( data.vwaveform().value_or_empty()); voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) : *ranges::max_element(voiceData->waveform); } else if (const auto songData = song()) { - songData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); songData->title = qs(data.vtitle().value_or_empty()); songData->performer = qs(data.vperformer().value_or_empty()); refreshPossibleCoverThumbnail(); @@ -442,7 +452,10 @@ void DocumentData::setattributes( _additional = std::make_unique(); sticker()->type = StickerType::Webm; } - if (isAudioFile() || isAnimation() || isVoiceMessage()) { + if (isAudioFile() + || isAnimation() + || isVoiceMessage() + || storyMedia()) { setMaybeSupportsStreaming(true); } } @@ -1318,6 +1331,7 @@ bool DocumentData::canBeStreamed(HistoryItem *item) const { return hasRemoteLocation() && supportsStreaming() && (!isVideoFile() + || storyMedia() || !ExternalVideoPlayer.value() || (item && !item->allowsForward())); } @@ -1330,6 +1344,23 @@ bool DocumentData::inappPlaybackFailed() const { return (_flags & Flag::StreamingPlaybackFailed); } +int DocumentData::videoPreloadPrefix() const { + return _videoPreloadPrefix; +} + +StorageFileLocation DocumentData::videoPreloadLocation() const { + return hasRemoteLocation() + ? StorageFileLocation( + _dc, + session().userId(), + MTP_inputDocumentFileLocation( + MTP_long(id), + MTP_long(_access), + MTP_bytes(_fileReference), + MTP_string())) + : StorageFileLocation(); +} + auto DocumentData::createStreamingLoader( Data::FileOrigin origin, bool forceRemoteLoader) const @@ -1516,19 +1547,16 @@ bool DocumentData::isVideoFile() const { return (type == VideoDocument); } -TimeId DocumentData::getDuration() const { - if (const auto song = this->song()) { - return std::max(song->duration, 0); - } else if (const auto voice = this->voice()) { - return std::max(voice->duration, 0); - } else if (isAnimation() || isVideoFile()) { - return std::max(_duration, 0); - } else if (const auto sticker = this->sticker()) { - if (sticker->isWebm()) { - return std::max(_duration, 0); - } - } - return -1; +bool DocumentData::isSilentVideo() const { + return _flags & Flag::SilentVideo; +} + +crl::time DocumentData::duration() const { + return std::max(_duration, crl::time()); +} + +bool DocumentData::hasDuration() const { + return _duration >= 0; } bool DocumentData::isImage() const { @@ -1594,6 +1622,19 @@ void DocumentData::setRemoteLocation( } } +void DocumentData::setStoryMedia(bool value) { + if (value) { + _flags |= Flag::StoryDocument; + setMaybeSupportsStreaming(true); + } else { + _flags &= ~Flag::StoryDocument; + } +} + +bool DocumentData::storyMedia() const { + return (_flags & Flag::StoryDocument); +} + void DocumentData::setContentUrl(const QString &url) { _url = url; } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 42d49058d..59891c005 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -79,14 +79,12 @@ struct StickerData : public DocumentAdditionalData { }; struct SongData : public DocumentAdditionalData { - int32 duration = 0; QString title, performer; }; struct VoiceData : public DocumentAdditionalData { ~VoiceData(); - int duration = 0; VoiceWaveform waveform; char wavemax = 0; }; @@ -168,11 +166,13 @@ public: [[nodiscard]] bool isSongWithCover() const; [[nodiscard]] bool isAudioFile() const; [[nodiscard]] bool isVideoFile() const; + [[nodiscard]] bool isSilentVideo() const; [[nodiscard]] bool isAnimation() const; [[nodiscard]] bool isGifv() const; [[nodiscard]] bool isTheme() const; [[nodiscard]] bool isSharedMediaMusic() const; - [[nodiscard]] TimeId getDuration() const; + [[nodiscard]] crl::time duration() const; + [[nodiscard]] bool hasDuration() const; [[nodiscard]] bool isImage() const; void recountIsImage(); [[nodiscard]] bool supportsStreaming() const; @@ -233,6 +233,9 @@ public: [[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const; + void setStoryMedia(bool value); + [[nodiscard]] bool storyMedia() const; + void setRemoteLocation( int32 dc, uint64 access, @@ -272,6 +275,8 @@ public: void setInappPlaybackFailed(); [[nodiscard]] bool inappPlaybackFailed() const; + [[nodiscard]] int videoPreloadPrefix() const; + [[nodiscard]] StorageFileLocation videoPreloadLocation() const; DocumentId id = 0; int64 size = 0; @@ -284,18 +289,20 @@ public: private: enum class Flag : ushort { - StreamingMaybeYes = 0x001, - StreamingMaybeNo = 0x002, - StreamingPlaybackFailed = 0x004, - ImageType = 0x008, - DownloadCancelled = 0x010, - LoadedInMediaCache = 0x020, - HasAttachedStickers = 0x040, - InlineThumbnailIsPath = 0x080, - ForceToCache = 0x100, - PremiumSticker = 0x200, - PossibleCoverThumbnail = 0x400, - UseTextColor = 0x800, + StreamingMaybeYes = 0x0001, + StreamingMaybeNo = 0x0002, + StreamingPlaybackFailed = 0x0004, + ImageType = 0x0008, + DownloadCancelled = 0x0010, + LoadedInMediaCache = 0x0020, + HasAttachedStickers = 0x0040, + InlineThumbnailIsPath = 0x0080, + ForceToCache = 0x0100, + PremiumSticker = 0x0200, + PossibleCoverThumbnail = 0x0400, + UseTextColor = 0x0800, + StoryDocument = 0x1000, + SilentVideo = 0x2000, }; using Flags = base::flags; friend constexpr bool is_flag_type(Flag) { return true; }; @@ -341,6 +348,7 @@ private: const not_null _owner; + int _videoPreloadPrefix = 0; // Two types of location: from MTProto by dc+access or from web by url int32 _dc = 0; uint64 _access = 0; @@ -356,10 +364,10 @@ private: std::unique_ptr _replyPreview; std::weak_ptr _media; PhotoData *_goodThumbnailPhoto = nullptr; + crl::time _duration = -1; Core::FileLocation _location; std::unique_ptr _additional; - int32 _duration = -1; mutable Flags _flags = kStreamingSupportedUnknown; GoodThumbnailState _goodThumbnailState = GoodThumbnailState(); std::unique_ptr _loader; diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 2a03aaa7c..2839eeb76 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -254,7 +254,10 @@ void ResolveDocument( && !document->filepath().isEmpty()) { File::Launch(document->location(false).fname); } else if (controller) { - controller->openDocument(document, msgId, topicRootId, true); + controller->openDocument( + document, + true, + { msgId, topicRootId }); } }; diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 05080dfcb..088dc0b92 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -865,7 +865,7 @@ not_null DownloadManager::generateItem( ? previousItem->history() : session->data().history(session->user()); const auto flags = MessageFlag::FakeHistoryItem; - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto date = base::unixtime::now(); const auto postAuthor = QString(); diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 92750b49f..d37663208 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -40,10 +40,8 @@ struct FileReferenceAccumulator { }); } void push(const MTPPage &data) { - data.match([&](const auto &data) { - push(data.vphotos()); - push(data.vdocuments()); - }); + push(data.data().vphotos()); + push(data.data().vdocuments()); } void push(const MTPWallPaper &data) { data.match([&](const MTPDwallPaper &data) { @@ -52,12 +50,12 @@ struct FileReferenceAccumulator { }); } void push(const MTPTheme &data) { - data.match([&](const MTPDtheme &data) { - push(data.vdocument()); - }); + push(data.data().vdocument()); } void push(const MTPWebPageAttribute &data) { - data.match([&](const MTPDwebPageAttributeTheme &data) { + data.match([&](const MTPDwebPageAttributeStory &data) { + push(data.vstory()); + }, [&](const MTPDwebPageAttributeTheme &data) { push(data.vdocuments()); }); } @@ -104,6 +102,13 @@ struct FileReferenceAccumulator { }, [](const MTPDmessageEmpty &data) { }); } + void push(const MTPStoryItem &data) { + data.match([&](const MTPDstoryItem &data) { + push(data.vmedia()); + }, [](const MTPDstoryItemDeleted &) { + }, [](const MTPDstoryItemSkipped &) { + }); + } void push(const MTPmessages_Messages &data) { data.match([](const MTPDmessages_messagesNotModified &) { }, [&](const auto &data) { @@ -116,9 +121,7 @@ struct FileReferenceAccumulator { }); } void push(const MTPusers_UserFull &data) { - data.match([&](const auto &data) { - push(data.vfull_user().data().vpersonal_photo()); - }); + push(data.data().vfull_user().data().vpersonal_photo()); } void push(const MTPmessages_RecentStickers &data) { data.match([&](const MTPDmessages_recentStickers &data) { @@ -151,9 +154,10 @@ struct FileReferenceAccumulator { }); } void push(const MTPhelp_PremiumPromo &data) { - data.match([&](const MTPDhelp_premiumPromo &data) { - push(data.vvideos()); - }); + push(data.data().vvideos()); + } + void push(const MTPstories_Stories &data) { + push(data.data().vstories()); } UpdatedFileReferences result; @@ -216,6 +220,10 @@ UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) { return GetFileReferencesHelper(data); } +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) { + return GetFileReferencesHelper(data); +} + UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) { return GetFileReferencesHelper(data); } diff --git a/Telegram/SourceFiles/data/data_file_origin.h b/Telegram/SourceFiles/data/data_file_origin.h index 195ae9188..b3185d2df 100644 --- a/Telegram/SourceFiles/data/data_file_origin.h +++ b/Telegram/SourceFiles/data/data_file_origin.h @@ -120,6 +120,20 @@ struct FileOriginPremiumPreviews { } }; +struct FileOriginStory { + FileOriginStory(PeerId peerId, StoryId storyId) + : peerId(peerId) + , storyId(storyId) { + } + + PeerId peerId = 0; + StoryId storyId = 0; + + friend inline auto operator<=>( + FileOriginStory, + FileOriginStory) = default; +}; + struct FileOrigin { using Variant = std::variant< v::null_t, @@ -132,7 +146,8 @@ struct FileOrigin { FileOriginWallpaper, FileOriginTheme, FileOriginRingtones, - FileOriginPremiumPreviews>; + FileOriginPremiumPreviews, + FileOriginStory>; FileOrigin() = default; FileOrigin(FileOriginMessage data) : data(data) { @@ -155,6 +170,8 @@ struct FileOrigin { } FileOrigin(FileOriginPremiumPreviews data) : data(data) { } + FileOrigin(FileOriginStory data) : data(data) { + } explicit operator bool() const { return !v::is_null(data); @@ -204,6 +221,7 @@ UpdatedFileReferences GetFileReferences(const MTPTheme &data); UpdatedFileReferences GetFileReferences( const MTPaccount_SavedRingtones &data); UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data); +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data); // Admin Log Event. UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data); diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 4faa19f71..743487c85 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -39,6 +39,21 @@ constexpr auto kShowChatNamesCount = 8; not_null folder) { const auto &list = folder->lastHistories(); if (list.empty()) { + if (const auto storiesUnread = folder->storiesUnreadCount()) { + return { + tr::lng_contacts_stories_status_new( + tr::now, + lt_count, + storiesUnread), + }; + } else if (const auto storiesCount = folder->storiesCount()) { + return { + tr::lng_contacts_stories_status( + tr::now, + lt_count, + storiesCount), + }; + } return {}; } @@ -301,6 +316,33 @@ void Folder::validateListEntryCache() { Ui::ItemTextDefaultOptions()); } +void Folder::updateStoriesCount(int count, int unread) { + if (_storiesCount == count && _storiesUnreadCount == unread) { + return; + } + const auto limit = (1 << 16) - 1; + const auto was = (_storiesCount > 0); + _storiesCount = std::min(count, limit); + _storiesUnreadCount = std::min(unread, limit); + const auto now = (_storiesCount > 0); + if (was == now) { + updateChatListEntryPostponed(); + } else if (now) { + updateChatListSortPosition(); + } else { + updateChatListExistence(); + } + ++_chatListViewVersion; +} + +int Folder::storiesCount() const { + return _storiesCount; +} + +int Folder::storiesUnreadCount() const { + return _storiesUnreadCount; +} + void Folder::requestChatListMessage() { if (!chatListMessageKnown()) { owner().histories().requestDialogEntry(this); @@ -339,7 +381,7 @@ int Folder::fixedOnTopIndex() const { } bool Folder::shouldBeInChatList() const { - return !_chatsList.empty(); + return !_chatsList.empty() || (_storiesCount > 0); } Dialogs::UnreadState Folder::chatListUnreadState() const { diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index 9e62fcabe..7008adac0 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -75,6 +75,10 @@ public: return _listEntryCache; } + void updateStoriesCount(int count, int unread); + [[nodiscard]] int storiesCount() const; + [[nodiscard]] int storiesUnreadCount() const; + private: void indexNameParts(); @@ -104,6 +108,9 @@ private: int _chatListViewVersion = 0; //rpl::variable _unreadPosition; + uint16_t _storiesCount = 0; + uint16_t _storiesUnreadCount = 0; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index b6812b740..a0b35b974 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_scheduled_messages.h" +#include "data/data_user.h" #include "base/unixtime.h" #include "base/random.h" #include "main/main_session.h" @@ -36,6 +37,29 @@ constexpr auto kReadRequestTimeout = 3 * crl::time(1000); } // namespace +MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo) { + 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)); + } + } + } else if (replyTo.msgId || replyTo.topicRootId) { + using Flag = MTPDinputReplyToMessage::Flag; + return MTP_inputReplyToMessage( + (replyTo.topicRootId + ? MTP_flags(Flag::f_top_msg_id) + : MTP_flags(0)), + MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), + MTP_int(replyTo.topicRootId)); + } + return MTPInputReplyTo(); +} + Histories::Histories(not_null owner) : _owner(owner) , _readRequestsTimer([=] { sendReadRequests(); }) { @@ -903,23 +927,24 @@ bool Histories::isCreatingTopic( int Histories::sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail) { - if (isCreatingTopic(history, topicRootId)) { + if (isCreatingTopic(history, replyTo.topicRootId)) { const auto id = ++_requestAutoincrement; - const auto creatingId = FullMsgId(history->peer->id, topicRootId); + const auto creatingId = FullMsgId( + history->peer->id, + replyTo.topicRootId); auto i = _creatingTopics.find(creatingId); if (i == end(_creatingTopics)) { - sendCreateTopicRequest(history, topicRootId); + sendCreateTopicRequest(history, replyTo.topicRootId); i = _creatingTopics.emplace(creatingId).first; } i->second.push_back({ .randomId = randomId, - .replyTo = replyTo, + .replyTo = replyTo.msgId, .message = std::move(message), .done = std::move(done), .fail = std::move(fail), @@ -928,9 +953,12 @@ int Histories::sendPreparedMessage( _creatingTopicRequests.emplace(id); return id; } - const auto realReply = convertTopicReplyTo(history, replyTo); - const auto realRoot = convertTopicReplyTo(history, topicRootId); - return v::match(message(realReply, realRoot), [&](const auto &request) { + const auto realReplyTo = FullReplyTo{ + .msgId = convertTopicReplyToId(history, replyTo.msgId), + .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), + .storyId = replyTo.storyId, + }; + return v::match(message(_owner, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { const auto session = &_owner->session(); @@ -973,8 +1001,10 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { _creatingTopicRequests.erase(entry.requestId); sendPreparedMessage( history, - entry.replyTo, - realRoot, + FullReplyTo{ + .msgId = entry.replyTo, + .topicRootId = realRoot, + }, entry.randomId, std::move(entry.message), std::move(entry.done), @@ -994,14 +1024,14 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { } } -MsgId Histories::convertTopicReplyTo( +MsgId Histories::convertTopicReplyToId( not_null history, - MsgId replyTo) const { - if (!replyTo) { + MsgId replyToId) const { + if (!replyToId) { return {}; } - const auto i = _createdTopicIds.find({ history->peer->id, replyTo }); - return (i != end(_createdTopicIds)) ? i->second : replyTo; + const auto i = _createdTopicIds.find({ history->peer->id, replyToId }); + return (i != end(_createdTopicIds)) ? i->second : replyToId; } void Histories::checkPostponed(not_null history, int id) { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index 05f7534f4..c98dc352c 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -26,6 +26,10 @@ namespace Data { class Session; class Folder; +[[nodiscard]] MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo); + class Histories final { public: enum class RequestType : uchar { @@ -102,29 +106,27 @@ public: MTPmessages_SendMultiMedia>; int sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail); struct ReplyToPlaceholder { }; - struct TopicRootPlaceholder { - }; template - static Fn PrepareMessage( - const Args &...args) { - return [=](MsgId replyTo, MsgId topicRootId) -> RequestType { - return { ReplaceReplyIds(args, replyTo, topicRootId)... }; + static auto PrepareMessage(const Args &...args) + -> Fn, FullReplyTo)> { + return [=](not_null owner, FullReplyTo replyTo) + -> RequestType { + return { ReplaceReplyIds(owner, args, replyTo)... }; }; } void checkTopicCreated(FullMsgId rootId, MsgId realRoot); - [[nodiscard]] MsgId convertTopicReplyTo( + [[nodiscard]] MsgId convertTopicReplyToId( not_null history, - MsgId replyTo) const; + MsgId replyToId) const; private: struct PostponedHistoryRequest { @@ -151,7 +153,7 @@ private: struct DelayedByTopicMessage { uint64 randomId = 0; MsgId replyTo = 0; - Fn message; + Fn, FullReplyTo)> message; Fn done; Fn fail; int requestId = 0; @@ -166,11 +168,12 @@ private: }; template - static auto ReplaceReplyIds(Arg arg, MsgId replyTo, MsgId topicRootId) { + static auto ReplaceReplyIds( + not_null owner, + Arg arg, + FullReplyTo replyTo) { if constexpr (std::is_same_v) { - return MTP_int(replyTo); - } else if constexpr (std::is_same_v) { - return MTP_int(topicRootId); + return ReplyToForMTP(owner, replyTo); } else { return arg; } diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 4e311e1f1..19d89c500 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_slot_machine.h" #include "history/view/media/history_view_dice.h" #include "history/view/media/history_view_service_box.h" +#include "history/view/media/history_view_story_mention.h" #include "history/view/media/history_view_premium_gift.h" #include "history/view/media/history_view_userpic_suggestion.h" #include "dialogs/ui/dialogs_message_view.h" @@ -56,6 +57,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_poll.h" #include "data/data_channel.h" #include "data/data_file_origin.h" +#include "data/data_stories.h" +#include "data/data_story.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "core/application.h" @@ -72,6 +75,7 @@ namespace { constexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60); constexpr auto kMaxPreviewImages = 3; +constexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL); using ItemPreview = HistoryView::ItemPreview; using ItemPreviewImage = HistoryView::ItemPreviewImage; @@ -404,6 +408,18 @@ const WallPaper *Media::paper() const { return nullptr; } +FullStoryId Media::storyId() const { + return {}; +} + +bool Media::storyExpired(bool revalidate) { + return false; +} + +bool Media::storyMention() const { + return false; +} + bool Media::uploading() const { return false; } @@ -1968,4 +1984,161 @@ std::unique_ptr MediaWallPaper::createView( std::make_unique(message, _paper)); } +MediaStory::MediaStory( + not_null parent, + FullStoryId storyId, + bool mention) +: Media(parent) +, _storyId(storyId) +, _mention(mention) { + const auto owner = &parent->history()->owner(); + owner->registerStoryItem(storyId, parent); + + const auto stories = &owner->stories(); + if (const auto maybeStory = stories->lookup(storyId)) { + if (!_mention) { + parent->setText((*maybeStory)->caption()); + } + } else { + if (maybeStory.error() == NoStory::Unknown) { + stories->resolve(storyId, crl::guard(this, [=] { + if (const auto maybeStory = stories->lookup(storyId)) { + if (!_mention) { + parent->setText((*maybeStory)->caption()); + } + } else { + _expired = true; + } + if (_mention) { + parent->updateStoryMentionText(); + } + parent->history()->owner().requestItemViewRefresh(parent); + })); + } else { + _expired = true; + } + } +} + +MediaStory::~MediaStory() { + const auto owner = &parent()->history()->owner(); + owner->unregisterStoryItem(_storyId, parent()); +} + +std::unique_ptr MediaStory::clone(not_null parent) { + return std::make_unique(parent, _storyId, false); +} + +FullStoryId MediaStory::storyId() const { + return _storyId; +} + +bool MediaStory::storyExpired(bool revalidate) { + if (revalidate) { + const auto stories = &parent()->history()->owner().stories(); + if (const auto maybeStory = stories->lookup(_storyId)) { + _expired = false; + } else if (maybeStory.error() == Data::NoStory::Deleted) { + _expired = true; + } + } + return _expired; +} + +bool MediaStory::storyMention() const { + return _mention; +} + +TextWithEntities MediaStory::notificationText() const { + const auto stories = &parent()->history()->owner().stories(); + const auto maybeStory = stories->lookup(_storyId); + return WithCaptionNotificationText( + ((_expired + || (!maybeStory + && maybeStory.error() == Data::NoStory::Deleted)) + ? tr::lng_in_dlg_story_expired + : tr::lng_in_dlg_story)(tr::now), + (maybeStory + ? (*maybeStory)->caption() + : TextWithEntities())); +} + +QString MediaStory::pinnedTextSubstring() const { + return tr::lng_action_pinned_media_story(tr::now); +} + +TextForMimeData MediaStory::clipboardText() const { + return WithCaptionClipboardText( + (_expired + ? tr::lng_in_dlg_story_expired + : tr::lng_in_dlg_story)(tr::now), + parent()->clipboardText()); +} + +bool MediaStory::dropForwardedInfo() const { + return true; +} + +bool MediaStory::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaStory::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +not_null MediaStory::LoadingStoryPhoto( + not_null owner) { + return owner->photo(kLoadingStoryPhotoId); +} + +std::unique_ptr MediaStory::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + const auto spoiler = false; + const auto stories = &parent()->history()->owner().stories(); + const auto maybeStory = stories->lookup(_storyId); + if (!maybeStory) { + if (!_mention) { + realParent->setText(TextWithEntities()); + } + if (maybeStory.error() == Data::NoStory::Deleted) { + _expired = true; + return nullptr; + } + _expired = false; + if (_mention) { + return nullptr; + } + return std::make_unique( + message, + realParent, + LoadingStoryPhoto(&realParent->history()->owner()), + spoiler); + } + _expired = false; + const auto story = *maybeStory; + if (_mention) { + return std::make_unique( + message, + std::make_unique(message, story)); + } else { + realParent->setText(story->caption()); + if (const auto photo = story->photo()) { + return std::make_unique( + message, + realParent, + photo, + spoiler); + } else { + return std::make_unique( + message, + realParent, + story->document(), + spoiler); + } + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 985e2c8aa..7649a37ed 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" #include "data/data_location.h" #include "data/data_wall_paper.h" @@ -37,6 +38,7 @@ namespace Data { class CloudImage; class WallPaper; +class Session; enum class CallFinishReason : char { Missed, @@ -58,6 +60,7 @@ struct Call { int duration = 0; FinishReason finishReason = FinishReason::Missed; bool video = false; + }; struct ExtendedPreview { @@ -110,6 +113,9 @@ public: virtual CloudImage *location() const; virtual PollData *poll() const; virtual const WallPaper *paper() const; + virtual FullStoryId storyId() const; + virtual bool storyExpired(bool revalidate = false); + virtual bool storyMention() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -563,6 +569,42 @@ private: }; +class MediaStory final : public Media, public base::has_weak_ptr { +public: + MediaStory( + not_null parent, + FullStoryId storyId, + bool mention); + ~MediaStory(); + + std::unique_ptr clone(not_null parent) override; + + FullStoryId storyId() const override; + bool storyExpired(bool revalidate = false) override; + bool storyMention() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + bool dropForwardedInfo() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + + [[nodiscard]] static not_null LoadingStoryPhoto( + not_null owner); + +private: + const FullStoryId _storyId; + const bool _mention = false; + bool _expired = false; + +}; + [[nodiscard]] TextForMimeData WithCaptionClipboardText( const QString &attachType, TextForMimeData &&caption); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index a0834888d..e4279cbb3 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" #include "main/main_app_config.h" +#include "main/session/send_as_peers.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_histories.h" @@ -82,6 +83,30 @@ constexpr auto kTopReactionsLimit = 14; : config->get("reactions_user_max_default", 1); } +bool IsMyRecent( + const MTPDmessagePeerReaction &data, + const ReactionId &id, + not_null peer, + const base::flat_map< + ReactionId, + std::vector> &recent, + bool ignoreChosen) { + if (peer->id == peer->session().userPeerId()) { + return true; + } else if (!ignoreChosen) { + return data.is_my(); + } + const auto j = recent.find(id); + if (j == end(recent)) { + return false; + } + const auto k = ranges::find( + j->second, + peer, + &RecentReaction::peer); + return (k != end(j->second)) && k->my; +} + } // namespace PossibleItemReactionsRef LookupPossibleReactions( @@ -953,7 +978,6 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { Expects(!id.empty()); const auto history = _item->history(); - const auto self = history->session().user(); const auto myLimit = SentReactionsLimit(_item); if (ranges::contains(chosen(), id)) { return; @@ -973,7 +997,7 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { _recent.erase(j); } else { j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), + ranges::remove(j->second, true, &RecentReaction::my), end(j->second)); if (j->second.empty()) { _recent.erase(j); @@ -982,9 +1006,14 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { } return removed; }), end(_list)); - if (_item->canViewReactions() || history->peer->isUser()) { + const auto peer = history->peer; + if (_item->canViewReactions() || peer->isUser()) { auto &list = _recent[id]; - list.insert(begin(list), RecentReaction{ self }); + const auto from = peer->session().sendAsPeers().resolveChosen(peer); + list.insert(begin(list), RecentReaction{ + .peer = from, + .my = true, + }); } const auto i = ranges::find(_list, id, &MessageReaction::id); if (i != end(_list)) { @@ -1018,13 +1047,16 @@ void MessageReactions::remove(const ReactionId &id) { _list.erase(i); } if (j != end(_recent)) { - j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), - end(j->second)); - if (j->second.empty()) { + if (removed) { + j->second.clear(); _recent.erase(j); } else { - Assert(!removed); + j->second.erase( + ranges::remove(j->second, true, &RecentReaction::my), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } } } auto &owner = history->owner(); @@ -1034,7 +1066,8 @@ void MessageReactions::remove(const ReactionId &id) { bool MessageReactions::checkIfChanged( const QVector &list, - const QVector &recent) const { + const QVector &recent, + bool min) const { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1066,13 +1099,18 @@ bool MessageReactions::checkIfChanged( for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); - if (ranges::contains(_list, id, &MessageReaction::id)) { - parsed[id].push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); + if (!ranges::contains(_list, id, &MessageReaction::id)) { + return; } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + parsed[id].push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } return !ranges::equal(_recent, parsed, []( @@ -1081,7 +1119,7 @@ bool MessageReactions::checkIfChanged( return ranges::equal(a.second, b.second, []( const RecentReaction &a, const RecentReaction &b) { - return (a.peer == b.peer) && (a.big == b.big); + return (a.peer == b.peer) && (a.big == b.big) && (a.my == b.my); }); }); } @@ -1089,7 +1127,7 @@ bool MessageReactions::checkIfChanged( bool MessageReactions::change( const QVector &list, const QVector &recent, - bool ignoreChosen) { + bool min) { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1102,7 +1140,7 @@ bool MessageReactions::change( count.match([&](const MTPDreactionCount &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto &chosen = data.vchosen_order(); - if (!ignoreChosen && chosen) { + if (!min && chosen) { order[id] = chosen->v; } const auto i = ranges::find(_list, id, &MessageReaction::id); @@ -1112,10 +1150,10 @@ bool MessageReactions::change( _list.push_back({ .id = id, .count = nowCount, - .my = (!ignoreChosen && chosen) + .my = (!min && chosen) }); } else { - const auto nowMy = ignoreChosen ? i->my : chosen.has_value(); + const auto nowMy = min ? i->my : chosen.has_value(); if (i->count != nowCount || i->my != nowMy) { i->count = nowCount; i->my = nowMy; @@ -1125,13 +1163,13 @@ bool MessageReactions::change( existing.emplace(id); }); } - if (!ignoreChosen && !order.empty()) { - const auto min = std::numeric_limits::min(); + if (!min && !order.empty()) { + const auto minimal = std::numeric_limits::min(); const auto proj = [&](const MessageReaction &reaction) { - return reaction.my ? order[reaction.id] : min; + return reaction.my ? order[reaction.id] : minimal; }; const auto correctOrder = [&] { - auto previousOrder = min; + auto previousOrder = minimal; for (const auto &reaction : _list) { const auto nowOrder = proj(reaction); if (nowOrder < previousOrder) { @@ -1161,16 +1199,22 @@ bool MessageReactions::change( reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto i = ranges::find(_list, id, &MessageReaction::id); - if (i != end(_list)) { - auto &list = parsed[id]; - if (list.size() < i->count) { - list.push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); - } + if (i == end(_list)) { + return; } + auto &list = parsed[id]; + if (list.size() >= i->count) { + return; + } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + list.push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } if (_recent != parsed) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index c1aeb6f73..61b29107d 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -225,14 +225,14 @@ struct RecentReaction { not_null peer; bool unread = false; bool big = false; + bool my = false; - inline friend constexpr bool operator==( - const RecentReaction &a, - const RecentReaction &b) noexcept { - return (a.peer.get() == b.peer.get()) - && (a.unread == b.unread) - && (a.big == b.big); - } + friend inline auto operator<=>( + const RecentReaction &a, + const RecentReaction &b) = default; + friend inline bool operator==( + const RecentReaction &a, + const RecentReaction &b) = default; }; class MessageReactions final { @@ -247,7 +247,8 @@ public: bool ignoreChosen); [[nodiscard]] bool checkIfChanged( const QVector &list, - const QVector &recent) const; + const QVector &recent, + bool ignoreChosen) const; [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index ac39eddfb..49357d88f 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -51,14 +51,49 @@ Q_DECLARE_METATYPE(MsgId); return MsgId(a.bare - b.bare); } +using StoryId = int32; + +struct FullStoryId { + PeerId peer = 0; + StoryId story = 0; + + [[nodiscard]] bool valid() const { + return peer != 0 && story != 0; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; +}; + +struct FullReplyTo { + MsgId msgId = 0; + MsgId topicRootId = 0; + FullStoryId storyId; + + [[nodiscard]] bool valid() const { + return msgId || storyId; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58)); constexpr auto ClientMsgIds = (1LL << 31); constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds); +constexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1); +constexpr auto ServerMaxStoryId = StoryId(1 << 30); +constexpr auto StoryMsgIds = int64(ServerMaxStoryId); +constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); constexpr auto ScheduledMsgIdsRange = (1LL << 32); constexpr auto ShowAtUnreadMsgId = MsgId(0); -constexpr auto SpecialMsgIdShift = EndClientMsgId.bare; +constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1); constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2); constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4); @@ -81,6 +116,20 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId); return MsgId(StartClientMsgId.bare + index); } +[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { + return (id >= StartStoryMsgId && id < EndStoryMsgId); +} +[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept { + Expects(IsStoryMsgId(id)); + + return StoryId(id.bare - StartStoryMsgId.bare); +} +[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept { + Expects(id >= 0); + + return MsgId(StartStoryMsgId.bare + id); +} + [[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept { return (id > 0 && id < ServerMaxMsgId); } @@ -145,4 +194,13 @@ struct hash : private hash { } }; +template <> +struct hash { + size_t operator()(FullStoryId value) const { + return QtPrivate::QHashCombine().operator()( + std::hash()(value.peer.value), + value.story); + } +}; + } // namespace std diff --git a/Telegram/SourceFiles/data/data_photo_media.cpp b/Telegram/SourceFiles/data/data_photo_media.cpp index 7bbdcdd1e..be713b4a9 100644 --- a/Telegram/SourceFiles/data/data_photo_media.cpp +++ b/Telegram/SourceFiles/data/data_photo_media.cpp @@ -166,14 +166,22 @@ bool PhotoMedia::autoLoadThumbnailAllowed(not_null peer) const { } void PhotoMedia::automaticLoad( - Data::FileOrigin origin, + FileOrigin origin, const HistoryItem *item) { - if (!item || loaded() || _owner->cancelled()) { + if (item) { + automaticLoad(origin, item->history()->peer); + } +} + +void PhotoMedia::automaticLoad( + FileOrigin origin, + not_null peer) { + if (loaded() || _owner->cancelled()) { return; } const auto loadFromCloud = Data::AutoDownload::Should( _owner->session().settings().autoDownload(), - item->history()->peer, + peer, _owner); _owner->load( origin, diff --git a/Telegram/SourceFiles/data/data_photo_media.h b/Telegram/SourceFiles/data/data_photo_media.h index 7c5c34bd4..f9e3c7708 100644 --- a/Telegram/SourceFiles/data/data_photo_media.h +++ b/Telegram/SourceFiles/data/data_photo_media.h @@ -43,7 +43,8 @@ public: [[nodiscard]] bool autoLoadThumbnailAllowed( not_null peer) const; - void automaticLoad(Data::FileOrigin origin, const HistoryItem *item); + void automaticLoad(FileOrigin origin, const HistoryItem *item); + void automaticLoad(FileOrigin origin, not_null peer); void collectLocalData(not_null local); diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index ce2cbee35..f781f95bc 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -132,26 +132,23 @@ bool PollData::applyResults(const MTPPollResults &results) { } } if (const auto recent = results.vrecent_voters()) { - const auto bareProj = [](not_null user) { - return peerToUser(user->id).bare; - }; const auto recentChanged = !ranges::equal( recentVoters, recent->v, ranges::equal_to(), - bareProj, - &MTPlong::v); + &PeerData::id, + peerFromMTP); if (recentChanged) { changed = true; recentVoters = ranges::views::all( recent->v - ) | ranges::views::transform([&](MTPlong userId) { - const auto user = _owner->user(userId.v); - return user->isMinimalLoaded() ? user.get() : nullptr; - }) | ranges::views::filter([](UserData *user) { - return user != nullptr; - }) | ranges::views::transform([](UserData *user) { - return not_null(user); + ) | ranges::views::transform([&](MTPPeer peerId) { + const auto peer = _owner->peer(peerFromMTP(peerId)); + return peer->isMinimalLoaded() ? peer.get() : nullptr; + }) | ranges::views::filter([](PeerData *peer) { + return peer != nullptr; + }) | ranges::views::transform([](PeerData *peer) { + return not_null(peer); }) | ranges::to_vector; } } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 72d2731d6..4f428479b 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -67,7 +67,7 @@ struct PollData { PollId id = 0; QString question; std::vector answers; - std::vector> recentVoters; + std::vector> recentVoters; std::vector sendingVotes; TextWithEntities solution; TimeId closePeriod = 0; diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 21e0c48b3..820b7c0d2 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -192,13 +192,13 @@ void ScheduledMessages::sendNowSimpleMessage( const auto history = local->history(); auto action = Api::SendAction(history); - action.replyTo = local->replyToId(); + action.replyTo = local->replyTo(); const auto replyHeader = NewMessageReplyHeader(action); const auto localFlags = NewMessageFlags(history->peer) & ~MessageFlag::BeingSent; const auto flags = MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id - | (local->replyToId() + | (action.replyTo ? MTPDmessage::Flag::f_reply_to : MTPDmessage::Flag(0)) | (update.vttl_period() diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d383997e8..e1cf3ef8b 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -66,6 +66,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_emoji_statuses.h" #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" +#include "data/data_stories.h" +#include "data/data_story.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" #include "data/data_histories.h" @@ -271,7 +273,8 @@ Session::Session(not_null session) , _emojiStatuses(std::make_unique(this)) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) -, _customEmojiManager(std::make_unique(this)) { +, _customEmojiManager(std::make_unique(this)) +, _stories(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -316,6 +319,8 @@ Session::Session(not_null session) } } }, _lifetime); + + _stories->loadMore(Data::StorySourcesList::NotHidden); }); } @@ -523,7 +528,15 @@ not_null Session::processUser(const MTPUser &data) { ? Flag::Contact | Flag::MutualContact | Flag::DiscardMinPhoto + | Flag::StoriesHidden : Flag()); + const auto storiesState = minimal + ? std::optional() + : data.is_stories_unavailable() + ? Data::Stories::PeerSourceState() + : !data.vstories_max_id() + ? std::optional() + : stories().peerSourceState(result, data.vstories_max_id()->v); const auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) | (data.is_scam() ? Flag::Scam : Flag()) @@ -535,6 +548,7 @@ not_null Session::processUser(const MTPUser &data) { ? (data.is_contact() ? Flag::Contact : Flag()) | (data.is_mutual_contact() ? Flag::MutualContact : Flag()) | (data.is_apply_min_photo() ? Flag() : Flag::DiscardMinPhoto) + | (data.is_stories_hidden() ? Flag::StoriesHidden : Flag()) : Flag()); result->setFlags((result->flags() & ~flagsMask) | flagsSet); if (minimal) { @@ -549,6 +563,13 @@ not_null Session::processUser(const MTPUser &data) { MTP_long(data.vaccess_hash().value_or_empty())); } } else { + if (storiesState) { + result->setStoriesState(!storiesState->maxId + ? UserData::StoriesState::None + : (storiesState->maxId > storiesState->readTill) + ? UserData::StoriesState::HasUnread + : UserData::StoriesState::HasRead); + } if (data.is_self()) { result->input = MTP_inputPeerSelf(); result->inputUser = MTP_inputUserSelf(); @@ -1142,7 +1163,8 @@ UserData *Session::userByPhone(const QString &phone) const { PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); for (const auto &[peerId, peer] : _peers) { - if (!peer->userName().compare(uname, Qt::CaseInsensitive)) { + if (peer->isLoaded() + && !peer->userName().compare(uname, Qt::CaseInsensitive)) { return peer.get(); } } @@ -3284,6 +3306,7 @@ not_null Session::processWebpage(const MTPDwebPagePending &data) { QString(), QString(), TextWithEntities(), + FullStoryId(), nullptr, nullptr, WebPageCollage(), @@ -3338,6 +3361,7 @@ not_null Session::webpage( siteName, title, description, + FullStoryId(), photo, document, std::move(collage), @@ -3380,6 +3404,8 @@ void Session::webpageApplyFields( const auto result = attribute.match([&]( const MTPDwebPageAttributeTheme &data) { return lookupInAttribute(data); + }, [&](const MTPDwebPageAttributeStory &data) { + return (DocumentData*)nullptr; }); if (result) { return result; @@ -3388,16 +3414,55 @@ void Session::webpageApplyFields( } return nullptr; }; + auto story = (Data::Story*)nullptr; + auto storyId = FullStoryId(); + if (const auto attributes = data.vattributes()) { + for (const auto &attribute : attributes->v) { + attribute.match([&](const MTPDwebPageAttributeStory &data) { + storyId = FullStoryId{ + peerFromUser(data.vuser_id()), + data.vid().v, + }; + if (const auto embed = data.vstory()) { + story = stories().applyFromWebpage( + peerFromUser(data.vuser_id()), + *embed); + } else if (const auto maybe = stories().lookup(storyId)) { + story = *maybe; + } else if (maybe.error() == Data::NoStory::Unknown) { + stories().resolve(storyId, [=] { + if (const auto maybe = stories().lookup(storyId)) { + const auto story = *maybe; + page->document = story->document(); + page->photo = story->photo(); + page->description = story->caption(); + page->type = WebPageType::Story; + notifyWebPageUpdateDelayed(page); + } + }); + } + }, [](const auto &) {}); + } + } webpageApplyFields( page, - ParseWebPageType(data), + (story ? WebPageType::Story : ParseWebPageType(data)), qs(data.vurl()), qs(data.vdisplay_url()), siteName, qs(data.vtitle().value_or_empty()), - description, - photo ? processPhoto(*photo).get() : nullptr, - document ? processDocument(*document).get() : lookupThemeDocument(), + (story ? story->caption() : description), + storyId, + (story + ? story->photo() + : photo + ? processPhoto(*photo).get() + : nullptr), + (story + ? story->document() + : document + ? processDocument(*document).get() + : lookupThemeDocument()), WebPageCollage(this, data), data.vduration().value_or_empty(), qs(data.vauthor().value_or_empty()), @@ -3412,6 +3477,7 @@ void Session::webpageApplyFields( const QString &siteName, const QString &title, const TextWithEntities &description, + FullStoryId storyId, PhotoData *photo, DocumentData *document, WebPageCollage &&collage, @@ -3426,6 +3492,7 @@ void Session::webpageApplyFields( siteName, title, description, + storyId, photo, document, std::move(collage), @@ -3904,6 +3971,38 @@ void Session::destroyAllCallItems() { } } +void Session::registerStoryItem( + FullStoryId id, + not_null item) { + _storyItems[id].emplace(item); +} + +void Session::unregisterStoryItem( + FullStoryId id, + not_null item) { + const auto i = _storyItems.find(id); + if (i != _storyItems.end()) { + auto &items = i->second; + if (items.remove(item) && items.empty()) { + _storyItems.erase(i); + } + } +} + +void Session::refreshStoryItemViews(FullStoryId id) { + const auto i = _storyItems.find(id); + if (i != _storyItems.end()) { + for (const auto item : i->second) { + if (const auto media = item->media()) { + if (media->storyMention()) { + item->updateStoryMentionText(); + } + } + requestItemViewRefresh(item); + } + } +} + void Session::documentMessageRemoved(not_null document) { if (_documentItems.find(document) != _documentItems.end()) { return; @@ -4254,7 +4353,8 @@ void Session::serviceNotification( MTPstring(), // bot_inline_placeholder MTPstring(), // lang_code MTPEmojiStatus(), - MTPVector())); + MTPVector(), + MTPint())); // stories_max_id } const auto history = this->history(PeerData::kServiceNotificationsId); if (!history->folderKnown()) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 16c56c23e..9ba13d1a9 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -63,6 +63,7 @@ class Stickers; class GroupCall; class NotifySettings; class CustomEmojiManager; +class Stories; struct RepliesReadTillUpdate { FullMsgId id; @@ -136,6 +137,9 @@ public: [[nodiscard]] CustomEmojiManager &customEmojiManager() const { return *_customEmojiManager; } + [[nodiscard]] Stories &stories() const { + return *_stories; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -633,6 +637,9 @@ public: not_null item); void registerCallItem(not_null item); void unregisterCallItem(not_null item); + void registerStoryItem(FullStoryId id, not_null item); + void unregisterStoryItem(FullStoryId id, not_null item); + void refreshStoryItemViews(FullStoryId id); void documentMessageRemoved(not_null document); @@ -807,6 +814,7 @@ private: const QString &siteName, const QString &title, const TextWithEntities &description, + FullStoryId storyId, PhotoData *photo, DocumentData *document, WebPageCollage &&collage, @@ -944,6 +952,9 @@ private: UserId, base::flat_set>> _contactViews; std::unordered_set> _callItems; + std::unordered_map< + FullStoryId, + base::flat_set>> _storyItems; base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; @@ -1007,6 +1018,7 @@ private: const std::unique_ptr _forumIcons; const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; + const std::unique_ptr _stories; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index 418de9e51..8912d8990 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -273,28 +273,45 @@ void SponsoredMessages::append( .isForceUserpicDisplay = data.is_show_peer_photo(), }; }; + const auto externalLink = data.vwebpage() + ? qs(data.vwebpage()->data().vurl()) + : QString(); + const auto userpicFromPhoto = [&](const MTPphoto &photo) { + return photo.match([&](const MTPDphoto &data) { + for (const auto &size : data.vsizes().v) { + const auto result = Images::FromPhotoSize( + _session, + data, + size); + if (result.location.valid()) { + return result; + } + } + return ImageWithLocation{}; + }, [](const MTPDphotoEmpty &) { + return ImageWithLocation{}; + }); + }; const auto from = [&]() -> SponsoredFrom { - if (data.vfrom_id()) { + if (const auto webpage = data.vwebpage()) { + const auto &data = webpage->data(); + auto userpic = data.vphoto() + ? userpicFromPhoto(*data.vphoto()) + : ImageWithLocation{}; + return SponsoredFrom{ + .title = qs(data.vsite_name()), + .isExternalLink = true, + .userpic = std::move(userpic), + .isForceUserpicDisplay = message.data().is_show_peer_photo(), + }; + } else if (const auto fromId = data.vfrom_id()) { return makeFrom( - _session->data().peer(peerFromMTP(*data.vfrom_id())), + _session->data().peer(peerFromMTP(*fromId)), (data.vchannel_post() != nullptr)); } Assert(data.vchat_invite()); return data.vchat_invite()->match([&](const MTPDchatInvite &data) { - auto userpic = data.vphoto().match([&](const MTPDphoto &data) { - for (const auto &size : data.vsizes().v) { - const auto result = Images::FromPhotoSize( - _session, - data, - size); - if (result.location.valid()) { - return result; - } - } - return ImageWithLocation{}; - }, [](const MTPDphotoEmpty &) { - return ImageWithLocation{}; - }); + auto userpic = userpicFromPhoto(data.vphoto()); return SponsoredFrom{ .title = qs(data.vtitle()), .isBroadcast = data.is_broadcast(), @@ -339,6 +356,7 @@ void SponsoredMessages::append( .history = history, .msgId = data.vchannel_post().value_or_empty(), .chatInviteHash = hash, + .externalLink = externalLink, .sponsorInfo = std::move(sponsorInfo), .additionalInfo = std::move(additionalInfo), }; @@ -370,7 +388,7 @@ const SponsoredMessages::Entry *SponsoredMessages::find( } auto &list = it->second; const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) { - return e.item->fullId() == fullId; + return e.item && e.item->fullId() == fullId; }); if (entryIt == end(list.entries)) { return nullptr; @@ -426,9 +444,24 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( .peer = data.from.peer, .msgId = data.msgId, .info = std::move(info), + .externalLink = data.externalLink, }; } +void SponsoredMessages::clicked(const FullMsgId &fullId) { + const auto entryPtr = find(fullId); + if (!entryPtr) { + return; + } + const auto randomId = entryPtr->sponsored.randomId; + const auto channel = entryPtr->item->history()->peer->asChannel(); + Assert(channel != nullptr); + _session->api().request(MTPchannels_ClickSponsoredMessage( + channel->inputChannel, + MTP_bytes(randomId) + )).send(); +} + SponsoredMessages::State SponsoredMessages::state( not_null history) const { const auto it = _data.find(history); diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h index 7abbe2ea3..e9cd46b45 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.h +++ b/Telegram/SourceFiles/data/data_sponsored_messages.h @@ -31,6 +31,7 @@ struct SponsoredFrom { bool isBot = false; bool isExactPost = false; bool isRecommended = false; + bool isExternalLink = false; ImageWithLocation userpic; bool isForceUserpicDisplay = false; }; @@ -42,6 +43,7 @@ struct SponsoredMessage { History *history = nullptr; MsgId msgId; QString chatInviteHash; + QString externalLink; TextWithEntities sponsorInfo; TextWithEntities additionalInfo; }; @@ -58,6 +60,7 @@ public: PeerData *peer = nullptr; MsgId msgId; std::vector info; + QString externalLink; }; using RandomId = QByteArray; explicit SponsoredMessages(not_null owner); @@ -69,6 +72,7 @@ public: void request(not_null history, Fn done); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; + void clicked(const FullMsgId &fullId); [[nodiscard]] bool append(not_null history); void inject( diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp new file mode 100644 index 000000000..085958ea2 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -0,0 +1,1813 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_stories.h" + +#include "api/api_report.h" +#include "base/unixtime.h" +#include "apiwrap.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_folder.h" +#include "data/data_photo.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/show.h" +#include "ui/text/text_utilities.h" + +namespace Data { +namespace { + +constexpr auto kMaxResolveTogether = 100; +constexpr auto kIgnorePreloadAroundIfLoaded = 15; +constexpr auto kPreloadAroundCount = 30; +constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); +constexpr auto kIncrementViewsDelay = 5 * crl::time(1000); +constexpr auto kArchiveFirstPerPage = 30; +constexpr auto kArchivePerPage = 100; +constexpr auto kSavedFirstPerPage = 30; +constexpr auto kSavedPerPage = 100; +constexpr auto kMaxPreloadSources = 10; +constexpr auto kStillPreloadFromFirst = 3; +constexpr auto kMaxSegmentsCount = 180; +constexpr auto kPollingIntervalChat = 5 * TimeId(60); +constexpr auto kPollingIntervalViewer = 1 * TimeId(60); +constexpr auto kPollViewsInterval = 10 * crl::time(1000); +constexpr auto kPollingViewsPerPage = Story::kRecentViewersMax; + +using UpdateFlag = StoryUpdate::Flag; + +[[nodiscard]] std::optional ParseMedia( + not_null owner, + const MTPMessageMedia &media) { + return media.match([&](const MTPDmessageMediaPhoto &data) + -> std::optional { + if (const auto photo = data.vphoto()) { + const auto result = owner->processPhoto(*photo); + if (!result->isNull()) { + return StoryMedia{ result }; + } + } + return {}; + }, [&](const MTPDmessageMediaDocument &data) + -> std::optional { + if (const auto document = data.vdocument()) { + const auto result = owner->processDocument(*document); + if (!result->isNull() + && (result->isGifv() || result->isVideoFile())) { + result->setStoryMedia(true); + return StoryMedia{ result }; + } + } + return {}; + }, [&](const MTPDmessageMediaUnsupported &data) { + return std::make_optional(StoryMedia{ v::null }); + }, [](const auto &) { return std::optional(); }); +} + +} // namespace + +StoriesSourceInfo StoriesSource::info() const { + return { + .id = user->id, + .last = ids.empty() ? 0 : ids.back().date, + .count = uint32(std::min(int(ids.size()), kMaxSegmentsCount)), + .unreadCount = uint32(std::min(unreadCount(), kMaxSegmentsCount)), + .premium = user->isPremium() ? 1U : 0U, + }; +} + +int StoriesSource::unreadCount() const { + const auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 }); + return int(end(ids) - i); +} + +StoryIdDates StoriesSource::toOpen() const { + if (ids.empty()) { + return {}; + } + const auto i = ids.lower_bound(StoryIdDates{ readTill + 1 }); + return (i != end(ids)) ? *i : ids.front(); +} + +Stories::Stories(not_null owner) +: _owner(owner) +, _expireTimer([=] { processExpired(); }) +, _markReadTimer([=] { sendMarkAsReadRequests(); }) +, _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) +, _pollingTimer([=] { sendPollingRequests(); }) +, _pollingViewsTimer([=] { sendPollingViewsRequests(); }) { +} + +Stories::~Stories() { + Expects(_pollingSettings.empty()); + Expects(_pollingViews.empty()); +} + +Session &Stories::owner() const { + return *_owner; +} + +Main::Session &Stories::session() const { + return _owner->session(); +} + +void Stories::apply(const MTPDupdateStory &data) { + const auto peerId = peerFromUser(data.vuser_id()); + const auto user = not_null(_owner->peer(peerId)->asUser()); + const auto now = base::unixtime::now(); + const auto idDates = parseAndApply(user, data.vstory(), now); + if (!idDates) { + return; + } + const auto expired = (idDates.expires <= now); + if (expired) { + applyExpired({ peerId, idDates.id }); + return; + } + const auto i = _all.find(peerId); + if (i == end(_all)) { + requestUserStories(user); + return; + } else if (i->second.ids.contains(idDates)) { + return; + } + const auto wasInfo = i->second.info(); + i->second.ids.emplace(idDates); + const auto nowInfo = i->second.info(); + if (user->isSelf() && i->second.readTill < idDates.id) { + _readTill[user->id] = i->second.readTill = idDates.id; + } + if (wasInfo == nowInfo) { + return; + } + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i != end(sources)) { + *i = nowInfo; + sort(list); + } + }; + if (user->hasStoriesHidden()) { + refreshInList(StorySourcesList::Hidden); + } else { + refreshInList(StorySourcesList::NotHidden); + } + _sourceChanged.fire_copy(peerId); + updateUserStoriesState(user); +} + +void Stories::apply(const MTPDupdateReadStories &data) { + bumpReadTill(peerFromUser(data.vuser_id()), data.vmax_id().v); +} + +void Stories::apply(not_null peer, const MTPUserStories *data) { + if (!data) { + applyDeletedFromSources(peer->id, StorySourcesList::NotHidden); + applyDeletedFromSources(peer->id, StorySourcesList::Hidden); + _all.erase(peer->id); + _sourceChanged.fire_copy(peer->id); + updateUserStoriesState(peer); + } else { + parseAndApply(*data); + } +} + +Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) { + const auto idDates = parseAndApply( + _owner->peer(peerId), + story, + base::unixtime::now()); + const auto value = idDates + ? lookup({ peerId, idDates.id }) + : base::make_unexpected(NoStory::Deleted); + return value ? value->get() : nullptr; +} + +void Stories::requestUserStories( + not_null user, + Fn done) { + const auto [i, ok] = _requestingUserStories.emplace(user); + if (done) { + i->second.push_back(std::move(done)); + } + if (!ok) { + return; + } + const auto finish = [=] { + if (const auto callbacks = _requestingUserStories.take(user)) { + for (const auto &callback : *callbacks) { + callback(); + } + } + }; + _owner->session().api().request(MTPstories_GetUserStories( + user->inputUser + )).done([=](const MTPstories_UserStories &result) { + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + parseAndApply(data.vstories()); + finish(); + }).fail([=] { + applyDeletedFromSources(user->id, StorySourcesList::NotHidden); + applyDeletedFromSources(user->id, StorySourcesList::Hidden); + finish(); + }).send(); +} + +void Stories::registerExpiring(TimeId expires, FullStoryId id) { + for (auto i = _expiring.findFirst(expires) + ; (i != end(_expiring)) && (i->first == expires) + ; ++i) { + if (i->second == id) { + return; + } + } + const auto reschedule = _expiring.empty() + || (_expiring.front().first > expires); + _expiring.emplace(expires, id); + if (reschedule) { + scheduleExpireTimer(); + } +} + +void Stories::scheduleExpireTimer() { + if (_expireSchedulePosted) { + return; + } + _expireSchedulePosted = true; + crl::on_main(this, [=] { + if (!_expireSchedulePosted) { + return; + } + _expireSchedulePosted = false; + if (_expiring.empty()) { + _expireTimer.cancel(); + } else { + const auto nearest = _expiring.front().first; + const auto now = base::unixtime::now(); + const auto delay = (nearest > now) + ? (nearest - now) + : 0; + _expireTimer.callOnce(delay * crl::time(1000)); + } + }); +} + +void Stories::processExpired() { + const auto now = base::unixtime::now(); + auto expired = base::flat_set(); + auto i = begin(_expiring); + for (; i != end(_expiring) && i->first <= now; ++i) { + expired.emplace(i->second); + } + _expiring.erase(begin(_expiring), i); + for (const auto &id : expired) { + applyExpired(id); + } + if (!_expiring.empty()) { + scheduleExpireTimer(); + } +} + +void Stories::parseAndApply(const MTPUserStories &stories) { + const auto &data = stories.data(); + const auto peerId = peerFromUser(data.vuser_id()); + const auto already = _readTill.find(peerId); + const auto readTill = std::max( + data.vmax_read_id().value_or_empty(), + (already != end(_readTill) ? already->second : 0)); + const auto user = _owner->peer(peerId)->asUser(); + auto result = StoriesSource{ + .user = user, + .readTill = readTill, + .hidden = user->hasStoriesHidden(), + }; + const auto &list = data.vstories().v; + const auto now = base::unixtime::now(); + result.ids.reserve(list.size()); + for (const auto &story : list) { + if (const auto id = parseAndApply(result.user, story, now)) { + result.ids.emplace(id); + } + } + if (result.ids.empty()) { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::Hidden); + user->setStoriesState(UserData::StoriesState::None); + return; + } else if (user->isSelf()) { + result.readTill = result.ids.back().id; + } + _readTill[peerId] = result.readTill; + const auto info = result.info(); + const auto i = _all.find(peerId); + if (i != end(_all)) { + if (i->second != result) { + i->second = std::move(result); + } + } else { + _all.emplace(peerId, std::move(result)); + } + const auto add = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i == end(sources)) { + sources.push_back(info); + } else if (*i == info) { + return; + } else { + *i = info; + } + sort(list); + }; + if (result.user->isContact()) { + const auto hidden = result.user->hasStoriesHidden(); + using List = StorySourcesList; + add(hidden ? List::Hidden : List::NotHidden); + applyDeletedFromSources( + peerId, + hidden ? List::NotHidden : List::Hidden); + } else { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::Hidden); + } + _sourceChanged.fire_copy(peerId); + updateUserStoriesState(result.user); +} + +Story *Stories::parseAndApply( + not_null peer, + const MTPDstoryItem &data, + TimeId now) { + const auto media = ParseMedia(_owner, data.vmedia()); + if (!media) { + return nullptr; + } + const auto expires = data.vexpire_date().v; + const auto expired = (expires <= now); + if (expired && !data.is_pinned() && !peer->isSelf()) { + return nullptr; + } + const auto id = data.vid().v; + const auto fullId = FullStoryId{ peer->id, id }; + auto &stories = _stories[peer->id]; + const auto i = stories.find(id); + if (i != end(stories)) { + const auto result = i->second.get(); + const auto mediaChanged = (result->media() != *media); + result->applyChanges(*media, data, now); + const auto j = _pollingSettings.find(result); + if (j != end(_pollingSettings)) { + maybeSchedulePolling(result, j->second, now); + } + if (mediaChanged) { + _preloaded.remove(fullId); + if (_preloading && _preloading->id() == fullId) { + _preloading = nullptr; + rebuildPreloadSources(StorySourcesList::NotHidden); + rebuildPreloadSources(StorySourcesList::Hidden); + continuePreloading(); + } + _owner->refreshStoryItemViews(fullId); + } + return result; + } + const auto wasDeleted = _deleted.remove(fullId); + const auto result = stories.emplace(id, std::make_unique( + id, + peer, + StoryMedia{ *media }, + data, + now + )).first->second.get(); + + if (peer->isSelf()) { + const auto added = _archive.list.emplace(id).second; + if (added) { + if (_archiveTotal >= 0 && id > _archiveLastId) { + ++_archiveTotal; + } + _archiveChanged.fire({}); + } + } + + if (expired) { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } else { + registerExpiring(expires, fullId); + } + + if (wasDeleted) { + _owner->refreshStoryItemViews(fullId); + } + + return result; +} + +StoryIdDates Stories::parseAndApply( + not_null peer, + const MTPstoryItem &story, + TimeId now) { + return story.match([&](const MTPDstoryItem &data) { + if (const auto story = parseAndApply(peer, data, now)) { + return story->idDates(); + } + applyDeleted({ peer->id, data.vid().v }); + return StoryIdDates(); + }, [&](const MTPDstoryItemSkipped &data) { + const auto expires = data.vexpire_date().v; + const auto expired = (expires <= now); + const auto fullId = FullStoryId{ peer->id, data.vid().v }; + if (!expired) { + registerExpiring(expires, fullId); + } else if (!peer->isSelf()) { + applyDeleted(fullId); + return StoryIdDates(); + } else { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } + return StoryIdDates{ + data.vid().v, + data.vdate().v, + data.vexpire_date().v, + }; + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + return StoryIdDates(); + }); +} + +void Stories::updateDependentMessages(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + for (const auto &dependent : i->second) { + dependent->updateDependencyItem(); + } + } + session().changes().storyUpdated( + story, + Data::StoryUpdate::Flag::Edited); +} + +void Stories::registerDependentMessage( + not_null dependent, + not_null dependency) { + _dependentMessages[dependency].emplace(dependent); +} + +void Stories::unregisterDependentMessage( + not_null dependent, + not_null dependency) { + const auto i = _dependentMessages.find(dependency); + if (i != end(_dependentMessages)) { + if (i->second.remove(dependent) && i->second.empty()) { + _dependentMessages.erase(i); + } + } +} + +void Stories::savedStateChanged(not_null story) { + const auto id = story->id(); + const auto peer = story->peer()->id; + const auto pinned = story->pinned(); + if (pinned) { + auto &saved = _saved[peer]; + const auto added = saved.ids.list.emplace(id).second; + if (added) { + if (saved.total >= 0 && id > saved.lastId) { + ++saved.total; + } + _savedChanged.fire_copy(peer); + } + } else if (const auto i = _saved.find(peer); i != end(_saved)) { + auto &saved = i->second; + if (saved.ids.list.remove(id)) { + if (saved.total > 0) { + --saved.total; + } + _savedChanged.fire_copy(peer); + } + } +} + +void Stories::loadMore(StorySourcesList list) { + const auto index = static_cast(list); + if (_loadMoreRequestId[index] || _sourcesLoaded[index]) { + return; + } + const auto hidden = (list == StorySourcesList::Hidden); + const auto api = &_owner->session().api(); + using Flag = MTPstories_GetAllStories::Flag; + _loadMoreRequestId[index] = api->request(MTPstories_GetAllStories( + MTP_flags((hidden ? Flag::f_hidden : Flag()) + | (_sourcesStates[index].isEmpty() + ? Flag(0) + : (Flag::f_next | Flag::f_state))), + MTP_string(_sourcesStates[index]) + )).done([=](const MTPstories_AllStories &result) { + _loadMoreRequestId[index] = 0; + + result.match([&](const MTPDstories_allStories &data) { + _owner->processUsers(data.vusers()); + _sourcesStates[index] = qs(data.vstate()); + _sourcesLoaded[index] = !data.is_has_more(); + for (const auto &single : data.vuser_stories().v) { + parseAndApply(single); + } + }, [](const MTPDstories_allStoriesNotModified &) { + }); + + preloadListsMore(); + }).fail([=] { + _loadMoreRequestId[index] = 0; + }).send(); +} + +void Stories::preloadListsMore() { + if (_loadMoreRequestId[static_cast(StorySourcesList::NotHidden)] + || _loadMoreRequestId[static_cast(StorySourcesList::Hidden)]) { + return; + } + const auto loading = [&](StorySourcesList list) { + return _loadMoreRequestId[static_cast(list)] != 0; + }; + const auto countLoaded = [&](StorySourcesList list) { + const auto index = static_cast(list); + return _sourcesLoaded[index] || !_sourcesStates[index].isEmpty(); + }; + if (loading(StorySourcesList::NotHidden) + || loading(StorySourcesList::Hidden)) { + return; + } else if (!countLoaded(StorySourcesList::NotHidden)) { + loadMore(StorySourcesList::NotHidden); + } else if (!countLoaded(StorySourcesList::Hidden)) { + loadMore(StorySourcesList::Hidden); + } else if (!archiveCountKnown()) { + archiveLoadMore(); + } +} + +void Stories::notifySourcesChanged(StorySourcesList list) { + _sourcesChanged[static_cast(list)].fire({}); + if (list == StorySourcesList::Hidden) { + pushHiddenCountsToFolder(); + } +} + +void Stories::pushHiddenCountsToFolder() { + const auto &list = sources(StorySourcesList::Hidden); + if (list.empty()) { + if (_folderForHidden) { + _folderForHidden->updateStoriesCount(0, 0); + } + return; + } + if (!_folderForHidden) { + _folderForHidden = _owner->folder(Folder::kId); + } + const auto count = int(list.size()); + const auto unread = ranges::count_if( + list, + [](const StoriesSourceInfo &info) { return info.unreadCount > 0; }); + _folderForHidden->updateStoriesCount(count, unread); +} + +void Stories::sendResolveRequests() { + if (!_resolveSent.empty()) { + return; + } + auto leftToSend = kMaxResolveTogether; + auto byPeer = base::flat_map>(); + for (auto i = begin(_resolvePending); i != end(_resolvePending);) { + const auto peerId = i->first; + auto &ids = i->second; + auto &sent = _resolveSent[peerId]; + if (ids.size() <= leftToSend) { + sent = base::take(ids); + i = _resolvePending.erase(i); // Invalidates `ids`. + leftToSend -= int(sent.size()); + } else { + sent = { + std::make_move_iterator(begin(ids)), + std::make_move_iterator(begin(ids) + leftToSend) + }; + ids.erase(begin(ids), begin(ids) + leftToSend); + leftToSend = 0; + } + auto &prepared = byPeer[peerId]; + for (auto &[storyId, callbacks] : sent) { + prepared.push_back(MTP_int(storyId)); + } + if (!leftToSend) { + break; + } + } + const auto api = &_owner->session().api(); + for (auto &entry : byPeer) { + const auto peerId = entry.first; + auto &prepared = entry.second; + const auto finish = [=](PeerId peerId) { + const auto sent = _resolveSent.take(peerId); + Assert(sent.has_value()); + for (const auto &[storyId, list] : *sent) { + finalizeResolve({ peerId, storyId }); + for (const auto &callback : list) { + callback(); + } + } + _itemsChanged.fire_copy(peerId); + if (_resolveSent.empty() && !_resolvePending.empty()) { + crl::on_main(&session(), [=] { sendResolveRequests(); }); + } + }; + const auto user = _owner->session().data().peer(peerId)->asUser(); + if (!user) { + finish(peerId); + continue; + } + api->request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(prepared) + )).done([=](const MTPstories_Stories &result) { + owner().processUsers(result.data().vusers()); + processResolvedStories(user, result.data().vstories().v); + finish(user->id); + }).fail([=] { + finish(peerId); + }).send(); + } +} + +void Stories::processResolvedStories( + not_null peer, + const QVector &list) { + const auto now = base::unixtime::now(); + for (const auto &item : list) { + item.match([&](const MTPDstoryItem &data) { + if (!parseAndApply(peer, data, now)) { + applyDeleted({ peer->id, data.vid().v }); + } + }, [&](const MTPDstoryItemSkipped &data) { + LOG(("API Error: Unexpected storyItemSkipped in resolve.")); + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + }); + } +} + +void Stories::finalizeResolve(FullStoryId id) { + const auto already = lookup(id); + if (!already.has_value() && already.error() == NoStory::Unknown) { + LOG(("API Error: Could not resolve story %1_%2" + ).arg(id.peer.value + ).arg(id.story)); + applyDeleted(id); + } +} + +void Stories::applyDeleted(FullStoryId id) { + applyRemovedFromActive(id); + + _deleted.emplace(id); + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + const auto &story = _deletingStories[id] = std::move(j->second); + _expiring.remove(story->expires(), story->fullId()); + i->second.erase(j); + + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + if (id.peer == session().userPeerId() + && _archive.list.remove(id.story)) { + if (_archiveTotal > 0) { + --_archiveTotal; + } + _archiveChanged.fire({}); + } + if (story->pinned()) { + if (const auto k = _saved.find(id.peer); k != end(_saved)) { + const auto saved = &k->second; + if (saved->ids.list.remove(id.story)) { + if (saved->total > 0) { + --saved->total; + } + _savedChanged.fire_copy(id.peer); + } + } + } + if (_preloading && _preloading->id() == id) { + preloadFinished(id); + } + _owner->refreshStoryItemViews(id); + Assert(!_pollingSettings.contains(story.get())); + if (const auto j = _items.find(id.peer); j != end(_items)) { + const auto k = j->second.find(id.story); + if (k != end(j->second)) { + Assert(!k->second.lock()); + j->second.erase(k); + if (j->second.empty()) { + _items.erase(j); + } + } + } + if (i->second.empty()) { + _stories.erase(i); + } + _deletingStories.remove(id); + } + } +} + +void Stories::applyExpired(FullStoryId id) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + if (!story->peer()->isSelf() && !story->pinned()) { + applyDeleted(id); + return; + } + } + applyRemovedFromActive(id); +} + +void Stories::applyRemovedFromActive(FullStoryId id) { + const auto removeFromList = [&](StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto i = ranges::find( + sources, + id.peer, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); + notifySourcesChanged(list); + } + }; + const auto i = _all.find(id.peer); + if (i != end(_all)) { + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); + if (j != end(i->second.ids) && j->id == id.story) { + i->second.ids.erase(j); + const auto user = i->second.user; + if (i->second.ids.empty()) { + _all.erase(i); + removeFromList(StorySourcesList::NotHidden); + removeFromList(StorySourcesList::Hidden); + } + _sourceChanged.fire_copy(id.peer); + updateUserStoriesState(user); + } + } +} + +void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + id, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); + } + notifySourcesChanged(list); +} + +void Stories::removeDependencyStory(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + const auto items = std::move(i->second); + _dependentMessages.erase(i); + + for (const auto &dependent : items) { + dependent->dependencyStoryRemoved(story); + } + } +} + +void Stories::sort(StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto self = _owner->session().userPeerId(); + const auto changelogSenderId = UserData::kServiceNotificationsId; + const auto proj = [&](const StoriesSourceInfo &info) { + const auto key = int64(info.last) + + (info.premium ? (int64(1) << 47) : 0) + + ((info.id == changelogSenderId) ? (int64(1) << 47) : 0) + + ((info.unreadCount > 0) ? (int64(1) << 49) : 0) + + ((info.id == self) ? (int64(1) << 50) : 0); + return std::make_pair(key, info.id); + }; + ranges::sort(sources, ranges::greater(), proj); + notifySourcesChanged(list); + preloadSourcesChanged(list); +} + +std::shared_ptr Stories::lookupItem(not_null story) { + const auto i = _items.find(story->peer()->id); + if (i == end(_items)) { + return nullptr; + } + const auto j = i->second.find(story->id()); + if (j == end(i->second)) { + return nullptr; + } + return j->second.lock(); +} + +std::shared_ptr Stories::resolveItem(not_null story) { + auto &items = _items[story->peer()->id]; + auto i = items.find(story->id()); + if (i == end(items)) { + i = items.emplace(story->id()).first; + } else if (const auto result = i->second.lock()) { + return result; + } + const auto history = _owner->history(story->peer()); + auto result = std::shared_ptr( + history->makeMessage(story).get(), + HistoryItem::Destroyer()); + i->second = result; + return result; +} + +std::shared_ptr Stories::resolveItem(FullStoryId id) { + const auto story = lookup(id); + return story ? resolveItem(*story) : std::shared_ptr(); +} + +const StoriesSource *Stories::source(PeerId id) const { + const auto i = _all.find(id); + return (i != end(_all)) ? &i->second : nullptr; +} + +const std::vector &Stories::sources( + StorySourcesList list) const { + return _sources[static_cast(list)]; +} + +bool Stories::sourcesLoaded(StorySourcesList list) const { + return _sourcesLoaded[static_cast(list)]; +} + +rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { + return _sourcesChanged[static_cast(list)].events(); +} + +rpl::producer Stories::sourceChanged() const { + return _sourceChanged.events(); +} + +rpl::producer Stories::itemsChanged() const { + return _itemsChanged.events(); +} + +base::expected, NoStory> Stories::lookup( + FullStoryId id) const { + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + return j->second.get(); + } + } + return base::make_unexpected( + _deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown); +} + +void Stories::resolve(FullStoryId id, Fn done, bool force) { + if (!force) { + const auto already = lookup(id); + if (already.has_value() || already.error() != NoStory::Unknown) { + if (done) { + done(); + } + return; + } + } + if (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) { + if (const auto j = i->second.find(id.story); j != end(i->second)) { + if (done) { + j->second.push_back(std::move(done)); + } + return; + } + } + auto &ids = _resolvePending[id.peer]; + if (ids.empty()) { + crl::on_main(&session(), [=] { + sendResolveRequests(); + }); + } + auto &callbacks = ids[id.story]; + if (done) { + callbacks.push_back(std::move(done)); + } +} + +void Stories::loadAround(FullStoryId id, StoriesContext context) { + if (v::is(context.data)) { + return; + } else if (v::is(context.data) + || v::is(context.data)) { + return; + } + const auto i = _all.find(id.peer); + if (i == end(_all)) { + return; + } + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); + if (j == end(i->second.ids) || j->id != id.story) { + return; + } + const auto ignore = [&] { + const auto side = kIgnorePreloadAroundIfLoaded; + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); + for (auto k = j - left; k != j + right; ++k) { + const auto maybeStory = lookup({ id.peer, k->id }); + if (!maybeStory && maybeStory.error() == NoStory::Unknown) { + return false; + } + } + return true; + }(); + if (ignore) { + return; + } + const auto side = kPreloadAroundCount; + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); + const auto from = j - left; + const auto till = j + right; + for (auto k = from; k != till; ++k) { + resolve({ id.peer, k->id }, nullptr); + } +} + +void Stories::markAsRead(FullStoryId id, bool viewed) { + if (id.peer == _owner->session().userPeerId()) { + return; + } + const auto maybeStory = lookup(id); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; + if (story->expired() && story->pinned()) { + _incrementViewsPending[id.peer].emplace(id.story); + if (!_incrementViewsTimer.isActive()) { + _incrementViewsTimer.callOnce(kIncrementViewsDelay); + } + } + if (!bumpReadTill(id.peer, id.story)) { + return; + } + if (!_markReadPending.contains(id.peer)) { + sendMarkAsReadRequests(); + } + _markReadPending.emplace(id.peer); + _markReadTimer.callOnce(kMarkAsReadDelay); +} + +bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) { + auto &till = _readTill[peerId]; + auto refreshItems = std::vector(); + const auto guard = gsl::finally([&] { + for (const auto id : refreshItems) { + _owner->refreshStoryItemViews({ peerId, id }); + } + }); + if (till < maxReadTill) { + const auto from = till; + till = maxReadTill; + updateUserStoriesState(_owner->peer(peerId)); + const auto i = _stories.find(peerId); + if (i != end(_stories)) { + refreshItems = ranges::make_subrange( + i->second.lower_bound(from + 1), + i->second.lower_bound(till + 1) + ) | ranges::views::transform([=](const auto &pair) { + _owner->session().changes().storyUpdated( + pair.second.get(), + StoryUpdate::Flag::MarkRead); + return pair.first; + }) | ranges::to_vector; + } + } + const auto i = _all.find(peerId); + if (i == end(_all) || i->second.readTill >= maxReadTill) { + return false; + } + const auto wasUnreadCount = i->second.unreadCount(); + i->second.readTill = maxReadTill; + const auto nowUnreadCount = i->second.unreadCount(); + if (wasUnreadCount != nowUnreadCount) { + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i != end(sources)) { + i->unreadCount = nowUnreadCount; + sort(list); + } + }; + refreshInList(StorySourcesList::NotHidden); + refreshInList(StorySourcesList::Hidden); + } + return true; +} + +void Stories::toggleHidden( + PeerId peerId, + bool hidden, + std::shared_ptr show) { + const auto user = _owner->peer(peerId)->asUser(); + Assert(user != nullptr); + if (user->hasStoriesHidden() != hidden) { + user->setFlags(hidden + ? (user->flags() | UserDataFlag::StoriesHidden) + : (user->flags() & ~UserDataFlag::StoriesHidden)); + session().api().request(MTPcontacts_ToggleStoriesHidden( + user->inputUser, + MTP_bool(hidden) + )).send(); + } + + const auto name = user->shortName(); + const auto guard = gsl::finally([&] { + if (show) { + const auto phrase = hidden + ? tr::lng_stories_hidden_to_contacts + : tr::lng_stories_shown_in_chats; + show->showToast(phrase( + tr::now, + lt_user, + Ui::Text::Bold(name), + Ui::Text::RichLangValue)); + } + }); + + const auto i = _all.find(peerId); + if (i == end(_all)) { + return; + } + i->second.hidden = hidden; + const auto info = i->second.info(); + const auto main = static_cast(StorySourcesList::NotHidden); + const auto other = static_cast(StorySourcesList::Hidden); + const auto proj = &StoriesSourceInfo::id; + if (hidden) { + const auto i = ranges::find(_sources[main], peerId, proj); + if (i != end(_sources[main])) { + _sources[main].erase(i); + notifySourcesChanged(StorySourcesList::NotHidden); + preloadSourcesChanged(StorySourcesList::NotHidden); + } + const auto j = ranges::find(_sources[other], peerId, proj); + if (j == end(_sources[other])) { + _sources[other].push_back(info); + } else { + *j = info; + } + sort(StorySourcesList::Hidden); + } else { + const auto i = ranges::find(_sources[other], peerId, proj); + if (i != end(_sources[other])) { + _sources[other].erase(i); + notifySourcesChanged(StorySourcesList::Hidden); + preloadSourcesChanged(StorySourcesList::Hidden); + } + const auto j = ranges::find(_sources[main], peerId, proj); + if (j == end(_sources[main])) { + _sources[main].push_back(info); + } else { + *j = info; + } + sort(StorySourcesList::NotHidden); + } +} + +void Stories::sendMarkAsReadRequest( + not_null peer, + StoryId tillId) { + Expects(peer->isUser()); + + const auto peerId = peer->id; + _markReadRequests.emplace(peerId); + const auto finish = [=] { + _markReadRequests.remove(peerId); + if (!_markReadTimer.isActive() + && _markReadPending.contains(peerId)) { + sendMarkAsReadRequests(); + } + checkQuitPreventFinished(); + }; + + const auto api = &_owner->session().api(); + api->request(MTPstories_ReadStories( + peer->asUser()->inputUser, + MTP_int(tillId) + )).done(finish).fail(finish).send(); +} + +void Stories::checkQuitPreventFinished() { + if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { + if (Core::Quitting()) { + LOG(("Stories doesn't prevent quit any more.")); + } + Core::App().quitPreventFinished(); + } +} + +void Stories::sendMarkAsReadRequests() { + _markReadTimer.cancel(); + for (auto i = begin(_markReadPending); i != end(_markReadPending);) { + const auto peerId = *i; + if (_markReadRequests.contains(peerId)) { + ++i; + continue; + } + const auto j = _all.find(peerId); + if (j != end(_all)) { + sendMarkAsReadRequest(j->second.user, j->second.readTill); + } + i = _markReadPending.erase(i); + } +} + +void Stories::sendIncrementViewsRequests() { + if (_incrementViewsPending.empty()) { + return; + } + auto ids = QVector(); + struct Prepared { + PeerId peer = 0; + QVector ids; + }; + auto prepared = std::vector(); + for (const auto &[peer, ids] : _incrementViewsPending) { + if (_incrementViewsRequests.contains(peer)) { + continue; + } + prepared.push_back({ .peer = peer }); + for (const auto &id : ids) { + prepared.back().ids.push_back(MTP_int(id)); + } + } + + const auto api = &_owner->session().api(); + for (auto &[peer, ids] : prepared) { + _incrementViewsRequests.emplace(peer); + const auto finish = [=, peer = peer] { + _incrementViewsRequests.remove(peer); + if (!_incrementViewsTimer.isActive() + && _incrementViewsPending.contains(peer)) { + sendIncrementViewsRequests(); + } + checkQuitPreventFinished(); + }; + api->request(MTPstories_IncrementStoryViews( + _owner->peer(peer)->asUser()->inputUser, + MTP_vector(std::move(ids)) + )).done(finish).fail(finish).send(); + _incrementViewsPending.remove(peer); + } +} + +void Stories::loadViewsSlice( + StoryId id, + std::optional offset, + Fn)> done) { + if (_viewsStoryId == id + && _viewsOffset == offset + && (offset || _viewsRequestId)) { + if (_viewsRequestId) { + _viewsDone = std::move(done); + } + return; + } + _viewsStoryId = id; + _viewsOffset = offset; + _viewsDone = std::move(done); + + const auto api = &_owner->session().api(); + const auto perPage = _viewsDone ? kViewsPerPage : kPollingViewsPerPage; + api->request(_viewsRequestId).cancel(); + _viewsRequestId = api->request(MTPstories_GetStoryViewsList( + MTP_int(id), + MTP_int(offset ? offset->date : 0), + MTP_long(offset ? peerToUser(offset->peer->id).bare : 0), + MTP_int(perPage) + )).done([=](const MTPstories_StoryViewsList &result) { + _viewsRequestId = 0; + + auto slice = std::vector(); + + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + slice.reserve(data.vviews().v.size()); + for (const auto &view : data.vviews().v) { + slice.push_back({ + .peer = _owner->peer(peerFromUser(view.data().vuser_id())), + .date = view.data().vdate().v, + }); + } + const auto fullId = FullStoryId{ + .peer = _owner->session().userPeerId(), + .story = _viewsStoryId, + }; + if (const auto story = lookup(fullId)) { + (*story)->applyViewsSlice(_viewsOffset, slice, data.vcount().v); + } + if (const auto done = base::take(_viewsDone)) { + done(std::move(slice)); + } + }).fail([=] { + _viewsRequestId = 0; + if (const auto done = base::take(_viewsDone)) { + done({}); + } + }).send(); +} + +const StoriesIds &Stories::archive() const { + return _archive; +} + +rpl::producer<> Stories::archiveChanged() const { + return _archiveChanged.events(); +} + +int Stories::archiveCount() const { + return std::max(_archiveTotal, 0); +} + +bool Stories::archiveCountKnown() const { + return _archiveTotal >= 0; +} + +bool Stories::archiveLoaded() const { + return _archiveLoaded; +} + +const StoriesIds *Stories::saved(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? &i->second.ids : nullptr; +} + +rpl::producer Stories::savedChanged() const { + return _savedChanged.events(); +} + +int Stories::savedCount(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? i->second.total : 0; +} + +bool Stories::savedCountKnown(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && (i->second.total >= 0); +} + +bool Stories::savedLoaded(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && i->second.loaded; +} + +void Stories::archiveLoadMore() { + if (_archiveRequestId || _archiveLoaded) { + return; + } + const auto api = &_owner->session().api(); + _archiveRequestId = api->request(MTPstories_GetStoriesArchive( + MTP_int(_archiveLastId), + MTP_int(_archiveLastId ? kArchivePerPage : kArchiveFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + _archiveRequestId = 0; + + const auto &data = result.data(); + const auto self = _owner->session().user(); + const auto now = base::unixtime::now(); + _archiveTotal = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + _archive.list.emplace(id); + _archiveLastId = id; + if (!parseAndApply(self, story, now)) { + _archive.list.remove(id); + if (_archiveTotal > 0) { + --_archiveTotal; + } + } + } + const auto ids = int(_archive.list.size()); + _archiveLoaded = data.vstories().v.empty(); + _archiveTotal = _archiveLoaded ? ids : std::max(_archiveTotal, ids); + _archiveChanged.fire({}); + }).fail([=] { + _archiveRequestId = 0; + _archiveLoaded = true; + _archiveTotal = int(_archive.list.size()); + _archiveChanged.fire({}); + }).send(); +} + +void Stories::savedLoadMore(PeerId peerId) { + Expects(peerIsUser(peerId)); + + auto &saved = _saved[peerId]; + if (saved.requestId || saved.loaded) { + return; + } + const auto api = &_owner->session().api(); + const auto peer = _owner->peer(peerId); + saved.requestId = api->request(MTPstories_GetPinnedStories( + peer->asUser()->inputUser, + MTP_int(saved.lastId), + MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + auto &saved = _saved[peerId]; + saved.requestId = 0; + + const auto &data = result.data(); + const auto now = base::unixtime::now(); + saved.total = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + saved.ids.list.emplace(id); + saved.lastId = id; + if (!parseAndApply(peer, story, now)) { + saved.ids.list.remove(id); + if (saved.total > 0) { + --saved.total; + } + } + } + const auto ids = int(saved.ids.list.size()); + saved.loaded = data.vstories().v.empty(); + saved.total = saved.loaded ? ids : std::max(saved.total, ids); + _savedChanged.fire_copy(peerId); + }).fail([=] { + auto &saved = _saved[peerId]; + saved.requestId = 0; + saved.loaded = true; + saved.total = int(saved.ids.list.size()); + _savedChanged.fire_copy(peerId); + }).send(); +} + +void Stories::deleteList(const std::vector &ids) { + auto list = QVector(); + list.reserve(ids.size()); + const auto selfId = session().userPeerId(); + for (const auto &id : ids) { + if (id.peer == selfId) { + list.push_back(MTP_int(id.story)); + } + } + if (!list.empty()) { + const auto api = &_owner->session().api(); + api->request(MTPstories_DeleteStories( + MTP_vector(list) + )).done([=](const MTPVector &result) { + for (const auto &id : result.v) { + applyDeleted({ selfId, id.v }); + } + }).send(); + } +} + +void Stories::togglePinnedList( + const std::vector &ids, + bool pinned) { + auto list = QVector(); + list.reserve(ids.size()); + const auto selfId = session().userPeerId(); + for (const auto &id : ids) { + if (id.peer == selfId) { + list.push_back(MTP_int(id.story)); + } + } + if (list.empty()) { + return; + } + const auto api = &_owner->session().api(); + api->request(MTPstories_TogglePinned( + MTP_vector(list), + MTP_bool(pinned) + )).done([=](const MTPVector &result) { + auto &saved = _saved[selfId]; + const auto loaded = saved.loaded; + const auto lastId = !saved.ids.list.empty() + ? saved.ids.list.back() + : saved.lastId + ? saved.lastId + : std::numeric_limits::max(); + auto dirty = false; + for (const auto &id : result.v) { + if (const auto maybeStory = lookup({ selfId, id.v })) { + const auto story = *maybeStory; + story->setPinned(pinned); + if (pinned) { + const auto add = loaded || (id.v >= lastId); + if (!add) { + dirty = true; + } else if (saved.ids.list.emplace(id.v).second) { + if (saved.total >= 0) { + ++saved.total; + } + } + } else if (saved.ids.list.remove(id.v)) { + if (saved.total > 0) { + --saved.total; + } + } else if (!loaded) { + dirty = true; + } + } else if (!loaded) { + dirty = true; + } + } + if (dirty) { + savedLoadMore(selfId); + } else { + _savedChanged.fire_copy(selfId); + } + }).send(); +} + +void Stories::report( + std::shared_ptr show, + FullStoryId id, + Ui::ReportReason reason, + QString text) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + Api::SendReport(show, story->peer(), reason, text, story->id()); + } +} + +bool Stories::isQuitPrevent() { + if (!_markReadPending.empty()) { + sendMarkAsReadRequests(); + } + if (!_incrementViewsPending.empty()) { + sendIncrementViewsRequests(); + } + if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { + return false; + } + LOG(("Stories prevents quit, marking as read...")); + return true; +} + +void Stories::incrementPreloadingMainSources() { + Expects(_preloadingMainSourcesCounter >= 0); + + if (++_preloadingMainSourcesCounter == 1 + && rebuildPreloadSources(StorySourcesList::NotHidden)) { + continuePreloading(); + } +} + +void Stories::decrementPreloadingMainSources() { + Expects(_preloadingMainSourcesCounter > 0); + + if (!--_preloadingMainSourcesCounter + && rebuildPreloadSources(StorySourcesList::NotHidden)) { + continuePreloading(); + } +} + +void Stories::incrementPreloadingHiddenSources() { + Expects(_preloadingHiddenSourcesCounter >= 0); + + if (++_preloadingHiddenSourcesCounter == 1 + && rebuildPreloadSources(StorySourcesList::Hidden)) { + continuePreloading(); + } +} + +void Stories::decrementPreloadingHiddenSources() { + Expects(_preloadingHiddenSourcesCounter > 0); + + if (!--_preloadingHiddenSourcesCounter + && rebuildPreloadSources(StorySourcesList::Hidden)) { + continuePreloading(); + } +} + +void Stories::setPreloadingInViewer(std::vector ids) { + ids.erase(ranges::remove_if(ids, [&](FullStoryId id) { + return _preloaded.contains(id); + }), end(ids)); + if (_toPreloadViewer != ids) { + _toPreloadViewer = std::move(ids); + continuePreloading(); + } +} + +std::optional Stories::peerSourceState( + not_null peer, + StoryId storyMaxId) { + const auto i = _readTill.find(peer->id); + if (_readTillReceived || (i != end(_readTill))) { + return PeerSourceState{ + .maxId = storyMaxId, + .readTill = std::min( + storyMaxId, + (i != end(_readTill)) ? i->second : 0), + }; + } + requestReadTills(); + _pendingUserStateMaxId[peer] = storyMaxId; + return std::nullopt; +} + +void Stories::requestReadTills() { + if (_readTillReceived || _readTillsRequestId) { + return; + } + const auto api = &_owner->session().api(); + _readTillsRequestId = api->request(MTPstories_GetAllReadUserStories( + )).done([=](const MTPUpdates &result) { + _readTillReceived = true; + api->applyUpdates(result); + for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) { + updateUserStoriesState(peer); + } + for (const auto &storyId : base::take(_pendingReadTillItems)) { + _owner->refreshStoryItemViews(storyId); + } + }).send(); +} + +bool Stories::isUnread(not_null story) { + const auto till = _readTill.find(story->peer()->id); + if (till == end(_readTill) && !_readTillReceived) { + requestReadTills(); + _pendingReadTillItems.emplace(story->fullId()); + return false; + } + const auto readTill = (till != end(_readTill)) ? till->second : 0; + return (story->id() > readTill); +} + +void Stories::registerPolling(not_null story, Polling polling) { + auto &settings = _pollingSettings[story]; + switch (polling) { + case Polling::Chat: ++settings.chat; break; + case Polling::Viewer: + ++settings.viewer; + if (story->peer()->isSelf() + && _pollingViews.emplace(story).second) { + sendPollingViewsRequests(); + } + break; + } + maybeSchedulePolling(story, settings, base::unixtime::now()); +} + +void Stories::unregisterPolling(not_null story, Polling polling) { + const auto i = _pollingSettings.find(story); + Assert(i != end(_pollingSettings)); + + switch (polling) { + case Polling::Chat: + Assert(i->second.chat > 0); + --i->second.chat; + break; + case Polling::Viewer: + Assert(i->second.viewer > 0); + if (!--i->second.viewer) { + _pollingViews.remove(story); + if (_pollingViews.empty()) { + _pollingViewsTimer.cancel(); + } + } + break; + } + if (!i->second.chat && !i->second.viewer) { + _pollingSettings.erase(i); + } +} + +bool Stories::registerPolling(FullStoryId id, Polling polling) { + if (const auto maybeStory = lookup(id)) { + registerPolling(*maybeStory, polling); + return true; + } + return false; +} + +void Stories::unregisterPolling(FullStoryId id, Polling polling) { + if (const auto maybeStory = lookup(id)) { + unregisterPolling(*maybeStory, polling); + } else if (const auto i = _deletingStories.find(id) + ; i != end(_deletingStories)) { + unregisterPolling(i->second.get(), polling); + } else { + Unexpected("Couldn't find story for unregistering polling."); + } +} + +int Stories::pollingInterval(const PollingSettings &settings) const { + return settings.viewer ? kPollingIntervalViewer : kPollingIntervalChat; +} + +void Stories::maybeSchedulePolling( + not_null story, + const PollingSettings &settings, + TimeId now) { + const auto last = story->lastUpdateTime(); + const auto next = last + pollingInterval(settings); + const auto left = std::max(next - now, 0) * crl::time(1000) + 1; + if (!_pollingTimer.isActive() || _pollingTimer.remainingTime() > left) { + _pollingTimer.callOnce(left); + } +} + +void Stories::sendPollingRequests() { + auto min = 0; + const auto now = base::unixtime::now(); + for (const auto &[story, settings] : _pollingSettings) { + const auto last = story->lastUpdateTime(); + const auto next = last + pollingInterval(settings); + if (now >= next) { + resolve(story->fullId(), nullptr, true); + } else { + const auto left = (next - now) * crl::time(1000) + 1; + if (!min || left < min) { + min = left; + } + } + } + if (min > 0) { + _pollingTimer.callOnce(min); + } +} + +void Stories::sendPollingViewsRequests() { + if (_pollingViews.empty()) { + return; + } else if (!_viewsRequestId) { + Assert(_viewsDone == nullptr); + loadViewsSlice(_pollingViews.front()->id(), std::nullopt, nullptr); + } + _pollingViewsTimer.callOnce(kPollViewsInterval); +} + +void Stories::updateUserStoriesState(not_null peer) { + const auto till = _readTill.find(peer->id); + const auto readTill = (till != end(_readTill)) ? till->second : 0; + const auto pendingMaxId = [&] { + const auto j = _pendingUserStateMaxId.find(peer); + return (j != end(_pendingUserStateMaxId)) ? j->second : 0; + }; + const auto i = _all.find(peer->id); + const auto max = (i != end(_all)) + ? (i->second.ids.empty() ? 0 : i->second.ids.back().id) + : pendingMaxId(); + if (const auto user = peer->asUser()) { + user->setStoriesState(!max + ? UserData::StoriesState::None + : (max <= readTill) + ? UserData::StoriesState::HasRead + : UserData::StoriesState::HasUnread); + } +} + +void Stories::preloadSourcesChanged(StorySourcesList list) { + if (rebuildPreloadSources(list)) { + continuePreloading(); + } +} + +bool Stories::rebuildPreloadSources(StorySourcesList list) { + const auto index = static_cast(list); + const auto &counter = (list == StorySourcesList::Hidden) + ? _preloadingHiddenSourcesCounter + : _preloadingMainSourcesCounter; + if (!counter) { + return !base::take(_toPreloadSources[index]).empty(); + } + auto now = std::vector(); + auto processed = 0; + for (const auto &source : _sources[index]) { + const auto i = _all.find(source.id); + if (i != end(_all)) { + if (const auto id = i->second.toOpen().id) { + const auto fullId = FullStoryId{ source.id, id }; + if (!_preloaded.contains(fullId)) { + now.push_back(fullId); + } + } + } + if (++processed >= kMaxPreloadSources) { + break; + } + } + if (now != _toPreloadSources[index]) { + _toPreloadSources[index] = std::move(now); + return true; + } + return false; +} + +void Stories::continuePreloading() { + const auto now = _preloading ? _preloading->id() : FullStoryId(); + if (now) { + if (shouldContinuePreload(now)) { + return; + } + _preloading = nullptr; + } + const auto id = nextPreloadId(); + if (!id) { + return; + } else if (const auto maybeStory = lookup(id)) { + startPreloading(*maybeStory); + } +} + +bool Stories::shouldContinuePreload(FullStoryId id) const { + const auto first = ranges::views::concat( + _toPreloadViewer, + _toPreloadSources[static_cast(StorySourcesList::Hidden)], + _toPreloadSources[static_cast(StorySourcesList::NotHidden)] + ) | ranges::views::take(kStillPreloadFromFirst); + return ranges::contains(first, id); +} + +FullStoryId Stories::nextPreloadId() const { + const auto hidden = static_cast(StorySourcesList::Hidden); + const auto main = static_cast(StorySourcesList::NotHidden); + const auto result = !_toPreloadViewer.empty() + ? _toPreloadViewer.front() + : !_toPreloadSources[hidden].empty() + ? _toPreloadSources[hidden].front() + : !_toPreloadSources[main].empty() + ? _toPreloadSources[main].front() + : FullStoryId(); + + Ensures(!_preloaded.contains(result)); + return result; +} + +void Stories::startPreloading(not_null story) { + Expects(!_preloaded.contains(story->fullId())); + + const auto id = story->fullId(); + auto preloading = std::make_unique(story, [=] { + _preloading = nullptr; + preloadFinished(id, true); + }); + if (!_preloaded.contains(id)) { + _preloading = std::move(preloading); + } +} + +void Stories::preloadFinished(FullStoryId id, bool markAsPreloaded) { + for (auto &sources : _toPreloadSources) { + sources.erase(ranges::remove(sources, id), end(sources)); + } + _toPreloadViewer.erase( + ranges::remove(_toPreloadViewer, id), + end(_toPreloadViewer)); + if (markAsPreloaded) { + _preloaded.emplace(id); + } + crl::on_main(this, [=] { + continuePreloading(); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h new file mode 100644 index 000000000..c4d55a129 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.h @@ -0,0 +1,380 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/qt/qt_compare.h" +#include "base/expected.h" +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "data/data_story.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class Show; +enum class ReportReason; +} // namespace Ui + +namespace Data { + +class Folder; +class Session; +struct StoryView; +struct StoryIdDates; +class Story; +class StoryPreload; + +struct StoriesIds { + base::flat_set> list; + + friend inline bool operator==( + const StoriesIds&, + const StoriesIds&) = default; +}; + +struct StoriesSourceInfo { + PeerId id = 0; + TimeId last = 0; + uint32 count : 15 = 0; + uint32 unreadCount : 15 = 0; + uint32 premium : 1 = 0; + + friend inline bool operator==( + StoriesSourceInfo, + StoriesSourceInfo) = default; +}; + +struct StoriesSource { + not_null user; + base::flat_set ids; + StoryId readTill = 0; + bool hidden = false; + + [[nodiscard]] StoriesSourceInfo info() const; + [[nodiscard]] int unreadCount() const; + [[nodiscard]] StoryIdDates toOpen() const; + + friend inline bool operator==(StoriesSource, StoriesSource) = default; +}; + +enum class NoStory : uchar { + Unknown, + Deleted, +}; + +enum class StorySourcesList : uchar { + NotHidden, + Hidden, +}; + +struct StoriesContextSingle { + friend inline auto operator<=>( + StoriesContextSingle, + StoriesContextSingle) = default; + friend inline bool operator==(StoriesContextSingle, StoriesContextSingle) = default; +}; + +struct StoriesContextPeer { + friend inline auto operator<=>( + StoriesContextPeer, + StoriesContextPeer) = default; + friend inline bool operator==(StoriesContextPeer, StoriesContextPeer) = default; +}; + +struct StoriesContextSaved { + friend inline auto operator<=>( + StoriesContextSaved, + StoriesContextSaved) = default; + friend inline bool operator==(StoriesContextSaved, StoriesContextSaved) = default; +}; + +struct StoriesContextArchive { + friend inline auto operator<=>( + StoriesContextArchive, + StoriesContextArchive) = default; + friend inline bool operator==(StoriesContextArchive, StoriesContextArchive) = default; +}; + +struct StoriesContext { + std::variant< + StoriesContextSingle, + StoriesContextPeer, + StoriesContextSaved, + StoriesContextArchive, + StorySourcesList> data; + + friend inline auto operator<=>( + StoriesContext, + StoriesContext) = default; + friend inline bool operator==(StoriesContext, StoriesContext) = default; +}; + +inline constexpr auto kStorySourcesListCount = 2; + +class Stories final : public base::has_weak_ptr { +public: + explicit Stories(not_null owner); + ~Stories(); + + static constexpr auto kPinnedToastDuration = 4 * crl::time(1000); + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + void updateDependentMessages(not_null story); + void registerDependentMessage( + not_null dependent, + not_null dependency); + void unregisterDependentMessage( + not_null dependent, + not_null dependency); + + void loadMore(StorySourcesList list); + void apply(const MTPDupdateStory &data); + void apply(const MTPDupdateReadStories &data); + void apply(not_null peer, const MTPUserStories *data); + Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story); + void loadAround(FullStoryId id, StoriesContext context); + + const StoriesSource *source(PeerId id) const; + [[nodiscard]] const std::vector &sources( + StorySourcesList list) const; + [[nodiscard]] bool sourcesLoaded(StorySourcesList list) const; + [[nodiscard]] rpl::producer<> sourcesChanged( + StorySourcesList list) const; + [[nodiscard]] rpl::producer sourceChanged() const; + [[nodiscard]] rpl::producer itemsChanged() const; + + [[nodiscard]] base::expected, NoStory> lookup( + FullStoryId id) const; + void resolve(FullStoryId id, Fn done, bool force = false); + [[nodiscard]] std::shared_ptr resolveItem(FullStoryId id); + [[nodiscard]] std::shared_ptr resolveItem( + not_null story); + + [[nodiscard]] bool isQuitPrevent(); + void markAsRead(FullStoryId id, bool viewed); + + void toggleHidden( + PeerId peerId, + bool hidden, + std::shared_ptr show); + + static constexpr auto kViewsPerPage = 50; + void loadViewsSlice( + StoryId id, + std::optional offset, + Fn)> done); + + [[nodiscard]] const StoriesIds &archive() const; + [[nodiscard]] rpl::producer<> archiveChanged() const; + [[nodiscard]] int archiveCount() const; + [[nodiscard]] bool archiveCountKnown() const; + [[nodiscard]] bool archiveLoaded() const; + void archiveLoadMore(); + + [[nodiscard]] const StoriesIds *saved(PeerId peerId) const; + [[nodiscard]] rpl::producer savedChanged() const; + [[nodiscard]] int savedCount(PeerId peerId) const; + [[nodiscard]] bool savedCountKnown(PeerId peerId) const; + [[nodiscard]] bool savedLoaded(PeerId peerId) const; + void savedLoadMore(PeerId peerId); + + void deleteList(const std::vector &ids); + void togglePinnedList(const std::vector &ids, bool pinned); + void report( + std::shared_ptr show, + FullStoryId id, + Ui::ReportReason reason, + QString text); + + void incrementPreloadingMainSources(); + void decrementPreloadingMainSources(); + void incrementPreloadingHiddenSources(); + void decrementPreloadingHiddenSources(); + void setPreloadingInViewer(std::vector ids); + + struct PeerSourceState { + StoryId maxId = 0; + StoryId readTill = 0; + }; + [[nodiscard]] std::optional peerSourceState( + not_null peer, + StoryId storyMaxId); + [[nodiscard]] bool isUnread(not_null story); + + enum class Polling { + Chat, + Viewer, + }; + void registerPolling(not_null story, Polling polling); + void unregisterPolling(not_null story, Polling polling); + + bool registerPolling(FullStoryId id, Polling polling); + void unregisterPolling(FullStoryId id, Polling polling); + void requestUserStories( + not_null user, + Fn done = nullptr); + + void savedStateChanged(not_null story); + [[nodiscard]] std::shared_ptr lookupItem( + not_null story); + +private: + struct Saved { + StoriesIds ids; + int total = -1; + StoryId lastId = 0; + bool loaded = false; + mtpRequestId requestId = 0; + }; + struct PollingSettings { + int chat = 0; + int viewer = 0; + }; + + void parseAndApply(const MTPUserStories &stories); + [[nodiscard]] Story *parseAndApply( + not_null peer, + const MTPDstoryItem &data, + TimeId now); + StoryIdDates parseAndApply( + not_null peer, + const MTPstoryItem &story, + TimeId now); + void processResolvedStories( + not_null peer, + const QVector &list); + void sendResolveRequests(); + void finalizeResolve(FullStoryId id); + void updateUserStoriesState(not_null peer); + + void applyDeleted(FullStoryId id); + void applyExpired(FullStoryId id); + void applyRemovedFromActive(FullStoryId id); + void applyDeletedFromSources(PeerId id, StorySourcesList list); + void removeDependencyStory(not_null story); + void sort(StorySourcesList list); + bool bumpReadTill(PeerId peerId, StoryId maxReadTill); + void requestReadTills(); + + void sendMarkAsReadRequests(); + void sendMarkAsReadRequest(not_null peer, StoryId tillId); + void sendIncrementViewsRequests(); + void checkQuitPreventFinished(); + + void registerExpiring(TimeId expires, FullStoryId id); + void scheduleExpireTimer(); + void processExpired(); + + void preloadSourcesChanged(StorySourcesList list); + bool rebuildPreloadSources(StorySourcesList list); + void continuePreloading(); + [[nodiscard]] bool shouldContinuePreload(FullStoryId id) const; + [[nodiscard]] FullStoryId nextPreloadId() const; + void startPreloading(not_null story); + void preloadFinished(FullStoryId id, bool markAsPreloaded = false); + void preloadListsMore(); + + void notifySourcesChanged(StorySourcesList list); + void pushHiddenCountsToFolder(); + + [[nodiscard]] int pollingInterval( + const PollingSettings &settings) const; + void maybeSchedulePolling( + not_null story, + const PollingSettings &settings, + TimeId now); + void sendPollingRequests(); + void sendPollingViewsRequests(); + + const not_null _owner; + std::unordered_map< + PeerId, + base::flat_map>> _stories; + base::flat_map> _deletingStories; + std::unordered_map< + PeerId, + base::flat_map>> _items; + base::flat_multi_map _expiring; + base::flat_set _deleted; + base::Timer _expireTimer; + bool _expireSchedulePosted = false; + + base::flat_map< + PeerId, + base::flat_map>>> _resolvePending; + base::flat_map< + PeerId, + base::flat_map>>> _resolveSent; + + std::unordered_map< + not_null, + base::flat_set>> _dependentMessages; + + std::unordered_map _all; + std::vector _sources[kStorySourcesListCount]; + rpl::event_stream<> _sourcesChanged[kStorySourcesListCount]; + bool _sourcesLoaded[kStorySourcesListCount] = { false }; + QString _sourcesStates[kStorySourcesListCount]; + Folder *_folderForHidden = nullptr; + + mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 }; + + rpl::event_stream _sourceChanged; + rpl::event_stream _itemsChanged; + + StoriesIds _archive; + int _archiveTotal = -1; + StoryId _archiveLastId = 0; + bool _archiveLoaded = false; + rpl::event_stream<> _archiveChanged; + mtpRequestId _archiveRequestId = 0; + + std::unordered_map _saved; + rpl::event_stream _savedChanged; + + base::flat_set _markReadPending; + base::Timer _markReadTimer; + base::flat_set _markReadRequests; + base::flat_map< + not_null, + std::vector>> _requestingUserStories; + + base::flat_map> _incrementViewsPending; + base::Timer _incrementViewsTimer; + base::flat_set _incrementViewsRequests; + + StoryId _viewsStoryId = 0; + std::optional _viewsOffset; + Fn)> _viewsDone; + mtpRequestId _viewsRequestId = 0; + + base::flat_set _preloaded; + std::vector _toPreloadSources[kStorySourcesListCount]; + std::vector _toPreloadViewer; + std::unique_ptr _preloading; + int _preloadingHiddenSourcesCounter = 0; + int _preloadingMainSourcesCounter = 0; + + base::flat_map _readTill; + base::flat_set _pendingReadTillItems; + base::flat_map, StoryId> _pendingUserStateMaxId; + mtpRequestId _readTillsRequestId = 0; + bool _readTillReceived = false; + + base::flat_map, PollingSettings> _pollingSettings; + base::flat_set> _pollingViews; + base::Timer _pollingTimer; + base::Timer _pollingViewsTimer; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp new file mode 100644 index 000000000..7506abea5 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -0,0 +1,161 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_stories_ids.h" + +#include "data/data_changes.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "main/main_session.h" + +namespace Data { + +rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto stories = &peer->owner().stories(); + if (!stories->savedCountKnown(peer->id)) { + return; + } + + const auto saved = stories->saved(peer->id); + Assert(saved != nullptr); + const auto count = stories->savedCount(peer->id); + const auto around = saved->list.lower_bound(aroundId); + const auto hasBefore = int(around - begin(saved->list)); + const auto hasAfter = int(end(saved->list) - around); + if (hasAfter < limit) { + stories->savedLoadMore(peer->id); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = base::flat_set{ + std::make_reverse_iterator(around + takeAfter), + std::make_reverse_iterator(around - takeBefore) + }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto stories = &peer->owner().stories(); + stories->savedChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next(schedule, lifetime); + + if (!stories->savedCountKnown(peer->id)) { + stories->savedLoadMore(peer->id); + } + + push(); + + return lifetime; + }; +} + +rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto stories = &session->data().stories(); + if (!stories->archiveCountKnown()) { + return; + } + + const auto &archive = stories->archive(); + const auto count = stories->archiveCount(); + const auto i = archive.list.lower_bound(aroundId); + const auto hasBefore = int(i - begin(archive.list)); + const auto hasAfter = int(end(archive.list) - i); + if (hasAfter < limit) { + stories->archiveLoadMore(); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = base::flat_set{ + std::make_reverse_iterator(i + takeAfter), + std::make_reverse_iterator(i - takeBefore) + }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto stories = &session->data().stories(); + stories->archiveChanged( + ) | rpl::start_with_next(schedule, lifetime); + + if (!stories->archiveCountKnown()) { + stories->archiveLoadMore(); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h new file mode 100644 index 000000000..26f827f96 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -0,0 +1,32 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_abstract_sparse_ids.h" + +class PeerData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +using StoriesIdsSlice = AbstractSparseIds>; + +[[nodiscard]] rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit); + +[[nodiscard]] rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp new file mode 100644 index 000000000..2a139de87 --- /dev/null +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -0,0 +1,591 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_story.h" + +#include "base/unixtime.h" +#include "api/api_text_entities.h" +#include "data/data_document.h" +#include "data/data_changes.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_thread.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "media/streaming/media_streaming_reader.h" +#include "storage/download_manager_mtproto.h" +#include "ui/text/text_utilities.h" + +namespace Data { +namespace { + +using UpdateFlag = StoryUpdate::Flag; + +} // namespace + +class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask { +public: + LoadTask( + FullStoryId id, + not_null document, + Fn done); + ~LoadTask(); + +private: + bool readyToRequest() const override; + int64 takeNextRequestOffset() override; + bool feedPart(int64 offset, const QByteArray &bytes) override; + void cancelOnFail() override; + bool setWebFileSizeHook(int64 size) override; + + base::flat_map _parts; + Fn _done; + base::flat_set _requestedOffsets; + int64 _full = 0; + int _nextRequestOffset = 0; + bool _finished = false; + bool _failed = false; + +}; + +StoryPreload::LoadTask::LoadTask( + FullStoryId id, + not_null document, + Fn done) +: DownloadMtprotoTask( + &document->session().downloader(), + document->videoPreloadLocation(), + FileOriginStory(id.peer, id.story)) +, _done(std::move(done)) +, _full(document->size) { + const auto prefix = document->videoPreloadPrefix(); + Assert(prefix > 0 && prefix <= document->size); + const auto part = Storage::kDownloadPartSize; + const auto parts = (prefix + part - 1) / part; + for (auto i = 0; i != parts; ++i) { + _parts.emplace(i * part, QByteArray()); + } + addToQueue(); +} + +StoryPreload::LoadTask::~LoadTask() { + if (!_finished && !_failed) { + cancelAllRequests(); + } +} + +bool StoryPreload::LoadTask::readyToRequest() const { + const auto part = Storage::kDownloadPartSize; + return !_failed && (_nextRequestOffset < _parts.size() * part); +} + +int64 StoryPreload::LoadTask::takeNextRequestOffset() { + Expects(readyToRequest()); + + _requestedOffsets.emplace(_nextRequestOffset); + _nextRequestOffset += Storage::kDownloadPartSize; + return _requestedOffsets.back(); +} + +bool StoryPreload::LoadTask::feedPart( + int64 offset, + const QByteArray &bytes) { + Expects(offset < _parts.size() * Storage::kDownloadPartSize); + Expects(_requestedOffsets.contains(int(offset))); + Expects(bytes.size() <= Storage::kDownloadPartSize); + + const auto part = Storage::kDownloadPartSize; + _requestedOffsets.remove(int(offset)); + _parts[offset] = bytes; + if ((_nextRequestOffset + part >= _parts.size() * part) + && _requestedOffsets.empty()) { + _finished = true; + removeFromQueue(); + auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts); + if (result.size() == _full) { + // Make sure it is parsed as a complex map. + result.push_back(char(0)); + } + _done(result); + } + return true; +} + +void StoryPreload::LoadTask::cancelOnFail() { + _failed = true; + cancelAllRequests(); + _done({}); +} + +bool StoryPreload::LoadTask::setWebFileSizeHook(int64 size) { + _failed = true; + cancelAllRequests(); + _done({}); + return false; +} + +Story::Story( + StoryId id, + not_null peer, + StoryMedia media, + const MTPDstoryItem &data, + TimeId now) +: _id(id) +, _peer(peer) +, _date(data.vdate().v) +, _expires(data.vexpire_date().v) { + applyFields(std::move(media), data, now, true); +} + +Session &Story::owner() const { + return _peer->owner(); +} + +Main::Session &Story::session() const { + return _peer->session(); +} + +not_null Story::peer() const { + return _peer; +} + +StoryId Story::id() const { + return _id; +} + +bool Story::mine() const { + return _peer->isSelf(); +} + +StoryIdDates Story::idDates() const { + return { _id, _date, _expires }; +} + +FullStoryId Story::fullId() const { + return { _peer->id, _id }; +} + +TimeId Story::date() const { + return _date; +} + +TimeId Story::expires() const { + return _expires; +} + +bool Story::expired(TimeId now) const { + return _expires <= (now ? now : base::unixtime::now()); +} + +bool Story::unsupported() const { + return v::is_null(_media.data); +} + +const StoryMedia &Story::media() const { + return _media; +} + +PhotoData *Story::photo() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +DocumentData *Story::document() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +bool Story::hasReplyPreview() const { + return v::match(_media.data, [](not_null photo) { + return !photo->isNull(); + }, [](not_null document) { + return document->hasThumbnail(); + }, [](v::null_t) { + return false; + }); +} + +Image *Story::replyPreview() const { + return v::match(_media.data, [&](not_null photo) { + return photo->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }, [&](not_null document) { + return document->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }, [](v::null_t) { + return (Image*)nullptr; + }); +} + +TextWithEntities Story::inReplyText() const { + const auto type = tr::lng_in_dlg_story(tr::now); + return _caption.text.isEmpty() + ? Ui::Text::PlainLink(type) + : tr::lng_dialogs_text_media( + tr::now, + lt_media_part, + tr::lng_dialogs_text_media_wrapped( + tr::now, + lt_media, + Ui::Text::PlainLink(type), + Ui::Text::WithEntities), + lt_caption, + _caption, + Ui::Text::WithEntities); +} + +void Story::setPinned(bool pinned) { + _pinned = pinned; +} + +bool Story::pinned() const { + return _pinned; +} + +StoryPrivacy Story::privacy() const { + return _privacyPublic + ? StoryPrivacy::Public + : _privacyCloseFriends + ? StoryPrivacy::CloseFriends + : _privacyContacts + ? StoryPrivacy::Contacts + : _privacySelectedContacts + ? StoryPrivacy::SelectedContacts + : StoryPrivacy::Other; +} + +bool Story::forbidsForward() const { + return _noForwards; +} + +bool Story::edited() const { + return _edited; +} + +bool Story::canDownload() const { + return /*!forbidsForward() || */_peer->isSelf(); +} + +bool Story::canShare() const { + return _privacyPublic && !forbidsForward() && (pinned() || !expired()); +} + +bool Story::canDelete() const { + return _peer->isSelf(); +} + +bool Story::canReport() const { + return !_peer->isSelf(); +} + +bool Story::hasDirectLink() const { + if (!_privacyPublic || (!_pinned && expired())) { + return false; + } + const auto user = _peer->asUser(); + return user && !user->username().isEmpty(); +} + +std::optional Story::errorTextForForward( + not_null to) const { + const auto peer = to->peer(); + const auto holdsPhoto = v::is>(_media.data); + const auto first = holdsPhoto + ? ChatRestriction::SendPhotos + : ChatRestriction::SendVideos; + const auto second = holdsPhoto + ? ChatRestriction::SendVideos + : ChatRestriction::SendPhotos; + if (const auto error = Data::RestrictionError(peer, first)) { + return *error; + } else if (const auto error = Data::RestrictionError(peer, second)) { + return *error; + } else if (!Data::CanSend(to, first, false) + || !Data::CanSend(to, second, false)) { + return tr::lng_forward_cant(tr::now); + } + return {}; +} + +void Story::setCaption(TextWithEntities &&caption) { + _caption = std::move(caption); +} + +const TextWithEntities &Story::caption() const { + static const auto empty = TextWithEntities(); + return unsupported() ? empty : _caption; +} + +const std::vector> &Story::recentViewers() const { + return _recentViewers; +} + +const std::vector &Story::viewsList() const { + return _viewsList; +} + +int Story::views() const { + return _views; +} + +void Story::applyViewsSlice( + const std::optional &offset, + const std::vector &slice, + int total) { + const auto changed = (_views != total); + _views = total; + if (!offset) { + const auto i = _viewsList.empty() + ? end(slice) + : ranges::find(slice, _viewsList.front()); + const auto merge = (i != end(slice)) + && !ranges::contains(slice, _viewsList.back()); + if (merge) { + _viewsList.insert(begin(_viewsList), begin(slice), i); + } else { + _viewsList = slice; + } + } else if (!slice.empty()) { + const auto i = ranges::find(_viewsList, *offset); + const auto merge = (i != end(_viewsList)) + && !ranges::contains(_viewsList, slice.back()); + if (merge) { + const auto after = i + 1; + if (after == end(_viewsList)) { + _viewsList.insert(after, begin(slice), end(slice)); + } else { + const auto j = ranges::find(slice, _viewsList.back()); + if (j != end(slice)) { + _viewsList.insert(end(_viewsList), j + 1, end(slice)); + } + } + } + } + const auto known = int(_viewsList.size()); + if (known >= _recentViewers.size()) { + const auto take = std::min(known, kRecentViewersMax); + auto viewers = _viewsList + | ranges::views::take(take) + | ranges::views::transform(&StoryView::peer) + | ranges::to_vector; + if (_recentViewers != viewers) { + _recentViewers = std::move(viewers); + if (!changed) { + // Count not changed, but list of recent viewers changed. + _peer->session().changes().storyUpdated( + this, + UpdateFlag::ViewsAdded); + } + } + } + if (changed) { + _peer->session().changes().storyUpdated( + this, + UpdateFlag::ViewsAdded); + } +} + +void Story::applyChanges( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now) { + applyFields(std::move(media), data, now, false); +} + +void Story::applyFields( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now, + bool initial) { + _lastUpdateTime = now; + + const auto pinned = data.is_pinned(); + const auto edited = data.is_edited(); + const auto privacy = data.is_public() + ? StoryPrivacy::Public + : data.is_close_friends() + ? StoryPrivacy::CloseFriends + : data.is_contacts() + ? StoryPrivacy::Contacts + : data.is_selected_contacts() + ? StoryPrivacy::SelectedContacts + : StoryPrivacy::Other; + const auto noForwards = data.is_noforwards(); + auto caption = TextWithEntities{ + data.vcaption().value_or_empty(), + Api::EntitiesFromMTP( + &owner().session(), + data.ventities().value_or_empty()), + }; + auto views = _views; + auto viewers = std::vector>(); + if (!data.is_min()) { + if (const auto info = data.vviews()) { + views = info->data().vviews_count().v; + if (const auto list = info->data().vrecent_viewers()) { + viewers.reserve(list->v.size()); + auto &owner = _peer->owner(); + auto &&cut = list->v + | ranges::views::take(kRecentViewersMax); + for (const auto &id : cut) { + viewers.push_back(owner.peer(peerFromUser(id))); + } + } + } + } + + const auto pinnedChanged = (_pinned != pinned); + const auto editedChanged = (_edited != edited); + const auto mediaChanged = (_media != media); + const auto captionChanged = (_caption != caption); + const auto viewsChanged = (_views != views) + || (_recentViewers != viewers); + + _privacyPublic = (privacy == StoryPrivacy::Public); + _privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends); + _privacyContacts = (privacy == StoryPrivacy::Contacts); + _privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts); + _noForwards = noForwards; + _edited = edited; + _pinned = pinned; + _noForwards = noForwards; + if (viewsChanged) { + _views = views; + _recentViewers = std::move(viewers); + } + if (mediaChanged) { + _media = std::move(media); + } + if (captionChanged) { + _caption = std::move(caption); + } + + const auto changed = (editedChanged || captionChanged || mediaChanged); + if (!initial && (changed || viewsChanged)) { + _peer->session().changes().storyUpdated(this, UpdateFlag() + | (changed ? UpdateFlag::Edited : UpdateFlag()) + | (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag())); + } + if (!initial && (captionChanged || mediaChanged)) { + if (const auto item = _peer->owner().stories().lookupItem(this)) { + item->applyChanges(this); + } + _peer->owner().refreshStoryItemViews(fullId()); + } + if (pinnedChanged) { + _peer->owner().stories().savedStateChanged(this); + } +} + +TimeId Story::lastUpdateTime() const { + return _lastUpdateTime; +} + +StoryPreload::StoryPreload(not_null story, Fn done) +: _story(story) +, _done(std::move(done)) { + start(); +} + +StoryPreload::~StoryPreload() { + if (_photo) { + base::take(_photo)->owner()->cancel(); + } +} + +FullStoryId StoryPreload::id() const { + return _story->fullId(); +} + +not_null StoryPreload::story() const { + return _story; +} + +void StoryPreload::start() { + const auto origin = FileOriginStory( + _story->peer()->id, + _story->id()); + if (const auto photo = _story->photo()) { + _photo = photo->createMediaView(); + if (_photo->loaded()) { + callDone(); + } else { + _photo->automaticLoad(origin, _story->peer()); + photo->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return _photo->loaded(); + }) | rpl::start_with_next([=] { callDone(); }, _lifetime); + } + } else if (const auto video = _story->document()) { + if (video->canBeStreamed(nullptr) && video->videoPreloadPrefix()) { + const auto key = video->bigFileBaseCacheKey(); + if (key) { + const auto weak = base::make_weak(this); + video->owner().cacheBigFile().get(key, [weak]( + const QByteArray &result) { + if (!result.isEmpty()) { + crl::on_main([weak] { + if (const auto strong = weak.get()) { + strong->callDone(); + } + }); + } else { + crl::on_main([weak] { + if (const auto strong = weak.get()) { + strong->load(); + } + }); + } + }); + } else { + callDone(); + } + } else { + callDone(); + } + } else { + callDone(); + } +} + +void StoryPreload::load() { + Expects(_story->document() != nullptr); + + const auto video = _story->document(); + const auto valid = video->videoPreloadLocation().valid(); + const auto prefix = video->videoPreloadPrefix(); + const auto key = video->bigFileBaseCacheKey(); + if (!valid || prefix <= 0 || prefix > video->size || !key) { + callDone(); + return; + } + _task = std::make_unique(id(), video, [=](QByteArray data) { + if (!data.isEmpty()) { + _story->owner().cacheBigFile().putIfEmpty( + key, + Storage::Cache::Database::TaggedValue(std::move(data), 0)); + } + callDone(); + }); +} + +void StoryPreload::callDone() { + if (const auto onstack = _done) { + onstack(); + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h new file mode 100644 index 000000000..8ea17b7b7 --- /dev/null +++ b/Telegram/SourceFiles/data/data_story.h @@ -0,0 +1,181 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/weak_ptr.h" + +class Image; +class PhotoData; +class DocumentData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +class Thread; +class PhotoMedia; + +enum class StoryPrivacy : uchar { + Public, + CloseFriends, + Contacts, + SelectedContacts, + Other, +}; + +struct StoryIdDates { + StoryId id = 0; + TimeId date = 0; + TimeId expires = 0; + + [[nodiscard]] bool valid() const { + return id != 0; + } + explicit operator bool() const { + return valid(); + } + + friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default; + friend inline bool operator==(StoryIdDates, StoryIdDates) = default; +}; + +struct StoryMedia { + std::variant< + v::null_t, + not_null, + not_null> data; + + friend inline bool operator==(StoryMedia, StoryMedia) = default; +}; + +struct StoryView { + not_null peer; + TimeId date = 0; + + friend inline bool operator==(StoryView, StoryView) = default; +}; + +class Story final { +public: + Story( + StoryId id, + not_null peer, + StoryMedia media, + const MTPDstoryItem &data, + TimeId now); + + static constexpr int kRecentViewersMax = 3; + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null peer() const; + + [[nodiscard]] StoryId id() const; + [[nodiscard]] bool mine() const; + [[nodiscard]] StoryIdDates idDates() const; + [[nodiscard]] FullStoryId fullId() const; + [[nodiscard]] TimeId date() const; + [[nodiscard]] TimeId expires() const; + [[nodiscard]] bool unsupported() const; + [[nodiscard]] bool expired(TimeId now = 0) const; + [[nodiscard]] const StoryMedia &media() const; + [[nodiscard]] PhotoData *photo() const; + [[nodiscard]] DocumentData *document() const; + + [[nodiscard]] bool hasReplyPreview() const; + [[nodiscard]] Image *replyPreview() const; + [[nodiscard]] TextWithEntities inReplyText() const; + + void setPinned(bool pinned); + [[nodiscard]] bool pinned() const; + [[nodiscard]] StoryPrivacy privacy() const; + [[nodiscard]] bool forbidsForward() const; + [[nodiscard]] bool edited() const; + + [[nodiscard]] bool canDownload() const; + [[nodiscard]] bool canShare() const; + [[nodiscard]] bool canDelete() const; + [[nodiscard]] bool canReport() const; + + [[nodiscard]] bool hasDirectLink() const; + [[nodiscard]] std::optional errorTextForForward( + not_null to) const; + + void setCaption(TextWithEntities &&caption); + [[nodiscard]] const TextWithEntities &caption() const; + + [[nodiscard]] auto recentViewers() const + -> const std::vector> &; + [[nodiscard]] const std::vector &viewsList() const; + [[nodiscard]] int views() const; + void applyViewsSlice( + const std::optional &offset, + const std::vector &slice, + int total); + + void applyChanges( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now); + [[nodiscard]] TimeId lastUpdateTime() const; + +private: + void applyFields( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now, + bool initial); + + const StoryId _id = 0; + const not_null _peer; + StoryMedia _media; + TextWithEntities _caption; + std::vector> _recentViewers; + std::vector _viewsList; + int _views = 0; + const TimeId _date = 0; + const TimeId _expires = 0; + TimeId _lastUpdateTime = 0; + bool _pinned : 1 = false; + bool _privacyPublic : 1 = false; + bool _privacyCloseFriends : 1 = false; + bool _privacyContacts : 1 = false; + bool _privacySelectedContacts : 1 = false; + bool _noForwards : 1 = false; + bool _edited : 1 = false; + +}; + +class StoryPreload final : public base::has_weak_ptr { +public: + StoryPreload(not_null story, Fn done); + ~StoryPreload(); + + [[nodiscard]] FullStoryId id() const; + [[nodiscard]] not_null story() const; + +private: + class LoadTask; + + void start(); + void load(); + void callDone(); + + const not_null _story; + Fn _done; + + std::shared_ptr _photo; + std::unique_ptr _task; + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index e07df91e3..a296e6cf4 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -135,6 +135,7 @@ using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; + constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); struct PreparedPhotoThumb { @@ -300,6 +301,8 @@ enum class MessageFlag : uint64 { // Fake message with bot cover and information. FakeBotAbout = (1ULL << 36), + + StoryItem = (1ULL << 37), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index a5c6de084..09c66a922 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -14,10 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_peer_bot_command.h" #include "data/data_photo.h" +#include "data/data_stories.h" #include "data/data_emoji_statuses.h" #include "data/data_user_names.h" #include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" +#include "history/history.h" #include "api/api_peer_photo.h" #include "apiwrap.h" #include "ui/text/text_options.h" @@ -112,6 +114,51 @@ void UserData::setCommonChatsCount(int count) { } } +bool UserData::hasPrivateForwardName() const { + return !_privateForwardName.isEmpty(); +} + +QString UserData::privateForwardName() const { + return _privateForwardName; +} + +void UserData::setPrivateForwardName(const QString &name) { + _privateForwardName = name; +} + +bool UserData::hasActiveStories() const { + return flags() & UserDataFlag::HasActiveStories; +} + +bool UserData::hasUnreadStories() const { + return flags() & UserDataFlag::HasUnreadStories; +} + +void UserData::setStoriesState(StoriesState state) { + Expects(state != StoriesState::Unknown); + + const auto was = flags(); + using Flag = UserDataFlag; + switch (state) { + case StoriesState::None: + _flags.remove(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + case StoriesState::HasRead: + _flags.set( + (flags() & ~Flag::HasUnreadStories) | Flag::HasActiveStories); + break; + case StoriesState::HasUnread: + _flags.add(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + } + if (flags() != was) { + if (const auto history = owner().historyLoaded(this)) { + history->updateChatListEntryPostponed(); + } + session().changes().peerUpdated(this, UpdateFlag::StoriesState); + } +} + void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) { bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty(); @@ -321,6 +368,10 @@ bool UserData::hasPersonalPhoto() const { return (flags() & UserDataFlag::PersonalPhoto); } +bool UserData::hasStoriesHidden() const { + return (flags() & UserDataFlag::StoriesHidden); +} + bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } @@ -444,6 +495,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->checkFolder(update.vfolder_id().value_or_empty()); user->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); user->setTranslationDisabled(update.is_translations_disabled()); + user->setPrivateForwardName( + update.vprivate_forward_name().value_or_empty()); if (const auto info = user->botInfo.get()) { const auto group = update.vbot_group_admin_rights() @@ -470,6 +523,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setWallPaper({}); } + user->owner().stories().apply(user, update.vstories()); + user->fullUpdated(); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index cc32613f9..ad6d0b7ae 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -62,6 +62,9 @@ enum class UserDataFlag { CanReceiveGifts = (1 << 15), VoiceMessagesForbidden = (1 << 16), PersonalPhoto = (1 << 17), + StoriesHidden = (1 << 18), + HasActiveStories = (1 << 19), + HasUnreadStories = (1 << 20), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -119,6 +122,7 @@ public: [[nodiscard]] bool isInaccessible() const; [[nodiscard]] bool applyMinPhoto() const; [[nodiscard]] bool hasPersonalPhoto() const; + [[nodiscard]] bool hasStoriesHidden() const; [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; @@ -168,6 +172,20 @@ public: int commonChatsCount() const; void setCommonChatsCount(int count); + [[nodiscard]] bool hasPrivateForwardName() const; + [[nodiscard]] QString privateForwardName() const; + void setPrivateForwardName(const QString &name); + + enum class StoriesState { + Unknown, + None, + HasRead, + HasUnread, + }; + [[nodiscard]] bool hasActiveStories() const; + [[nodiscard]] bool hasUnreadStories() const; + void setStoriesState(StoriesState state); + private: auto unavailableReasons() const -> const std::vector & override; @@ -178,6 +196,7 @@ private: std::vector _unavailableReasons; QString _phone; + QString _privateForwardName; ContactStatus _contactStatus = ContactStatus::Unknown; CallsStatus _callsStatus = CallsStatus::Unknown; int _commonChatsCount = 0; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 57b31d0f6..4a7b036e3 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -152,6 +152,8 @@ WebPageType ParseWebPageType( return WebPageType::WallPaper; } else if (type == u"telegram_theme"_q) { return WebPageType::Theme; + } else if (type == u"telegram_story"_q) { + return WebPageType::Story; } else if (type == u"telegram_channel"_q) { return WebPageType::Channel; } else if (type == u"telegram_channel_request"_q) { @@ -214,6 +216,7 @@ bool WebPageData::applyChanges( const QString &newSiteName, const QString &newTitle, const TextWithEntities &newDescription, + FullStoryId newStoryId, PhotoData *newPhoto, DocumentData *newDocument, WebPageCollage &&newCollage, @@ -254,6 +257,7 @@ bool WebPageData::applyChanges( && siteName == resultSiteName && title == resultTitle && description.text == newDescription.text + && storyId == newStoryId && photo == newPhoto && document == newDocument && collage.items == newCollage.items @@ -271,6 +275,7 @@ bool WebPageData::applyChanges( siteName = resultSiteName; title = resultTitle; description = newDescription; + storyId = newStoryId; photo = newPhoto; document = newDocument; collage = std::move(newCollage); diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 8481a7b3b..83e7bde80 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -35,6 +35,7 @@ enum class WebPageType { WallPaper, Theme, + Story, Article, ArticleWithIV, @@ -70,6 +71,7 @@ struct WebPageData { const QString &newSiteName, const QString &newTitle, const TextWithEntities &newDescription, + FullStoryId newStoryId, PhotoData *newPhoto, DocumentData *newDocument, WebPageCollage &&newCollage, @@ -89,6 +91,7 @@ struct WebPageData { QString siteName; QString title; TextWithEntities description; + FullStoryId storyId; int duration = 0; QString author; PhotoData *photo = nullptr; diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp index cee0afe8a..1675d1e92 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp @@ -174,8 +174,13 @@ void NotifySettings::update( not_null thread, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (thread->notify().change(muteForSeconds, silentPosts, sound)) { + std::optional sound, + std::optional storiesMuted) { + if (thread->notify().change( + muteForSeconds, + silentPosts, + sound, + storiesMuted)) { updateLocal(thread); thread->session().api().updateNotifySettingsDelayed(thread); } @@ -189,6 +194,11 @@ void NotifySettings::resetToDefault(not_null thread) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound()); if (thread->notify().change(empty)) { updateLocal(thread); @@ -200,8 +210,13 @@ void NotifySettings::update( not_null peer, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (peer->notify().change(muteForSeconds, silentPosts, sound)) { + std::optional sound, + std::optional storiesMuted) { + if (peer->notify().change( + muteForSeconds, + silentPosts, + sound, + storiesMuted)) { updateLocal(peer); peer->session().api().updateNotifySettingsDelayed(peer); } @@ -215,6 +230,11 @@ void NotifySettings::resetToDefault(not_null peer) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound()); if (peer->notify().change(empty)) { updateLocal(peer); @@ -262,9 +282,10 @@ void NotifySettings::defaultUpdate( DefaultNotify type, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { + std::optional sound, + std::optional storiesMuted) { auto &settings = defaultValue(type).settings; - if (settings.change(muteForSeconds, silentPosts, sound)) { + if (settings.change(muteForSeconds, silentPosts, sound, storiesMuted)) { updateLocal(type); _owner->session().api().updateNotifySettingsDelayed(type); } diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.h b/Telegram/SourceFiles/data/notify/data_notify_settings.h index be41ce8dc..6e51b87ac 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.h @@ -57,13 +57,15 @@ public: not_null thread, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); void resetToDefault(not_null thread); void update( not_null peer, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); void resetToDefault(not_null peer); void forumParentMuteUpdated(not_null forum); @@ -84,7 +86,8 @@ public: DefaultNotify type, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); [[nodiscard]] bool isMuted(not_null thread) const; [[nodiscard]] NotifySound sound( diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp index 6d5735624..4b756a611 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp @@ -18,6 +18,9 @@ namespace { MTPBool(), MTPBool(), MTPint(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), MTPNotificationSound()); } @@ -73,7 +76,8 @@ public: bool change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound); + std::optional sound, + std::optional storiesMuted); std::optional muteUntil() const; std::optional silentPosts() const; @@ -85,12 +89,14 @@ private: std::optional mute, std::optional sound, std::optional showPreviews, - std::optional silentPosts); + std::optional silentPosts, + std::optional storiesMuted); std::optional _mute; std::optional _sound; std::optional _silent; std::optional _showPreviews; + std::optional _storiesMuted; }; @@ -104,19 +110,24 @@ bool NotifyPeerSettingsValue::change(const MTPDpeerNotifySettings &data) { const auto sound = data.vother_sound(); const auto showPreviews = data.vshow_previews(); const auto silent = data.vsilent(); + const auto storiesMuted = data.vstories_muted(); return change( mute ? std::make_optional(mute->v) : std::nullopt, sound ? std::make_optional(ParseSound(*sound)) : std::nullopt, (showPreviews ? std::make_optional(mtpIsTrue(*showPreviews)) : std::nullopt), - silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt); + silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt, + (storiesMuted + ? std::make_optional(mtpIsTrue(*storiesMuted)) + : std::nullopt)); } bool NotifyPeerSettingsValue::change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { + std::optional sound, + std::optional storiesMuted) { const auto newMute = muteForSeconds ? base::make_optional(muteForSeconds.until()) : _mute; @@ -126,28 +137,35 @@ bool NotifyPeerSettingsValue::change( const auto newSound = sound ? base::make_optional(*sound) : _sound; + const auto newStoriesMuted = storiesMuted + ? base::make_optional(*storiesMuted) + : _storiesMuted; return change( newMute, newSound, _showPreviews, - newSilentPosts); + newSilentPosts, + newStoriesMuted); } bool NotifyPeerSettingsValue::change( std::optional mute, std::optional sound, std::optional showPreviews, - std::optional silentPosts) { + std::optional silentPosts, + std::optional storiesMuted) { if (_mute == mute && _sound == sound && _showPreviews == showPreviews - && _silent == silentPosts) { + && _silent == silentPosts + && _storiesMuted == storiesMuted) { return false; } _mute = mute; _sound = sound; _showPreviews = showPreviews; _silent = silentPosts; + _storiesMuted = storiesMuted; return true; } @@ -172,11 +190,15 @@ MTPinputPeerNotifySettings NotifyPeerSettingsValue::serialize() const { MTP_flags(flag(_mute, Flag::f_mute_until) | flag(_sound, Flag::f_sound) | flag(_silent, Flag::f_silent) - | flag(_showPreviews, Flag::f_show_previews)), - MTP_bool(_showPreviews ? *_showPreviews : true), - MTP_bool(_silent ? *_silent : false), - MTP_int(_mute ? *_mute : false), - SerializeSound(_sound)); + | flag(_showPreviews, Flag::f_show_previews) + | flag(_storiesMuted, Flag::f_stories_muted)), + MTP_bool(_showPreviews.value_or(true)), + MTP_bool(_silent.value_or(false)), + MTP_int(_mute.value_or(false)), + SerializeSound(_sound), + MTP_bool(_storiesMuted.value_or(false)), + MTP_bool(false), // stories_hide_sender + SerializeSound(std::nullopt)); // stories_sound } PeerNotifySettings::PeerNotifySettings() = default; @@ -203,16 +225,22 @@ bool PeerNotifySettings::change(const MTPPeerNotifySettings &settings) { bool PeerNotifySettings::change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (!muteForSeconds && !silentPosts && !sound) { + std::optional sound, + std::optional storiesMuted) { + if (!muteForSeconds && !silentPosts && !sound && !storiesMuted) { return false; } else if (_value) { - return _value->change(muteForSeconds, silentPosts, sound); + return _value->change( + muteForSeconds, + silentPosts, + sound, + storiesMuted); } using Flag = MTPDpeerNotifySettings::Flag; const auto flags = (muteForSeconds ? Flag::f_mute_until : Flag(0)) | (silentPosts ? Flag::f_silent : Flag(0)) - | (sound ? Flag::f_other_sound : Flag(0)); + | (sound ? Flag::f_other_sound : Flag(0)) + | (storiesMuted ? Flag::f_stories_muted : Flag(0)); return change(MTP_peerNotifySettings( MTP_flags(flags), MTPBool(), @@ -220,7 +248,12 @@ bool PeerNotifySettings::change( MTP_int(muteForSeconds.until()), MTPNotificationSound(), MTPNotificationSound(), - SerializeSound(sound))); + SerializeSound(sound), + storiesMuted ? MTP_bool(*storiesMuted) : MTPBool(), + MTPBool(), // stories_hide_sender + MTPNotificationSound(), + MTPNotificationSound(), + SerializeSound(std::nullopt))); // stories_sound } std::optional PeerNotifySettings::muteUntil() const { diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h index 3973d7820..76a8ecfd9 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h @@ -44,7 +44,8 @@ public: bool change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound); + std::optional sound, + std::optional storiesMuted); bool settingsUnknown() const; std::optional muteUntil() const; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 04565a741..ffedf1f50 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -344,7 +344,12 @@ void CustomEmojiLoader::check() { sizeOverride); }; auto put = [=, key = cacheKey(document)](QByteArray value) { - document->owner().cacheBigFile().put(key, std::move(value)); + const auto size = value.size(); + if (size <= Storage::Cache::Database::Settings().maxDataSize) { + document->owner().cacheBigFile().put(key, std::move(value)); + } else { + LOG(("Data Error: Cached emoji size too big: %1.").arg(size)); + } }; const auto type = document->sticker()->type; auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]() diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index f1ae4dbaa..c824b8219 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -218,22 +218,28 @@ dialogsMenuToggleUnreadMuted: icon { { "dialogs/dialogs_menu_unread_dot", dialogsMenuIconFg }, }; -dialogsLock: IconButton(dialogsMenuToggle) { - icon: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFg }}; - iconOver: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFgOver }}; +dialogsLock: IconButton { + width: 36px; + height: 38px; + + icon: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFg }}; + iconOver: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFgOver }}; + iconPosition: point(-1px, -1px); + + ripple: emptyRippleAnimation; } -dialogsUnlockIcon: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFg }}; -dialogsUnlockIconOver: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFgOver }}; +dialogsUnlockIcon: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFg }}; +dialogsUnlockIconOver: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFgOver }}; dialogsCalendar: IconButton { - width: 29px; - height: 32px; + width: 32px; + height: 35px; icon: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFg }}; iconOver: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFgOver }}; - iconPosition: point(0px, 5px); + iconPosition: point(1px, 6px); } dialogsSearchFrom: IconButton(dialogsCalendar) { - width: 26px; + width: 29px; icon: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFg }}; iconOver: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFgOver }}; } @@ -246,27 +252,28 @@ dialogsSearchForNarrowFilters: IconButton(dialogsMenuToggle) { dialogsFilter: InputField(defaultInputField) { textBg: filterInputInactiveBg; textBgActive: filterInputActiveBg; - textMargins: margins(12px, 7px, 30px, 3px); + textMargins: margins(12px, 8px, 30px, 5px); placeholderFg: placeholderFg; placeholderFgActive: placeholderFgActive; placeholderFgError: placeholderFgActive; - placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderMargins: margins(5px, 0px, 2px, 0px); placeholderScale: 0.; placeholderShift: -50px; placeholderFont: normalFont; borderFg: filterInputInactiveBg; - borderFgActive: filterInputBorderFg; + borderFgActive: windowBgRipple; borderFgError: activeLineFgError; - border: 2px; + border: 3px; borderActive: 2px; - borderRadius: roundRadiusSmall; + borderRadius: 18px; + borderDenominator: 2; font: normalFont; - heightMin: 32px; + heightMin: 35px; } dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) { icon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; @@ -276,12 +283,12 @@ dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) { rippleAreaSize: 34px; } dialogsCancelSearch: CrossButton { - width: 32px; - height: 32px; + width: 35px; + height: 35px; cross: CrossAnimation { - size: 32px; - skip: 10px; + size: 35px; + skip: 12px; stroke: 1.5; minScale: 0.3; } @@ -485,3 +492,73 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) { chooseTopicList: PeerList(defaultPeerList) { item: chooseTopicListItem; } + +DialogsStories { + left: pixels; + height: pixels; + photo: pixels; + photoLeft: pixels; + photoTop: pixels; + shift: pixels; + lineTwice: pixels; + lineReadTwice: pixels; + nameLeft: pixels; + nameRight: pixels; + nameTop: pixels; + nameStyle: TextStyle; +} +DialogsStoriesList { + small: DialogsStories; + full: DialogsStories; + bg: color; + readOpacity: double; + fullClickable: int; +} + +dialogsStories: DialogsStories { + left: 4px; + height: 35px; + photo: 21px; + photoTop: 4px; + photoLeft: 4px; + shift: 16px; + lineTwice: 3px; + lineReadTwice: 0px; + nameLeft: 11px; + nameRight: 10px; + nameTop: 3px; + nameStyle: semiboldTextStyle; +} + +dialogsStoriesFull: DialogsStories { + left: 4px; + height: 77px; + photo: 42px; + photoLeft: 10px; + photoTop: 9px; + lineTwice: 4px; + lineReadTwice: 2px; + nameLeft: 0px; + nameRight: 0px; + nameTop: 56px; + nameStyle: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px); + } +} + +dialogsStoriesList: DialogsStoriesList { + small: dialogsStories; + full: dialogsStoriesFull; + bg: dialogsBg; + readOpacity: 0.6; + fullClickable: 0; +} +dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) { + bg: transparent; + fullClickable: 1; +} +dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) { + readOpacity: 1.; +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index b763ca574..d0d01778b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_inner_widget.h" -#include "dialogs/dialogs_indexed_list.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" #include "dialogs/dialogs_search_from_controllers.h" #include "history/history.h" @@ -18,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/scroll_area.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/painter.h" @@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" #include "base/unixtime.h" @@ -317,6 +321,8 @@ InnerWidget::InnerWidget( switchToFilter(filterId); }, lifetime()); + session().data().stories().incrementPreloadingMainSources(); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -406,8 +412,13 @@ int InnerWidget::skipTopHeight() const { : 0; } +int InnerWidget::collapsedRowsOffset() const { + return 0; +} + int InnerWidget::dialogsOffset() const { - return _collapsedRows.size() * st::dialogsImportantBarHeight + return collapsedRowsOffset() + + (_collapsedRows.size() * st::dialogsImportantBarHeight) - skipTopHeight(); } @@ -569,6 +580,10 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .paused = videoPaused, .narrow = (fullWidth < st::columnMinimalWidthLeft / 2), }; + const auto fillGuard = gsl::finally([&] { + // We translate painter down, but it'll be cropped below rect. + p.fillRect(rect(), context.currentBg); + }); const auto paintRow = [&]( not_null row, bool selected, @@ -596,7 +611,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context); }; if (_state == WidgetState::Default) { - paintCollapsedRows(p, r); + const auto collapsedSkip = collapsedRowsOffset(); + p.translate(0, collapsedSkip); + paintCollapsedRows(p, r.translated(0, -collapsedSkip)); const auto &list = _shownList->all(); const auto shownBottom = _shownList->height() - skipTopHeight(); @@ -1215,6 +1232,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { } _mouseSelection = true; _lastMousePosition = globalPosition; + _lastRowLocalMouseX = local.x(); const auto w = width(); const auto mouseY = local.y(); @@ -2122,6 +2140,7 @@ FilterId InnerWidget::filterId() const { void InnerWidget::clearSelection() { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (isSelected()) { updateSelectedRow(); _collapsedSelected = -1; @@ -2255,7 +2274,7 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { if (_filter.isEmpty() && !_searchFromPeer) { clearFilter(); } else { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; _filterResults.clear(); _filterResultsGlobal.clear(); @@ -2339,6 +2358,7 @@ void InnerWidget::appendToFiltered(Key key) { } InnerWidget::~InnerWidget() { + session().data().stories().decrementPreloadingMainSources(); clearSearchResults(); } @@ -2725,7 +2745,7 @@ void InnerWidget::refresh(bool toTop) { resize(width(), h); if (toTop) { stopReorderPinned(); - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); preloadRowsData(); } _controller->setDialogsListDisplayForced( @@ -2809,6 +2829,7 @@ void InnerWidget::resizeEmptyLabel() { void InnerWidget::clearMouseSelection(bool clearSelection) { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (clearSelection) { if (_state == WidgetState::Default) { _collapsedSelected = -1; @@ -2915,10 +2936,10 @@ void InnerWidget::repaintSearchResult(int index) { void InnerWidget::clearFilter() { if (_state == WidgetState::Filtered || _searchInChat) { if (_searchInChat) { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; } else { - _state = WidgetState::Default; + setState(WidgetState::Default); } _hashtagResults.clear(); _filterResults.clear(); @@ -2930,6 +2951,10 @@ void InnerWidget::clearFilter() { } } +void InnerWidget::setState(WidgetState state) { + _state = state; +} + void InnerWidget::selectSkip(int32 direction) { clearMouseSelection(); if (_state == WidgetState::Default) { @@ -3196,7 +3221,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { filterId = 0; } if (_filterId == filterId) { - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); return; } saveChatsFilterScrollState(_filterId); @@ -3221,6 +3246,10 @@ void InnerWidget::switchToFilter(FilterId filterId) { } } +void InnerWidget::jumpToTop() { + _mustScrollTo.fire({ 0, -1 }); +} + void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { _chatsFilterScrollStates[filterId] = -y(); } @@ -3228,7 +3257,7 @@ void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { void InnerWidget::restoreChatsFilterScrollState(FilterId filterId) { const auto it = _chatsFilterScrollStates.find(filterId); if (it != end(_chatsFilterScrollStates)) { - _mustScrollTo.fire({ it->second, -1 }); + _mustScrollTo.fire({ std::max(it->second, 0), -1 }); } } @@ -3264,29 +3293,30 @@ ChosenRow InnerWidget::computeChosenRow() const { if (_state == WidgetState::Default) { if (_selected) { return { - _selected->key(), - Data::UnreadMessagePosition + .key = _selected->key(), + .message = Data::UnreadMessagePosition, }; } } else if (_state == WidgetState::Filtered) { if (base::in_range(_filteredSelected, 0, _filterResults.size())) { return { - _filterResults[_filteredSelected].key(), - Data::UnreadMessagePosition, - true + .key = _filterResults[_filteredSelected].key(), + .message = Data::UnreadMessagePosition, + .filteredRow = true, }; } else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) { + const auto peer = _peerSearchResults[_peerSearchSelected]->peer; return { - session().data().history(_peerSearchResults[_peerSearchSelected]->peer), - Data::UnreadMessagePosition + .key = session().data().history(peer), + .message = Data::UnreadMessagePosition }; } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { const auto result = _searchResults[_searchedSelected].get(); const auto topic = result->topic(); const auto item = result->item(); return { - (topic ? (Entry*)topic : (Entry*)item->history()), - item->position() + .key = (topic ? (Entry*)topic : (Entry*)item->history()), + .message = item->position() }; } } @@ -3301,10 +3331,13 @@ bool InnerWidget::chooseRow( } else if (chooseHashtag()) { return true; } - const auto modifyChosenRow = []( + const auto modifyChosenRow = [&]( ChosenRow row, Qt::KeyboardModifiers modifiers) { row.newWindow = (modifiers & Qt::ControlModifier); + row.userpicClick = (_lastRowLocalMouseX >= 0) + && (_lastRowLocalMouseX < _st->nameLeft) + && (width() > _narrowWidth); return row; }; auto chosen = modifyChosenRow(computeChosenRow(), modifiers); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 6b01e940f..608513c85 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -61,8 +61,9 @@ class IndexedList; struct ChosenRow { Key key; Data::MessagePosition message; - bool filteredRow = false; - bool newWindow = false; + bool userpicClick : 1 = false; + bool filteredRow : 1 = false; + bool newWindow : 1 = false; }; enum class SearchRequestType { @@ -219,6 +220,7 @@ private: void dialogRowReplaced(Row *oldRow, Row *newRow); + void setState(WidgetState state); void editOpenedFilter(); void repaintCollapsedFolderRow(not_null folder); void refreshWithCollapsedRows(bool toTop = false); @@ -274,6 +276,7 @@ private: int defaultRowTop(not_null row) const; void setupOnlineStatusCheck(); + void jumpToTop(); void updateRowCornerStatusShown(not_null history); void repaintDialogRowCornerStatus(not_null history); @@ -310,6 +313,7 @@ private: void refreshShownList(); [[nodiscard]] int skipTopHeight() const; + [[nodiscard]] int collapsedRowsOffset() const; [[nodiscard]] int dialogsOffset() const; [[nodiscard]] int shownHeight(int till = -1) const; [[nodiscard]] int fixedOnTopCount() const; @@ -398,6 +402,7 @@ private: FilterId _filterId = 0; bool _mouseSelection = false; std::optional _lastMousePosition; + int _lastRowLocalMouseX = -1; Qt::MouseButton _pressButton = Qt::LeftButton; Data::Folder *_openedFolder = nullptr; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index c5145c368..c937f10f2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" // CountAverageColor. #include "ui/color_contrast.h" +#include "ui/effects/outline_segments.h" #include "ui/effects/ripple_animation.h" #include "ui/image/image_prepare.h" #include "ui/text/format_values.h" @@ -21,7 +22,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_peer_values.h" +#include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" @@ -230,20 +233,11 @@ void BasicRow::paintRipple( void BasicRow::paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const { - PaintUserpic( - p, - peer, - videoUserpic, - _userpic, - context.st->padding.left(), - context.st->padding.top(), - context.width, - context.st->photoSize, - context.paused); + PaintUserpic(p, entry, peer, videoUserpic, _userpic, context); } Row::Row(Key key, int index, int top) : _id(key), _top(top), _index(index) { @@ -330,28 +324,72 @@ void Row::ensureCornerBadgeUserpic() const { void Row::PaintCornerBadgeFrame( not_null data, - not_null peer, + int framePadding, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, const Ui::PaintContext &context) { data->frame.fill(Qt::transparent); Painter q(&data->frame); + q.translate(framePadding, framePadding); + auto hq = std::optional(); + const auto photoSize = context.st->photoSize; + const auto storiesCount = data->storiesCount; + if (storiesCount) { + hq.emplace(q); + const auto line = st::dialogsStoriesFull.lineTwice / 2.; + const auto skip = line * 3 / 2.; + const auto scale = 1. - (2 * skip / photoSize); + const auto center = photoSize / 2.; + q.save(); + q.translate(center, center); + q.scale(scale, scale); + q.translate(-center, -center); + } + q.translate(-context.st->padding.left(), -context.st->padding.top()); PaintUserpic( q, + entry, peer, videoUserpic, view, - 0, - 0, - data->frame.width() / data->frame.devicePixelRatio(), - context.st->photoSize, - context.paused); + context); + q.translate(context.st->padding.left(), context.st->padding.top()); + if (storiesCount) { + q.restore(); + + const auto outline = QRectF(0, 0, photoSize, photoSize); + const auto storiesUnreadCount = data->storiesUnreadCount; + const auto storiesUnreadBrush = [&] { + if (context.active || !storiesUnreadCount) { + return st::dialogsUnreadBgMutedActive->b; + } + auto gradient = Ui::UnreadStoryOutlineGradient(outline); + return QBrush(gradient); + }(); + const auto storiesBrush = context.active + ? st::dialogsUnreadBgMutedActive->b + : st::dialogsUnreadBgMuted->b; + const auto storiesUnread = st::dialogsStoriesFull.lineTwice / 2.; + const auto storiesLine = st::dialogsStoriesFull.lineReadTwice / 2.; + auto segments = std::vector(); + segments.reserve(storiesCount); + const auto storiesReadCount = storiesCount - storiesUnreadCount; + for (auto i = 0; i != storiesReadCount; ++i) { + segments.push_back({ storiesBrush, storiesLine }); + } + for (auto i = 0; i != storiesUnreadCount; ++i) { + segments.push_back({ storiesUnreadBrush, storiesUnread }); + } + Ui::PaintOutlineSegments(q, outline, segments); + } const auto &manager = data->layersManager; - if (const auto p = manager.progressForLayer(kBottomLayer); p) { - const auto size = context.st->photoSize; - if (data->cacheTTL.isNull() && peer->messagesTTL()) { + if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) { + const auto size = photoSize; + if (data->cacheTTL.isNull() && peer && peer->messagesTTL()) { data->cacheTTL = CornerBadgeTTL(peer, view, size); } q.setOpacity(p); @@ -364,14 +402,17 @@ void Row::PaintCornerBadgeFrame( return; } - PainterHighQualityEnabler hq(q); + if (!hq) { + hq.emplace(q); + } q.setCompositionMode(QPainter::CompositionMode_Source); - const auto size = peer->isUser() + const auto online = peer && peer->isUser(); + const auto size = online ? st::dialogsOnlineBadgeSize : st::dialogsCallBadgeSize; const auto stroke = st::dialogsOnlineBadgeStroke; - const auto skip = peer->isUser() + const auto skip = online ? st::dialogsOnlineBadgeSkip : st::dialogsCallBadgeSkip; const auto shrink = (size / 2) * (1. - topLayerProgress); @@ -383,8 +424,8 @@ void Row::PaintCornerBadgeFrame( ? st::dialogsOnlineBadgeFgActive : st::dialogsOnlineBadgeFg); q.drawEllipse(QRectF( - context.st->photoSize - skip.x() - size, - context.st->photoSize - skip.y() - size, + photoSize - skip.x() - size, + photoSize - skip.y() - size, size, size ).marginsRemoved({ shrink, shrink, shrink, shrink })); @@ -392,46 +433,72 @@ void Row::PaintCornerBadgeFrame( void Row::paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const { - updateCornerBadgeShown(peer); + if (peer) { + updateCornerBadgeShown(peer); + } const auto cornerBadgeShown = !_cornerBadgeUserpic ? _cornerBadgeShown : !_cornerBadgeUserpic->layersManager.isDisplayedNone(); - if (!historyForCornerBadge || !cornerBadgeShown) { - BasicRow::paintUserpic( - p, - peer, - videoUserpic, - historyForCornerBadge, - context); - if (!historyForCornerBadge || !_cornerBadgeShown) { + const auto storiesUser = peer ? peer->asUser() : nullptr; + const auto storiesFolder = peer ? nullptr : _id.folder(); + const auto storiesHas = storiesUser + ? storiesUser->hasActiveStories() + : storiesFolder + ? storiesFolder->storiesCount() + : false; + if (!cornerBadgeShown && !storiesHas) { + BasicRow::paintUserpic(p, entry, peer, videoUserpic, context); + if (!peer || !_cornerBadgeShown) { _cornerBadgeUserpic = nullptr; } return; } ensureCornerBadgeUserpic(); const auto ratio = style::DevicePixelRatio(); - const auto added = std::max({ + const auto framePadding = std::max({ -st::dialogsCallBadgeSkip.x(), -st::dialogsCallBadgeSkip.y(), - 0 }); - const auto frameSide = (context.st->photoSize + added) - * style::DevicePixelRatio(); + st::lineWidth * 2 }); + const auto frameSide = (2 * framePadding + context.st->photoSize) + * ratio; const auto frameSize = QSize(frameSide, frameSide); + const auto storiesSource = (storiesHas && storiesUser) + ? storiesUser->owner().stories().source(storiesUser->id) + : nullptr; + const auto storiesCountReal = storiesSource + ? int(storiesSource->ids.size()) + : storiesFolder + ? storiesFolder->storiesCount() + : storiesHas + ? 1 + : 0; + const auto storiesUnreadCountReal = storiesSource + ? storiesSource->unreadCount() + : storiesFolder + ? storiesFolder->storiesUnreadCount() + : (storiesUser && storiesUser->hasUnreadStories()) + ? 1 + : 0; + const auto limit = Ui::kOutlineSegmentsMax; + const auto storiesCount = std::min(storiesCountReal, limit); + const auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit); if (_cornerBadgeUserpic->frame.size() != frameSize) { _cornerBadgeUserpic->frame = QImage( frameSize, QImage::Format_ARGB32_Premultiplied); _cornerBadgeUserpic->frame.setDevicePixelRatio(ratio); } - auto key = peer->userpicUniqueKey(userpicView()); - key.first += peer->messagesTTL(); + auto key = peer ? peer->userpicUniqueKey(userpicView()) : InMemoryKey(); + key.first += peer ? peer->messagesTTL() : 0; const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1; - const auto paletteVersion = style::PaletteVersion(); + const auto paletteVersionReal = style::PaletteVersion(); + const auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1)); + const auto active = context.active ? 1 : 0; const auto keyChanged = (_cornerBadgeUserpic->key != key) || (_cornerBadgeUserpic->paletteVersion != paletteVersion); if (keyChanged) { @@ -439,29 +506,36 @@ void Row::paintUserpic( } if (keyChanged || !_cornerBadgeUserpic->layersManager.isFinished() - || _cornerBadgeUserpic->active != context.active + || _cornerBadgeUserpic->active != active || _cornerBadgeUserpic->frameIndex != frameIndex + || _cornerBadgeUserpic->storiesCount != storiesCount + || _cornerBadgeUserpic->storiesUnreadCount != storiesUnreadCount || videoUserpic) { _cornerBadgeUserpic->key = key; _cornerBadgeUserpic->paletteVersion = paletteVersion; - _cornerBadgeUserpic->active = context.active; + _cornerBadgeUserpic->active = active; + _cornerBadgeUserpic->storiesCount = storiesCount; + _cornerBadgeUserpic->storiesUnreadCount = storiesUnreadCount; _cornerBadgeUserpic->frameIndex = frameIndex; _cornerBadgeUserpic->layersManager.markFrameShown(); PaintCornerBadgeFrame( _cornerBadgeUserpic.get(), + framePadding, + _id.entry(), peer, videoUserpic, userpicView(), context); } p.drawImage( - context.st->padding.left(), - context.st->padding.top(), + context.st->padding.left() - framePadding, + context.st->padding.top() - framePadding, _cornerBadgeUserpic->frame); - if (historyForCornerBadge->peer->isUser()) { + const auto history = _id.history(); + if (!history || history->peer->isUser()) { return; } - const auto actionPainter = historyForCornerBadge->sendActionPainter(); + const auto actionPainter = history->sendActionPainter(); const auto bg = context.active ? st::dialogsBgActive : st::dialogsBg; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index d79c2efdf..eecbd9c4f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -35,6 +35,7 @@ struct TopicJumpCache; namespace Dialogs { +class Entry; enum class SortMode; [[nodiscard]] QRect CornerBadgeTTLRect(int photoSize); @@ -46,9 +47,9 @@ public: virtual void paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const; void addRipple(QPoint origin, QSize size, Fn updateCallback); @@ -99,9 +100,9 @@ public: Fn updateCallback = nullptr) const; void paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const final override; [[nodiscard]] bool lookupIsInTopicJump(int x, int y) const; @@ -167,11 +168,13 @@ private: struct CornerBadgeUserpic { InMemoryKey key; CornerLayersManager layersManager; - int paletteVersion = 0; - int frameIndex = -1; - bool active = false; QImage frame; QImage cacheTTL; + int frameIndex = -1; + uint32 paletteVersion : 17 = 0; + uint32 storiesCount : 7 = 0; + uint32 storiesUnreadCount : 7 = 0; + uint32 active : 1 = 0; }; void setCornerBadgeShown( @@ -180,7 +183,9 @@ private: void ensureCornerBadgeUserpic() const; static void PaintCornerBadgeFrame( not_null data, - not_null peer, + int framePadding, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, const Ui::PaintContext &context); @@ -189,9 +194,9 @@ private: mutable std::unique_ptr _cornerBadgeUserpic; int _top = 0; int _height = 0; - int _index : 30 = 0; - int _cornerBadgeShown : 1 = 0; - int _topicJumpRipple : 1 = 0; + uint32 _index : 30 = 0; + uint32 _cornerBadgeShown : 1 = 0; + uint32 _topicJumpRipple : 1 = 0; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index dc8f3243e..1b696ae8d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_widget.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_key.h" @@ -19,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_group_call_bar.h" #include "boxes/peers/edit_peer_requests_box.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/elastic_scroll.h" #include "ui/widgets/input_fields.h" #include "ui/wrap/fade_wrap.h" #include "ui/effects/radial_animation.h" @@ -63,20 +66,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" +#include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" #include "info/info_memento.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_window.h" #include "base/qt/qt_common_adapters.h" #include +#include namespace Dialogs { namespace { constexpr auto kSearchPerPage = 50; +constexpr auto kStoriesExpandDuration = crl::time(200); } // namespace @@ -205,14 +212,26 @@ Widget::Widget( _searchControls, object_ptr(this, st::dialogsCalendar)) , _cancelSearch(_searchControls, st::dialogsCancelSearch) -, _lockUnlock(_searchControls, st::dialogsLock) +, _lockUnlock( + _searchControls, + object_ptr(this, st::dialogsLock)) , _scroll(this) , _scrollToTop(_scroll, st::dialogsToUp) +, _stories((_layout != Layout::Child) + ? std::make_unique( + this, + st::dialogsStoriesList, + _storiesContents.events() | rpl::flatten_latest()) + : nullptr) , _searchTimer([=] { searchMessages(); }) , _singleMessageSearch(&controller->session()) { const auto makeChildListShown = [](PeerId peerId, float64 shown) { return InnerWidget::ChildListShown{ peerId, shown }; }; + using OverscrollType = Ui::ElasticScroll::OverscrollType; + _scroll->setOverscrollTypes( + _stories ? OverscrollType::Virtual : OverscrollType::Real, + OverscrollType::Real); _inner = _scroll->setOwnedWidget(object_ptr( this, controller, @@ -220,6 +239,8 @@ Widget::Widget( _childListPeerId.value(), _childListShown.value(), makeChildListShown))); + _scrollToTop->raise(); + _lockUnlock->toggle(false, anim::type::instant); _inner->updated( ) | rpl::start_with_next([=] { @@ -352,14 +373,20 @@ Widget::Widget( ) | rpl::start_with_next([=] { updateLockUnlockVisibility(); }, lifetime()); - _lockUnlock->setClickedCallback([this] { - _lockUnlock->setIconOverride(&st::dialogsUnlockIcon, &st::dialogsUnlockIconOver); + const auto lockUnlock = _lockUnlock->entity(); + lockUnlock->setClickedCallback([=] { + lockUnlock->setIconOverride( + &st::dialogsUnlockIcon, + &st::dialogsUnlockIconOver); Core::App().maybeLockByPasscode(); - _lockUnlock->setIconOverride(nullptr); + lockUnlock->setIconOverride(nullptr); }); setupMainMenuToggle(); setupShortcuts(); + if (_stories) { + setupStories(); + } _searchForNarrowFilters->setClickedCallback([=] { _filter->setFocusFast(); @@ -408,6 +435,18 @@ Widget::Widget( setupSupportMode(); setupScrollUpButton(); + const auto overscrollBg = [=] { + return anim::color( + st::dialogsBg, + st::dialogsBgOver, + _childListShown.current()); + }; + _scroll->setOverscrollBg(overscrollBg()); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _scroll->setOverscrollBg(overscrollBg()); + }, lifetime()); + if (_layout != Layout::Child) { setupConnectingWidget(); @@ -431,6 +470,7 @@ Widget::Widget( _childListShown.changes( ) | rpl::start_with_next([=] { + _scroll->setOverscrollBg(overscrollBg()); updateControlsGeometry(); }, lifetime()); @@ -448,6 +488,8 @@ Widget::Widget( } void Widget::chosenRow(const ChosenRow &row) { + storiesToggleExplicitExpand(false); + const auto history = row.key.history(); const auto topicJump = history ? history->peer->forumTopicFor(row.message.fullId.msg) @@ -484,6 +526,16 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history) { const auto peer = history->peer; + if (const auto user = peer->asUser()) { + if (row.message.fullId.msg == ShowAtUnreadMsgId) { + if (row.userpicClick + && user->hasActiveStories() + && !user->isSelf()) { + controller()->openPeerStories(user->id); + return; + } + } + } const auto showAtMsgId = controller()->uniqueChatsInSearchResults() ? ShowAtUnreadMsgId : row.message.fullId.msg; @@ -497,6 +549,14 @@ void Widget::chosenRow(const ChosenRow &row) { hideChildList(); } } else if (const auto folder = row.key.folder()) { + if (row.userpicClick) { + const auto list = Data::StorySourcesList::Hidden; + const auto &sources = session().data().stories().sources(list); + if (!sources.empty()) { + controller()->openPeerStories(sources.front().id, list); + return; + } + } controller()->openFolder(folder); hideChildList(); } @@ -527,21 +587,17 @@ void Widget::setGeometryWithTopMoved( _topDelta = 0; } +void Widget::scrollToDefaultChecked(bool verytop) { + if (_scrollToAnimation.animating()) { + return; + } + scrollToDefault(verytop); +} + void Widget::setupScrollUpButton() { - _scrollToTop->setClickedCallback([=] { - if (_scrollToAnimation.animating()) { - return; - } - scrollToTop(); - }); - base::install_event_filter(_scrollToTop, [=](not_null event) { - if (event->type() != QEvent::Wheel) { - return base::EventFilterResult::Continue; - } - return _scroll->viewportEvent(event) - ? base::EventFilterResult::Cancel - : base::EventFilterResult::Continue; - }); + _scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); }); + trackScroll(_scrollToTop); + trackScroll(this); updateScrollUpVisibility(); } @@ -551,6 +607,8 @@ void Widget::setupMoreChatsBar() { } controller()->activeChatsFilter( ) | rpl::start_with_next([=](FilterId id) { + storiesToggleExplicitExpand(false); + if (!id) { _moreChatsBar = nullptr; updateControlsGeometry(); @@ -561,6 +619,8 @@ void Widget::setupMoreChatsBar() { this, filters->moreChatsContent(id)); + trackScroll(_moreChatsBar->wrap()); + _moreChatsBar->barClicks( ) | rpl::start_with_next([=] { if (const auto missing = filters->moreChats(id) @@ -732,6 +792,140 @@ void Widget::setupMainMenuToggle() { }, _mainMenu.toggle->lifetime()); } +void Widget::setupStories() { + _stories->verticalScrollEvents( + ) | rpl::start_with_next([=](not_null e) { + _scroll->viewportEvent(e); + }, _stories->lifetime()); + + _storiesContents.fire(Stories::ContentForSession( + &controller()->session(), + Data::StorySourcesList::NotHidden)); + + const auto currentSource = [=] { + using List = Data::StorySourcesList; + return _openedFolder ? List::Hidden : List::NotHidden; + }; + + rpl::combine( + _scroll->positionValue(), + _scroll->movementValue(), + _storiesExplicitExpandValue.value() + ) | rpl::start_with_next([=]( + Ui::ElasticScrollPosition position, + Ui::ElasticScrollMovement movement, + int explicitlyExpanded) { + if (_stories->isHidden()) { + return; + } + const auto overscrollTop = std::max(-position.overscroll, 0); + if (overscrollTop > 0 && _storiesExplicitExpand) { + _scroll->setOverscrollDefaults( + -st::dialogsStoriesFull.height, + 0, + true); + } + if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) { + _storiesExplicitExpandAnimation.stop(); + _storiesExplicitExpand = false; + _storiesExplicitExpandValue = 0; + return; + } + const auto above = std::max(explicitlyExpanded, overscrollTop); + if (_aboveScrollAdded != above) { + _aboveScrollAdded = above; + if (_updateScrollGeometryCached) { + _updateScrollGeometryCached(); + } + } + using Phase = Ui::ElasticScrollMovement; + _stories->setExpandedHeight( + _aboveScrollAdded, + ((movement == Phase::Momentum || movement == Phase::Returning) + && (explicitlyExpanded < above))); + if (position.overscroll > 0 + || (position.value + > (_storiesExplicitExpandScrollTop + + st::dialogsRowHeight))) { + storiesToggleExplicitExpand(false); + } + updateLockUnlockPosition(); + }, lifetime()); + + _stories->collapsedGeometryChanged( + ) | rpl::start_with_next([=] { + updateLockUnlockPosition(); + }, lifetime()); + + _stories->clicks( + ) | rpl::start_with_next([=](uint64 id) { + controller()->openPeerStories(PeerId(int64(id)), currentSource()); + }, lifetime()); + + _stories->showMenuRequests( + ) | rpl::start_with_next([=](const Stories::ShowMenuRequest &request) { + FillSourceMenu(controller(), request); + }, lifetime()); + + _stories->loadMoreRequests( + ) | rpl::start_with_next([=] { + session().data().stories().loadMore(currentSource()); + }, lifetime()); + + _stories->toggleExpandedRequests( + ) | rpl::start_with_next([=](bool expanded) { + const auto position = _scroll->position(); + if (!expanded) { + _scroll->setOverscrollDefaults(0, 0); + } else if (position.value > 0 || position.overscroll >= 0) { + storiesToggleExplicitExpand(true); + _scroll->setOverscrollDefaults(0, 0); + } else { + _scroll->setOverscrollDefaults( + -st::dialogsStoriesFull.height, + 0); + } + }, lifetime()); + + _stories->emptyValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateStoriesVisibility(); + }, lifetime()); + + _stories->widthValue() | rpl::start_with_next([=] { + updateLockUnlockPosition(); + }, lifetime()); +} + +void Widget::storiesToggleExplicitExpand(bool expand) { + if (_storiesExplicitExpand == expand) { + return; + } + _storiesExplicitExpand = expand; + if (!expand) { + _scroll->setOverscrollDefaults(0, 0, true); + } + const auto height = st::dialogsStoriesFull.height; + const auto duration = kStoriesExpandDuration; + _storiesExplicitExpandScrollTop = _scroll->position().value; + _storiesExplicitExpandAnimation.start([=](float64 value) { + _storiesExplicitExpandValue = int(base::SafeRound(value)); + }, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut); +} + +void Widget::trackScroll(not_null widget) { + widget->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd + || type == QEvent::TouchCancel + || type == QEvent::Wheel) { + _scroll->viewportEvent(e); + } + }, widget->lifetime()); +} + void Widget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { @@ -774,6 +968,7 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) { void Widget::updateControlsVisibility(bool fast) { updateLoadMoreChatsVisibility(); _scroll->show(); + updateStoriesVisibility(); if ((_openedFolder || _openedForum) && _filter->hasFocus()) { setInnerFocus(); } @@ -819,6 +1014,23 @@ void Widget::updateControlsVisibility(bool fast) { if (_childList && _filter->hasFocus()) { setInnerFocus(); } + updateLockUnlockPosition(); +} + +void Widget::updateLockUnlockPosition() { + if (_lockUnlock->isHidden()) { + return; + } + const auto stories = (_stories && !_stories->isHidden()) + ? _stories->collapsedGeometryCurrent() + : Stories::List::CollapsedGeometry(); + const auto simple = _filter->x() + _filter->width(); + const auto right = stories.geometry.isEmpty() + ? simple + : anim::interpolate(stories.geometry.x(), simple, stories.expanded); + _lockUnlock->move( + right - _lockUnlock->width(), + st::dialogsFilterPadding.y()); } void Widget::changeOpenedSubsection( @@ -838,6 +1050,7 @@ void Widget::changeOpenedSubsection( } oldContentCache = grabForFolderSlideAnimation(); } + //_scroll->verticalScrollBar()->setMinimum(0); _showAnimation = nullptr; destroyChildListCanvas(); change(); @@ -875,9 +1088,50 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { controller()->closeForum(); _openedFolder = folder; _inner->changeOpenedFolder(folder); + if (_stories) { + storiesExplicitCollapse(); + } }, (folder != nullptr), animated); } +void Widget::storiesExplicitCollapse() { + if (_storiesExplicitExpand) { + storiesToggleExplicitExpand(false); + } else if (_stories) { + using Type = Ui::ElasticScroll::OverscrollType; + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::None, Type::Real); + _scroll->setOverscrollTypes( + _stories->isHidden() ? Type::Real : Type::Virtual, + Type::Real); + } + _storiesExplicitExpandAnimation.stop(); + _storiesExplicitExpandValue = 0; + + using List = Data::StorySourcesList; + collectStoriesUserpicsViews(_openedFolder + ? List::NotHidden + : List::Hidden); + _storiesContents.fire(Stories::ContentForSession( + &session(), + _openedFolder ? List::Hidden : List::NotHidden)); +} + +void Widget::collectStoriesUserpicsViews(Data::StorySourcesList list) { + auto &map = (list == Data::StorySourcesList::Hidden) + ? _storiesUserpicsViewsHidden + : _storiesUserpicsViewsShown; + map.clear(); + auto &owner = session().data(); + for (const auto &source : owner.stories().sources(list)) { + if (const auto peer = owner.peerLoaded(source.id)) { + if (auto view = peer->activeUserpicView(); view.cloud) { + map.emplace(source.id, std::move(view)); + } + } + } +} + void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { if (_openedForum == forum) { return; @@ -888,6 +1142,8 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { _openedForum = forum; _api.request(base::take(_topicSearchRequest)).cancel(); _inner->changeOpenedForum(forum); + storiesToggleExplicitExpand(false); + updateStoriesVisibility(); }, (forum != nullptr), animated); } @@ -901,6 +1157,9 @@ void Widget::refreshTopBars() { if (_openedFolder || _openedForum) { if (!_subsectionTopBar) { _subsectionTopBar.create(this, controller()); + if (_stories) { + _stories->raise(); + } _subsectionTopBar->searchCancelled( ) | rpl::start_with_next([=] { escape(); @@ -1111,10 +1370,16 @@ void Widget::jumpToTop(bool belowPinned) { } } -void Widget::scrollToTop() { +void Widget::scrollToDefault(bool verytop) { + if (verytop) { + //_scroll->verticalScrollBar()->setMinimum(0); + } _scrollToAnimation.stop(); auto scrollTop = _scroll->scrollTop(); const auto scrollTo = 0; + if (scrollTop == scrollTo) { + return; + } const auto maxAnimatedDelta = _scroll->height(); if (scrollTo + maxAnimatedDelta < scrollTop) { scrollTop = scrollTo + maxAnimatedDelta; @@ -1124,9 +1389,19 @@ void Widget::scrollToTop() { startScrollUpButtonAnimation(false); const auto scroll = [=] { - _scroll->scrollToY(qRound(_scrollToAnimation.value(scrollTo))); + const auto animated = qRound(_scrollToAnimation.value(scrollTo)); + const auto animatedDelta = animated - scrollTo; + const auto realDelta = _scroll->scrollTop() - scrollTo; + if (base::OppositeSigns(realDelta, animatedDelta)) { + // We scrolled manually to the other side of target 'scrollTo'. + _scrollToAnimation.stop(); + } else if (std::abs(realDelta) > std::abs(animatedDelta)) { + // We scroll by animation only if it gets us closer to target. + _scroll->scrollToY(animated); + } }; + _scrollAnimationTo = scrollTo; _scrollToAnimation.start( scroll, scrollTop, @@ -1159,6 +1434,7 @@ void Widget::startWidthAnimation() { _widthAnimationCache = Ui::PixmapFromImage(std::move(image)); _scroll->setGeometry(scrollGeometry); _scroll->hide(); + updateStoriesVisibility(); } void Widget::stopWidthAnimation() { @@ -1166,9 +1442,45 @@ void Widget::stopWidthAnimation() { if (!_showAnimation) { _scroll->show(); } + updateStoriesVisibility(); update(); } +void Widget::updateStoriesVisibility() { + if (!_stories) { + return; + } + const auto hidden = (_showAnimation != nullptr) + || _openedForum + || !_widthAnimationCache.isNull() + || _childList + || !_filter->getLastText().isEmpty() + || _searchInChat + || _stories->empty(); + if (_stories->isHidden() != hidden) { + _stories->setVisible(!hidden); + using Type = Ui::ElasticScroll::OverscrollType; + if (hidden) { + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::Real, Type::Real); + if (_scroll->position().overscroll < 0) { + _scroll->scrollToY(0); + } + _scroll->update(); + } else { + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::Virtual, Type::Real); + _storiesExplicitExpandValue.force_assign( + _storiesExplicitExpandValue.current()); + } + if (_aboveScrollAdded > 0 && _updateScrollGeometryCached) { + _updateScrollGeometryCached(); + } + updateLockUnlockVisibility(); + updateLockUnlockPosition(); + } +} + void Widget::showFast() { if (isHidden()) { _inner->clearSelection(); @@ -1211,6 +1523,9 @@ void Widget::startSlideAnimation( QPixmap newContentCache, Window::SlideDirection direction) { _scroll->hide(); + if (_stories) { + _stories->hide(); + } _searchControls->hide(); if (_subsectionTopBar) { _subsectionTopBar->hide(); @@ -1991,7 +2306,8 @@ void Widget::dropEvent(QDropEvent *e) { if (_scroll->geometry().contains(e->pos())) { const auto point = mapToGlobal(e->pos()); if (const auto thread = _inner->updateFromParentDrag(point)) { - e->acceptProposedAction(); + e->setDropAction(Qt::CopyAction); + e->accept(); controller()->content()->filesOrForwardDrop( thread, e->mimeData()); @@ -2018,6 +2334,8 @@ void Widget::applyFilterUpdate(bool force) { return; } + updateLockUnlockVisibility(anim::type::normal); + updateStoriesVisibility(); const auto filterText = currentSearchQuery(); _inner->applyFilterUpdate(filterText, force); if (filterText.isEmpty() && !_searchFromAuthor) { @@ -2026,6 +2344,7 @@ void Widget::applyFilterUpdate(bool force) { _cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal); updateLoadMoreChatsVisibility(); updateJumpToDateVisibility(); + updateLockUnlockPosition(); if (filterText.isEmpty()) { _peerSearchCache.clear(); @@ -2171,6 +2490,7 @@ void Widget::closeChildList(anim::type animated) { } else { _childListShadow = nullptr; } + updateStoriesVisibility(); } void Widget::searchInChat(Key chat) { @@ -2224,11 +2544,13 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) { _searchInChat = chat; controller()->searchInChat = _searchInChat; updateJumpToDateVisibility(); + updateStoriesVisibility(); } if (searchFromUpdated) { updateSearchFromVisibility(); clearSearchCache(); } + updateLockUnlockPosition(); if (_searchInChat && _layout == Layout::Main) { controller()->closeFolder(); } @@ -2347,13 +2669,25 @@ void Widget::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } -void Widget::updateLockUnlockVisibility() { +void Widget::updateLockUnlockVisibility(anim::type animated) { if (_showAnimation) { return; } - const auto hidden = !session().domain().local().hasLocalPasscode(); - if (_lockUnlock->isHidden() != hidden) { - _lockUnlock->setVisible(!hidden); + const auto hidden = !session().domain().local().hasLocalPasscode() + || (_showAnimation != nullptr) + || _openedForum + || !_widthAnimationCache.isNull() + || _childList + || !_filter->getLastText().isEmpty() + || _searchInChat; + if (_lockUnlock->toggled() == hidden) { + const auto stories = _stories && !_stories->empty(); + _lockUnlock->toggle( + !hidden, + stories ? anim::type::instant : animated); + if (!hidden) { + updateLockUnlockPosition(); + } updateControlsGeometry(); } } @@ -2422,11 +2756,9 @@ void Widget::updateControlsGeometry() { ? st::dialogsFilterSkip : (st::dialogsFilterPadding.x() + _mainMenu.toggle->width())) + st::dialogsFilterPadding.x(); - auto filterRight = (session().domain().local().hasLocalPasscode() - ? (st::dialogsFilterPadding.x() + _lockUnlock->width()) - : st::dialogsFilterSkip) + st::dialogsFilterPadding.x(); - auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight; - auto filterAreaHeight = st::topBarHeight; + const auto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x(); + const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight; + const auto filterAreaHeight = st::topBarHeight; _searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight); if (_subsectionTopBar) { _subsectionTopBar->setGeometryWithNarrowRatio( @@ -2438,6 +2770,7 @@ void Widget::updateControlsGeometry() { auto filterTop = (filterAreaHeight - _filter->height()) / 2; filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio); _filter->setGeometryToLeft(filterLeft, filterTop, filterWidth, _filter->height()); + auto mainMenuLeft = anim::interpolate( st::dialogsFilterPadding.x(), (_narrowWidth - _mainMenu.toggle->width()) / 2, @@ -2457,52 +2790,39 @@ void Widget::updateControlsGeometry() { _searchForNarrowFilters->moveToLeft(searchLeft, st::dialogsFilterPadding.y()); auto right = filterLeft + filterWidth; - _lockUnlock->moveToLeft(right + st::dialogsFilterPadding.x(), st::dialogsFilterPadding.y()); _cancelSearch->moveToLeft(right - _cancelSearch->width(), _filter->y()); right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _filter->y()); right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _filter->y()); const auto barw = width(); + const auto expandedStoriesTop = filterAreaTop + filterAreaHeight; + const auto storiesHeight = 2 * st::dialogsStories.photoTop + + st::dialogsStories.photo; + const auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2; + if (_stories) { + _stories->setLayoutConstraints( + { filterLeft + filterWidth, filterTop + added }, + style::al_right, + { 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height }); + } if (_forumTopShadow) { _forumTopShadow->setGeometry( 0, - filterAreaTop + filterAreaHeight, + expandedStoriesTop, barw, st::lineWidth); } - const auto moreChatsBarTop = filterAreaTop + filterAreaHeight; - if (_moreChatsBar) { - _moreChatsBar->move(0, moreChatsBarTop); - _moreChatsBar->resizeToWidth(barw); - } - const auto forumGroupCallTop = moreChatsBarTop - + (_moreChatsBar ? _moreChatsBar->height() : 0); - if (_forumGroupCallBar) { - _forumGroupCallBar->move(0, forumGroupCallTop); - _forumGroupCallBar->resizeToWidth(barw); - } - const auto forumRequestsTop = forumGroupCallTop - + (_forumGroupCallBar ? _forumGroupCallBar->height() : 0); - if (_forumRequestsBar) { - _forumRequestsBar->move(0, forumRequestsTop); - _forumRequestsBar->resizeToWidth(barw); - } - const auto forumReportTop = forumRequestsTop - + (_forumRequestsBar ? _forumRequestsBar->height() : 0); - if (_forumReportBar) { - _forumReportBar->bar().move(0, forumReportTop); - } - auto scrollTop = forumReportTop - + (_forumReportBar ? _forumReportBar->bar().height() : 0); - auto newScrollTop = _scroll->scrollTop() + _topDelta; - auto scrollHeight = height() - scrollTop; + + updateLockUnlockPosition(); + + auto bottomSkip = 0; const auto putBottomButton = [&](auto &button) { if (button && !button->isHidden()) { const auto buttonHeight = button->height(); - scrollHeight -= buttonHeight; + bottomSkip += buttonHeight; button->setGeometry( 0, - scrollTop + scrollHeight, + height() - bottomSkip, barw, buttonHeight); } @@ -2510,21 +2830,61 @@ void Widget::updateControlsGeometry() { putBottomButton(_updateTelegram); putBottomButton(_downloadBar); putBottomButton(_loadMoreChats); - const auto bottomSkip = (height() - scrollTop) - scrollHeight; if (_connecting) { _connecting->setBottomSkip(bottomSkip); } controller()->setConnectingBottomSkip(bottomSkip); - const auto scrollw = _childList ? _narrowWidth : barw; - const auto wasScrollHeight = _scroll->height(); - _scroll->setGeometry(0, scrollTop, scrollw, scrollHeight); - _inner->resize(scrollw, _inner->height()); - _inner->setNarrowRatio(narrowRatio); - if (scrollHeight != wasScrollHeight) { - controller()->floatPlayerAreaUpdated(); + const auto wasScrollTop = _scroll->scrollTop(); + const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0) + ? wasScrollTop + : (wasScrollTop + _topDelta); + + const auto scrollWidth = _childList ? _narrowWidth : barw; + if (_moreChatsBar) { + _moreChatsBar->resizeToWidth(barw); } - if (_topDelta) { + if (_forumGroupCallBar) { + _forumGroupCallBar->resizeToWidth(barw); + } + if (_forumRequestsBar) { + _forumRequestsBar->resizeToWidth(barw); + } + _updateScrollGeometryCached = [=] { + const auto moreChatsBarTop = expandedStoriesTop + + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); + if (_moreChatsBar) { + _moreChatsBar->move(0, moreChatsBarTop); + } + const auto forumGroupCallTop = moreChatsBarTop + + (_moreChatsBar ? _moreChatsBar->height() : 0); + if (_forumGroupCallBar) { + _forumGroupCallBar->move(0, forumGroupCallTop); + } + const auto forumRequestsTop = forumGroupCallTop + + (_forumGroupCallBar ? _forumGroupCallBar->height() : 0); + if (_forumRequestsBar) { + _forumRequestsBar->move(0, forumRequestsTop); + } + const auto forumReportTop = forumRequestsTop + + (_forumRequestsBar ? _forumRequestsBar->height() : 0); + if (_forumReportBar) { + _forumReportBar->bar().move(0, forumReportTop); + } + const auto scrollTop = forumReportTop + + (_forumReportBar ? _forumReportBar->bar().height() : 0); + const auto scrollHeight = height() - scrollTop; + const auto wasScrollHeight = _scroll->height(); + _scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight); + if (scrollHeight != wasScrollHeight) { + controller()->floatPlayerAreaUpdated(); + } + }; + _updateScrollGeometryCached(); + + _inner->resize(scrollWidth, _inner->height()); + _inner->setNarrowRatio(narrowRatio); + if (newScrollTop != wasScrollTop) { _scroll->scrollToY(newScrollTop); } else { listScrollUpdated(); @@ -2534,8 +2894,8 @@ void Widget::updateControlsGeometry() { } if (_childList) { - const auto childw = std::max(_narrowWidth, width() - scrollw); - const auto childh = scrollTop + scrollHeight; + const auto childw = std::max(_narrowWidth, width() - scrollWidth); + const auto childh = _scroll->y() + _scroll->height(); const auto childx = width() - childw; _childList->setGeometryWithTopMoved( { childx, 0, childw, childh }, @@ -2601,7 +2961,7 @@ void Widget::paintEvent(QPaintEvent *e) { p.fillRect(above.intersected(r), bg); } - auto belowTop = _scroll->y() + qMin(_scroll->height(), _inner->height()); + auto belowTop = _scroll->y() + _scroll->height(); if (!_widthAnimationCache.isNull()) { p.drawPixmapLeft(0, _scroll->y(), width(), _widthAnimationCache); belowTop = _scroll->y() + (_widthAnimationCache.height() / cIntRetinaFactor()); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 009f6ca3a..9473ac348 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_key.h" #include "window/section_widget.h" #include "ui/effects/animations.h" -#include "ui/widgets/scroll_area.h" +#include "ui/userpic_view.h" #include "mtproto/sender.h" #include "api/api_single_message_search.h" @@ -21,6 +21,7 @@ class Error; namespace Data { class Forum; +enum class StorySourcesList : uchar; } // namespace Data namespace Main { @@ -46,6 +47,7 @@ class GroupCallBar; class RequestsBar; class MoreChatsBar; class JumpDownButton; +class ElasticScroll; template class FadeWrapScaled; } // namespace Ui @@ -56,6 +58,11 @@ class ConnectionState; struct SectionShow; } // namespace Window +namespace Dialogs::Stories { +class List; +struct Content; +} // namespace Dialogs::Stories + namespace Dialogs { struct RowDescriptor; @@ -162,6 +169,11 @@ private: void setupMoreChatsBar(); void setupDownloadBar(); void setupShortcuts(); + void setupStories(); + void storiesExplicitCollapse(); + void collectStoriesUserpicsViews(Data::StorySourcesList list); + void storiesToggleExplicitExpand(bool expand); + void trackScroll(not_null widget); [[nodiscard]] bool searchForPeersRequired(const QString &query) const; [[nodiscard]] bool searchForTopicsRequired(const QString &query) const; bool setSearchInChat(Key chat, PeerData *from = nullptr); @@ -171,8 +183,10 @@ private: void clearSearchCache(); void setSearchQuery(const QString &query); void updateControlsVisibility(bool fast = false); - void updateLockUnlockVisibility(); + void updateLockUnlockVisibility( + anim::type animated = anim::type::instant); void updateLoadMoreChatsVisibility(); + void updateStoriesVisibility(); void updateJumpToDateVisibility(bool fast = false); void updateSearchFromVisibility(bool fast = false); void updateControlsGeometry(); @@ -209,11 +223,13 @@ private: mtpRequestId requestId); void peopleFailed(const MTP::Error &error, mtpRequestId requestId); - void scrollToTop(); + void scrollToDefault(bool verytop = false); + void scrollToDefaultChecked(bool verytop = false); void setupScrollUpButton(); void updateScrollUpVisibility(); void startScrollUpButtonAnimation(bool shown); void updateScrollUpPosition(); + void updateLockUnlockPosition(); MTP::Sender _api; @@ -234,7 +250,7 @@ private: object_ptr> _chooseFromUser; object_ptr> _jumpToDate; object_ptr _cancelSearch; - object_ptr _lockUnlock; + object_ptr< Ui::FadeWrapScaled> _lockUnlock; std::unique_ptr _moreChatsBar; @@ -243,7 +259,7 @@ private: std::unique_ptr _forumRequestsBar; std::unique_ptr _forumReportBar; - object_ptr _scroll; + object_ptr _scroll; QPointer _inner; class BottomButton; object_ptr _updateTelegram = { nullptr }; @@ -252,6 +268,7 @@ private: std::unique_ptr _connecting; Ui::Animations::Simple _scrollToAnimation; + int _scrollAnimationTo = 0; std::unique_ptr _showAnimation; rpl::variable _shownProgressValue; @@ -267,6 +284,17 @@ private: PeerData *_searchFromAuthor = nullptr; QString _lastFilterText; + rpl::event_stream> _storiesContents; + base::flat_map _storiesUserpicsViewsHidden; + base::flat_map _storiesUserpicsViewsShown; + Fn _updateScrollGeometryCached; + std::unique_ptr _stories; + Ui::Animations::Simple _storiesExplicitExpandAnimation; + rpl::variable _storiesExplicitExpandValue = 0; + int _storiesExplicitExpandScrollTop = 0; + int _aboveScrollAdded = 0; + bool _storiesExplicitExpand = false; + base::Timer _searchTimer; QString _peerSearchQuery; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 7e195ba94..91674bfed 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -331,22 +331,23 @@ void PaintRow( context.st->padding.top(), context.width, context.st->photoSize); - } else if (from) { - row->paintUserpic( - p, - from, - videoUserpic, - (flags & Flag::AllowUserOnline) ? history : nullptr, - context); - } else if (hiddenSenderInfo) { + } else if (!from && hiddenSenderInfo) { hiddenSenderInfo->emptyUserpic.paintCircle( p, context.st->padding.left(), context.st->padding.top(), context.width, context.st->photoSize); + } else if (!(flags & Flag::AllowUserOnline)) { + PaintUserpic( + p, + entry, + from, + videoUserpic, + row->userpicView(), + context); } else { - entry->paintUserpic(p, row->userpicView(), context); + row->paintUserpic(p, entry, from, videoUserpic, context); } auto nameleft = context.st->nameLeft; @@ -785,7 +786,7 @@ void RowPainter::Paint( ? history->peer->migrateTo() : history->peer.get()) : nullptr; - const auto allowUserOnline = !context.narrow || badgesState.empty(); + const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) | (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0)) | (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0)) diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp new file mode 100644 index 000000000..ed723b55e --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -0,0 +1,548 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "dialogs/ui/dialogs_stories_content.h" + +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_user.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "info/stories/info_stories_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_menu_icons.h" + +namespace Dialogs::Stories { +namespace { + +constexpr auto kShownLastCount = 3; + +class PeerUserpic final : public Thumbnail { +public: + explicit PeerUserpic(not_null peer); + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + Ui::PeerUserpicView view; + Fn callback; + InMemoryKey key; + rpl::lifetime photoLifetime; + rpl::lifetime downloadLifetime; + }; + + [[nodiscard]] bool waitingUserpicLoad() const; + void processNewPhoto(); + + const not_null _peer; + QImage _frame; + std::unique_ptr _subscribed; + +}; + +class StoryThumbnail : public Thumbnail { +public: + explicit StoryThumbnail(FullStoryId id); + virtual ~StoryThumbnail() = default; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +protected: + struct Thumb { + Image *image = nullptr; + bool blurred = false; + }; + [[nodiscard]] virtual Main::Session &session() = 0; + [[nodiscard]] virtual Thumb loaded(FullStoryId id) = 0; + virtual void clear() = 0; + +private: + const FullStoryId _id; + QImage _full; + rpl::lifetime _subscription; + QImage _prepared; + bool _blurred = false; + +}; + +class PhotoThumbnail final : public StoryThumbnail { +public: + PhotoThumbnail(not_null photo, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _photo; + std::shared_ptr _media; + +}; + +class VideoThumbnail final : public StoryThumbnail { +public: + VideoThumbnail(not_null video, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _video; + std::shared_ptr _media; + +}; + +class EmptyThumbnail final : public Thumbnail { +public: + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + QImage _cached; + +}; + +class State final { +public: + State(not_null data, Data::StorySourcesList list); + + [[nodiscard]] Content next(); + +private: + const not_null _data; + const Data::StorySourcesList _list; + base::flat_map< + not_null, + std::shared_ptr> _userpics; + +}; + +PeerUserpic::PeerUserpic(not_null peer) +: _peer(peer) { +} + +QImage PeerUserpic::image(int size) { + Expects(_subscribed != nullptr); + + const auto good = (_frame.width() == size * _frame.devicePixelRatio()); + const auto key = _peer->userpicUniqueKey(_subscribed->view); + if (!good || (_subscribed->key != key && !waitingUserpicLoad())) { + const auto ratio = style::DevicePixelRatio(); + _subscribed->key = key; + _frame = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + _frame.fill(Qt::transparent); + + auto p = Painter(&_frame); + _peer->paintUserpic(p, _subscribed->view, 0, 0, size); + } + return _frame; +} + +bool PeerUserpic::waitingUserpicLoad() const { + return _peer->hasUserpic() && _peer->useEmptyUserpic(_subscribed->view); +} + +void PeerUserpic::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(std::move(callback)); + + _peer->session().changes().peerUpdates( + _peer, + Data::PeerUpdate::Flag::Photo + ) | rpl::start_with_next([=] { + _subscribed->callback(); + processNewPhoto(); + }, _subscribed->photoLifetime); + + processNewPhoto(); +} + +void PeerUserpic::processNewPhoto() { + Expects(_subscribed != nullptr); + + if (!waitingUserpicLoad()) { + _subscribed->downloadLifetime.destroy(); + return; + } + _peer->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return !waitingUserpicLoad(); + }) | rpl::start_with_next([=] { + _subscribed->callback(); + _subscribed->downloadLifetime.destroy(); + }, _subscribed->downloadLifetime); +} + +StoryThumbnail::StoryThumbnail(FullStoryId id) +: _id(id) { +} + +QImage StoryThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_prepared.width() != size * ratio) { + if (_full.isNull()) { + _prepared = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _prepared.fill(Qt::black); + } else { + const auto width = _full.width(); + const auto skip = std::max((_full.height() - width) / 2, 0); + _prepared = _full.copy(0, skip, width, width).scaled( + QSize(size, size) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + _prepared = Images::Circle(std::move(_prepared)); + _prepared.setDevicePixelRatio(ratio); + } + return _prepared; +} + +void StoryThumbnail::subscribeToUpdates(Fn callback) { + _subscription.destroy(); + if (!callback) { + clear(); + return; + } else if (!_full.isNull() && !_blurred) { + return; + } + const auto thumbnail = loaded(_id); + if (const auto image = thumbnail.image) { + _full = image->original(); + } + _blurred = thumbnail.blurred; + if (!_blurred) { + _prepared = QImage(); + } else { + _subscription = session().downloaderTaskFinished( + ) | rpl::filter([=] { + const auto thumbnail = loaded(_id); + if (!thumbnail.blurred) { + _full = thumbnail.image->original(); + _prepared = QImage(); + _blurred = false; + return true; + } + return false; + }) | rpl::take(1) | rpl::start_with_next(callback); + } +} + +PhotoThumbnail::PhotoThumbnail(not_null photo, FullStoryId id) +: StoryThumbnail(id) +, _photo(photo) { +} + +Main::Session &PhotoThumbnail::session() { + return _photo->session(); +} + +StoryThumbnail::Thumb PhotoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _photo->createMediaView(); + _media->wanted( + Data::PhotoSize::Small, + Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->image(Data::PhotoSize::Small)) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void PhotoThumbnail::clear() { + _media = nullptr; +} + +VideoThumbnail::VideoThumbnail( + not_null video, + FullStoryId id) +: StoryThumbnail(id) +, _video(video) { +} + +Main::Session &VideoThumbnail::session() { + return _video->session(); +} + +StoryThumbnail::Thumb VideoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _video->createMediaView(); + _media->thumbnailWanted(Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->thumbnail()) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void VideoThumbnail::clear() { + _media = nullptr; +} + +QImage EmptyThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_cached.width() != size * ratio) { + _cached = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _cached.fill(Qt::black); + _cached.setDevicePixelRatio(ratio); + } + return _cached; +} + +void EmptyThumbnail::subscribeToUpdates(Fn callback) { +} + +State::State(not_null data, Data::StorySourcesList list) +: _data(data) +, _list(list) { +} + +Content State::next() { + auto result = Content(); + const auto &sources = _data->sources(_list); + result.elements.reserve(sources.size()); + for (const auto &info : sources) { + const auto source = _data->source(info.id); + Assert(source != nullptr); + + auto userpic = std::shared_ptr(); + const auto user = source->user; + if (const auto i = _userpics.find(user); i != end(_userpics)) { + userpic = i->second; + } else { + userpic = MakeUserpicThumbnail(user); + _userpics.emplace(user, userpic); + } + result.elements.push_back({ + .id = uint64(user->id.value), + .name = (user->isSelf() + ? tr::lng_stories_my_name(tr::now) + : user->shortName()), + .thumbnail = std::move(userpic), + .count = info.count, + .unreadCount = info.unreadCount, + .skipSmall = user->isSelf() ? 1U : 0U, + }); + } + return result; +} + +} // namespace + +rpl::producer ContentForSession( + not_null session, + Data::StorySourcesList list) { + return [=](auto consumer) { + auto result = rpl::lifetime(); + const auto stories = &session->data().stories(); + const auto state = result.make_state(stories, list); + rpl::single( + rpl::empty + ) | rpl::then( + stories->sourcesChanged(list) + ) | rpl::start_with_next([=] { + consumer.put_next(state->next()); + }, result); + return result; + }; +} + +rpl::producer LastForPeer(not_null peer) { + using namespace rpl::mappers; + + const auto stories = &peer->owner().stories(); + const auto peerId = peer->id; + + return rpl::single( + peerId + ) | rpl::then( + stories->sourceChanged() | rpl::filter(_1 == peerId) + ) | rpl::map([=] { + auto ids = std::vector(); + auto readTill = StoryId(); + if (const auto source = stories->source(peerId)) { + readTill = source->readTill; + ids = ranges::views::all(source->ids) + | ranges::views::reverse + | ranges::views::take(kShownLastCount) + | ranges::views::transform(&Data::StoryIdDates::id) + | ranges::to_vector; + } + return rpl::make_producer([=](auto consumer) { + auto lifetime = rpl::lifetime(); + if (ids.empty()) { + consumer.put_next(Content()); + consumer.put_done(); + return lifetime; + } + + struct State { + Fn check; + base::has_weak_ptr guard; + int readTill = StoryId(); + bool pushed = false; + }; + const auto state = lifetime.make_state(); + state->readTill = readTill; + state->check = [=] { + if (state->pushed) { + return; + } + auto done = true; + auto resolving = false; + auto result = Content{}; + for (const auto id : ids) { + const auto storyId = FullStoryId{ peerId, id }; + const auto maybe = stories->lookup(storyId); + if (maybe) { + if (!resolving) { + const auto unread = (id > state->readTill); + result.elements.reserve(ids.size()); + result.elements.push_back({ + .id = uint64(id), + .thumbnail = MakeStoryThumbnail(*maybe), + .count = 1U, + .unreadCount = unread ? 1U : 0U, + }); + if (unread) { + done = false; + } + } + } else if (maybe.error() == Data::NoStory::Unknown) { + resolving = true; + stories->resolve( + storyId, + crl::guard(&state->guard, state->check)); + } + } + if (resolving) { + return; + } + state->pushed = true; + consumer.put_next(std::move(result)); + if (done) { + consumer.put_done(); + } + }; + + rpl::single(peerId) | rpl::then( + stories->itemsChanged() | rpl::filter(_1 == peerId) + ) | rpl::start_with_next(state->check, lifetime); + + stories->session().changes().storyUpdates( + Data::StoryUpdate::Flag::MarkRead + ) | rpl::start_with_next([=](const Data::StoryUpdate &update) { + if (update.story->peer()->id == peerId) { + if (update.story->id() > state->readTill) { + state->readTill = update.story->id(); + if (ranges::contains(ids, state->readTill) + || state->readTill > ids.front()) { + state->pushed = false; + state->check(); + } + } + } + }, lifetime); + + return lifetime; + }); + }) | rpl::flatten_latest(); +} + +std::shared_ptr MakeUserpicThumbnail(not_null peer) { + return std::make_shared(peer); +} + +std::shared_ptr MakeStoryThumbnail( + not_null story) { + using Result = std::shared_ptr; + const auto id = story->fullId(); + return v::match(story->media().data, [](v::null_t) -> Result { + return std::make_shared(); + }, [&](not_null photo) -> Result { + return std::make_shared(photo, id); + }, [&](not_null video) -> Result { + return std::make_shared(video, id); + }); +} + +void FillSourceMenu( + not_null controller, + const ShowMenuRequest &request) { + const auto owner = &controller->session().data(); + const auto peer = owner->peer(PeerId(request.id)); + const auto &add = request.callback; + if (peer->isSelf()) { + add(tr::lng_stories_archive_button(tr::now), [=] { + controller->showSection(Info::Stories::Make( + peer, + Info::Stories::Tab::Archive)); + }, &st::menuIconStoriesArchiveSection); + add(tr::lng_stories_my_title(tr::now), [=] { + controller->showSection(Info::Stories::Make(peer)); + }, &st::menuIconStoriesSavedSection); + } else { + add(tr::lng_profile_send_message(tr::now), [=] { + controller->showPeerHistory(peer); + }, &st::menuIconChatBubble); + add(tr::lng_context_view_profile(tr::now), [=] { + controller->showPeerInfo(peer); + }, &st::menuIconProfile); + const auto in = [&](Data::StorySourcesList list) { + return ranges::contains( + owner->stories().sources(list), + peer->id, + &Data::StoriesSourceInfo::id); + }; + const auto toggle = [=](bool shown) { + owner->stories().toggleHidden( + peer->id, + !shown, + controller->uiShow()); + }; + if (in(Data::StorySourcesList::NotHidden)) { + add(tr::lng_stories_archive(tr::now), [=] { + toggle(false); + }, &st::menuIconArchive); + } + if (in(Data::StorySourcesList::Hidden)) { + add(tr::lng_stories_unarchive(tr::now), [=] { + toggle(true); + }, &st::menuIconUnarchive); + } + } +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h new file mode 100644 index 000000000..b42715d54 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -0,0 +1,44 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +enum class StorySourcesList : uchar; +class Story; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Dialogs::Stories { + +struct Content; +class Thumbnail; +struct ShowMenuRequest; + +[[nodiscard]] rpl::producer ContentForSession( + not_null session, + Data::StorySourcesList list); + +[[nodiscard]] rpl::producer LastForPeer(not_null peer); + +[[nodiscard]] std::shared_ptr MakeUserpicThumbnail( + not_null peer); +[[nodiscard]] std::shared_ptr MakeStoryThumbnail( + not_null story); + +void FillSourceMenu( + not_null controller, + const ShowMenuRequest &request); + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp new file mode 100644 index 000000000..1a1a395d3 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -0,0 +1,967 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "dialogs/ui/dialogs_stories_list.h" + +#include "lang/lang_keys.h" +#include "ui/effects/outline_segments.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/popup_menu.h" +#include "ui/painter.h" +#include "styles/style_dialogs.h" + +#include + +#include "base/debug_log.h" + +namespace Dialogs::Stories { +namespace { + +constexpr auto kSmallThumbsShown = 3; +constexpr auto kPreloadPages = 2; +constexpr auto kExpandAfterRatio = 0.72; +constexpr auto kCollapseAfterRatio = 0.68; +constexpr auto kFrictionRatio = 0.15; +constexpr auto kExpandCatchUpDuration = crl::time(200); + +[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) { + const auto &full = st.full; + const auto &font = full.nameStyle.font; + const auto skip = font->spacew; + return full.photoLeft * 2 + full.photo - 2 * skip; +} + +} // namespace + +struct List::Layout { + int itemsCount = 0; + QPointF geometryShift; + float64 expandedRatio = 0.; + float64 expandRatio = 0.; + float64 ratio = 0.; + float64 segmentsSpinProgress = 0.; + float64 thumbnailLeft = 0.; + float64 photoLeft = 0.; + float64 left = 0.; + float64 single = 0.; + int smallSkip = 0; + int leftFull = 0; + int leftSmall = 0; + int singleFull = 0; + int singleSmall = 0; + int startIndexSmall = 0; + int endIndexSmall = 0; + int startIndexFull = 0; + int endIndexFull = 0; +}; + +List::List( + not_null parent, + const style::DialogsStoriesList &st, + rpl::producer content) +: RpWidget(parent) +, _st(st) { + setCursor(style::cur_default); + + std::move(content) | rpl::start_with_next([=](Content &&content) { + showContent(std::move(content)); + }, lifetime()); + + setMouseTracking(true); + resize(0, _data.empty() ? 0 : st.full.height); +} + +List::~List() = default; + +void List::showContent(Content &&content) { + if (_content == content) { + return; + } + if (content.elements.empty()) { + _data = {}; + _empty = true; + return; + } + const auto wasCount = int(_data.items.size()); + _content = std::move(content); + auto items = base::take(_data.items); + _data.items.reserve(_content.elements.size()); + for (const auto &element : _content.elements) { + const auto id = element.id; + const auto i = ranges::find(items, id, [](const Item &item) { + return item.element.id; + }); + if (i != end(items)) { + _data.items.push_back(std::move(*i)); + auto &item = _data.items.back(); + if (item.element.thumbnail != element.thumbnail) { + item.element.thumbnail = element.thumbnail; + item.subscribed = false; + } + if (item.element.name != element.name) { + item.element.name = element.name; + item.nameCache = QImage(); + } + item.element.count = element.count; + item.element.unreadCount = element.unreadCount; + } else { + _data.items.emplace_back(Item{ .element = element }); + } + } + if (int(_data.items.size()) != wasCount) { + updateGeometry(); + } + updateScrollMax(); + update(); + if (!wasCount) { + _empty = false; + } +} + +void List::updateScrollMax() { + const auto &full = _st.full; + const auto singleFull = full.photoLeft * 2 + full.photo; + const auto widthFull = full.left + int(_data.items.size()) * singleFull; + _scrollLeftMax = std::max(widthFull - width(), 0); + _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); + checkLoadMore(); + update(); +} + +rpl::producer List::clicks() const { + return _clicks.events(); +} + +rpl::producer List::showMenuRequests() const { + return _showMenuRequests.events(); +} + +rpl::producer List::toggleExpandedRequests() const { + return _toggleExpandedRequests.events(); +} + +rpl::producer<> List::entered() const { + return _entered.events(); +} + +rpl::producer<> List::loadMoreRequests() const { + return _loadMoreRequests.events(); +} + +rpl::producer> List::verticalScrollEvents() const { + return _verticalScrollEvents.events(); +} + +void List::requestExpanded(bool expanded) { + if (_expanded != expanded) { + _expanded = expanded; + const auto from = _expanded ? 0. : 1.; + const auto till = _expanded ? 2. : 0.; + const auto duration = (_expanded ? 2 : 1) * st::slideWrapDuration; + _expandedAnimation.start([=] { + checkForFullState(); + update(); + _collapsedGeometryChanged.fire({}); + }, from, till, duration, anim::sineInOut); + } + _toggleExpandedRequests.fire_copy(_expanded); +} + +void List::enterEventHook(QEnterEvent *e) { + _entered.fire({}); +} + +void List::resizeEvent(QResizeEvent *e) { + updateScrollMax(); +} + +void List::updateExpanding(int expandingHeight, int expandedHeight) { + Expects(!expandingHeight || expandedHeight > 0); + + const auto ratio = !expandingHeight + ? 0. + : (float64(expandingHeight) / expandedHeight); + if (_lastRatio == ratio) { + return; + } + const auto expanding = (ratio > _lastRatio); + _lastRatio = ratio; + const auto change = _expanded + ? (!expanding && ratio < kCollapseAfterRatio) + : (expanding && ratio > kExpandAfterRatio); + if (change) { + requestExpanded(!_expanded); + } +} + +List::Layout List::computeLayout() { + updateExpanding( + _lastExpandedHeight * _expandCatchUpAnimation.value(1.), + _st.full.height); + return computeLayout(_expandedAnimation.value(_expanded ? 2. : 0.)); +} + +List::Layout List::computeLayout(float64 expanded) const { + const auto segmentsSpinProgress = expanded / 2.; + expanded = std::min(expanded, 1.); + + const auto &st = _st.small; + const auto &full = _st.full; + const auto expandedRatio = _lastRatio; + const auto collapsedRatio = expandedRatio * kFrictionRatio; + const auto ratio = expandedRatio * expanded + + collapsedRatio * (1. - expanded); + const auto expandRatio = (ratio >= kCollapseAfterRatio) + ? 1. + : (ratio <= kExpandAfterRatio * kFrictionRatio) + ? 0. + : ((ratio - (kExpandAfterRatio * kFrictionRatio)) + / (kCollapseAfterRatio - (kExpandAfterRatio * kFrictionRatio))); + + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto widthFull = width(); + const auto itemsCount = int(_data.items.size()); + const auto leftFullMin = full.left; + const auto singleFullMin = full.photoLeft * 2 + full.photo; + const auto totalFull = leftFullMin + singleFullMin * itemsCount; + const auto skipSide = (totalFull < widthFull) + ? (widthFull - totalFull) / (itemsCount + 1) + : 0; + const auto skipBetween = (totalFull < widthFull && itemsCount > 1) + ? (widthFull - totalFull - 2 * skipSide) / (itemsCount - 1) + : skipSide; + const auto singleFull = singleFullMin + skipBetween; + const auto smallSkip = (itemsCount > 1 + && _data.items[0].element.skipSmall) + ? 1 + : 0; + const auto smallCount = std::min( + kSmallThumbsShown, + itemsCount - smallSkip); + const auto leftSmall = st.left - (smallSkip ? st.shift : 0); + const auto leftFull = full.left - _scrollLeft + skipSide; + const auto startIndexFull = std::max(-leftFull, 0) / singleFull; + const auto cellLeftFull = leftFull + (startIndexFull * singleFull); + const auto endIndexFull = std::min( + (width() - leftFull + singleFull - 1) / singleFull, + itemsCount); + const auto startIndexSmall = std::min(startIndexFull, smallSkip); + const auto endIndexSmall = smallSkip + smallCount; + const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift); + const auto thumbnailLeftFull = cellLeftFull + full.photoLeft; + const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft; + const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull); + const auto photoLeft = lerp(st.photoLeft, full.photoLeft); + return Layout{ + .itemsCount = itemsCount, + .geometryShift = QPointF( + (_state == State::Changing + ? (lerp(_changingGeometryFrom.x(), _geometryFull.x()) - x()) + : 0.), + (_state == State::Changing + ? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y()) + : 0.)), + .expandedRatio = expandedRatio, + .expandRatio = expandRatio, + .ratio = ratio, + .segmentsSpinProgress = segmentsSpinProgress, + .thumbnailLeft = thumbnailLeft, + .photoLeft = photoLeft, + .left = thumbnailLeft - photoLeft, + .single = lerp(st.shift, singleFull), + .smallSkip = smallSkip, + .leftFull = leftFull, + .leftSmall = leftSmall, + .singleFull = singleFull, + .singleSmall = st.shift, + .startIndexSmall = startIndexSmall, + .endIndexSmall = endIndexSmall, + .startIndexFull = startIndexFull, + .endIndexFull = endIndexFull, + }; +} + +void List::paintEvent(QPaintEvent *e) { + const auto &st = _st.small; + const auto &full = _st.full; + const auto layout = computeLayout(); + const auto ratio = layout.ratio; + const auto expandRatio = layout.expandRatio; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto elerp = [&](float64 a, float64 b) { + return a + (b - a) * expandRatio; + }; + const auto line = elerp(st.lineTwice, full.lineTwice) / 2.; + const auto photo = lerp(st.photo, full.photo); + const auto layered = layout.single < (photo + 4 * line); + auto p = QPainter(this); + if (layered) { + ensureLayer(); + auto q = QPainter(&_layer); + paint(q, layout, photo, line, true); + q.end(); + p.drawImage(0, 0, _layer); + } else { + paint(p, layout, photo, line, false); + } +} + +void List::ensureLayer() { + const auto ratio = style::DevicePixelRatio(); + const auto layer = size() * ratio; + if (_layer.size() != layer) { + _layer = QImage(layer, QImage::Format_ARGB32_Premultiplied); + _layer.setDevicePixelRatio(ratio); + } + _layer.fill(Qt::transparent); +} + +void List::paint( + QPainter &p, + const Layout &layout, + float64 photo, + float64 line, + bool layered) { + const auto &st = _st.small; + const auto &full = _st.full; + const auto ratio = layout.ratio; + const auto expandRatio = layout.expandRatio; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto elerp = [&](float64 a, float64 b) { + return a + (b - a) * expandRatio; + }; + const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto photoTopSmall = st.photoTop; + const auto photoTop = photoTopSmall + + (full.photoTop - photoTopSmall) * layout.expandedRatio; + const auto nameScale = _lastRatio; + const auto nameTop = full.nameTop + + (photoTop + photo - full.photoTop - full.photo); + const auto nameWidth = nameScale * AvailableNameWidth(_st); + const auto nameHeight = nameScale * full.nameStyle.font->height; + const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; + const auto readUserpicOpacity = elerp(_st.readOpacity, 1.); + const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.); + if (_state == State::Changing) { + p.translate(layout.geometryShift); + } + + const auto drawSmall = (expandRatio < 1.); + const auto drawFull = (expandRatio > 0.); + auto hq = PainterHighQualityEnabler(p); + + const auto count = std::max( + layout.endIndexFull - layout.startIndexFull, + layout.endIndexSmall - layout.startIndexSmall); + + struct Single { + float64 x = 0.; + int indexSmall = 0; + Item *itemSmall = nullptr; + int indexFull = 0; + Item *itemFull = nullptr; + float64 photoTop = 0.; + + explicit operator bool() const { + return itemSmall || itemFull; + } + }; + const auto lookup = [&](int index) { + const auto indexSmall = layout.startIndexSmall + index; + const auto indexFull = layout.startIndexFull + index; + const auto ySmall = photoTopSmall + + ((photoTop - photoTopSmall) + * (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5); + const auto y = elerp(ySmall, photoTop); + + const auto small = (drawSmall + && indexSmall < layout.endIndexSmall + && indexSmall >= layout.smallSkip) + ? &_data.items[indexSmall] + : nullptr; + const auto full = (drawFull && indexFull < layout.endIndexFull) + ? &_data.items[indexFull] + : nullptr; + const auto x = layout.left + layout.single * index; + return Single{ x, indexSmall, small, indexFull, full, y }; + }; + const auto hasUnread = [&](const Single &single) { + return (single.itemSmall && single.itemSmall->element.unreadCount) + || (single.itemFull && single.itemFull->element.unreadCount); + }; + const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { + auto nextGradientPainted = false; + auto skippedPainted = false; + const auto first = layout.smallSkip - layout.startIndexSmall; + for (auto i = count; i != first;) { + --i; + const auto next = (i > 0) ? lookup(i - 1) : Single(); + const auto gradientPainted = nextGradientPainted; + nextGradientPainted = false; + if (const auto current = lookup(i)) { + if (i == first && next && !skippedPainted) { + skippedPainted = true; + paintGradient(next); + paintOther(next); + } + if (!gradientPainted) { + paintGradient(current); + } + if (i > first && hasUnread(current) && next) { + if (current.itemSmall || !next.itemSmall) { + if (i - 1 == first + && first > 0 + && !skippedPainted) { + if (const auto skipped = lookup(i - 2)) { + skippedPainted = true; + paintGradient(skipped); + paintOther(skipped); + } + } + nextGradientPainted = true; + paintGradient(next); + } + } + paintOther(current); + } + } + }; + auto gradient = Ui::UnreadStoryOutlineGradient(); + enumerate([&](Single single) { + // Name. + if (const auto full = single.itemFull) { + validateName(full); + if (expandRatio > 0.) { + p.setOpacity(expandRatio); + p.drawImage(QRectF( + single.x + nameLeft, + nameTop, + nameWidth, + nameHeight + ), full->nameCache); + } + } + + // Unread gradient. + const auto x = single.x; + const auto userpic = QRectF( + x + layout.photoLeft, + single.photoTop, + photo, + photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = (small && small->element.unreadCount); + const auto fullUnreadCount = itemFull + ? itemFull->element.unreadCount + : 0; + const auto unreadOpacity = (smallUnread && fullUnreadCount) + ? 1. + : smallUnread + ? (1. - expandRatio) + : fullUnreadCount + ? expandRatio + : 0.; + if (unreadOpacity > 0.) { + p.setOpacity(unreadOpacity); + const auto outerAdd = 1.5 * line; + const auto outer = userpic.marginsAdded( + { outerAdd, outerAdd, outerAdd, outerAdd }); + gradient.setStart(userpic.topRight()); + gradient.setFinalStop(userpic.bottomLeft()); + if (!fullUnreadCount) { + p.setPen(QPen(gradient, line)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(outer); + } else { + validateSegments(itemFull, gradient, line, true); + Ui::PaintOutlineSegments( + p, + outer, + itemFull->segments, + layout.segmentsSpinProgress); + } + } + p.setOpacity(1.); + }, [&](Single single) { + Expects(single.itemSmall || single.itemFull); + + const auto x = single.x; + const auto userpic = QRectF( + x + layout.photoLeft, + single.photoTop, + photo, + photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = small && small->element.unreadCount; + const auto fullUnreadCount = itemFull + ? itemFull->element.unreadCount + : 0; + const auto fullCount = itemFull ? itemFull->element.count : 0; + + // White circle with possible read gray line. + const auto hasReadLine = (itemFull && fullUnreadCount < fullCount); + p.setOpacity((small && itemFull) + ? 1. + : small + ? (1. - expandRatio) + : expandRatio); + const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.); + const auto rect = userpic.marginsAdded({ add, add, add, add }); + if (layered) { + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setPen(Qt::NoPen); + p.setBrush(st::transparent); + p.drawEllipse(rect); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + if (hasReadLine) { + if (small && !small->element.unreadCount) { + p.setOpacity(expandRatio); + } + validateSegments( + itemFull, + st::dialogsUnreadBgMuted->b, + lineRead, + false); + Ui::PaintOutlineSegments( + p, + rect, + itemFull->segments, + layout.segmentsSpinProgress); + } + + // Userpic. + if (itemFull == small) { + p.setOpacity(smallUnread ? 1. : readUserpicOpacity); + validateThumbnail(itemFull); + const auto size = full.photo; + p.drawImage(userpic, itemFull->element.thumbnail->image(size)); + } else { + if (small) { + p.setOpacity(smallUnread + ? (itemFull ? 1. : (1. - expandRatio)) + : (itemFull + ? _st.readOpacity + : readUserpicAppearingOpacity)); + validateThumbnail(small); + const auto size = (expandRatio > 0.) + ? full.photo + : st.photo; + p.drawImage(userpic, small->element.thumbnail->image(size)); + } + if (itemFull) { + p.setOpacity(expandRatio); + validateThumbnail(itemFull); + const auto size = full.photo; + p.drawImage( + userpic, + itemFull->element.thumbnail->image(size)); + } + } + p.setOpacity(1.); + }); +} + +void List::validateThumbnail(not_null item) { + if (!item->subscribed) { + item->subscribed = true; + //const auto id = item.element.id; + item->element.thumbnail->subscribeToUpdates([=] { + update(); + }); + } +} + +void List::validateSegments( + not_null item, + const QBrush &brush, + float64 line, + bool forUnread) { + const auto count = item->element.count; + const auto unread = item->element.unreadCount; + if (int(item->segments.size()) != count) { + item->segments.resize(count); + } + auto i = 0; + if (forUnread) { + for (; i != count - unread; ++i) { + item->segments[i].width = 0.; + } + for (; i != count; ++i) { + item->segments[i].brush = brush; + item->segments[i].width = line; + } + } else { + for (; i != count - unread; ++i) { + item->segments[i].brush = brush; + item->segments[i].width = line; + } + for (; i != count; ++i) { + item->segments[i].width = 0.; + } + } +} + +void List::validateName(not_null item) { + const auto &element = item->element; + const auto &color = (element.unreadCount || element.skipSmall) + ? st::dialogsNameFg + : st::windowSubTextFg; + if (!item->nameCache.isNull() && item->nameCacheColor == color->c) { + return; + } + const auto &full = _st.full; + const auto &font = full.nameStyle.font; + const auto available = AvailableNameWidth(_st); + const auto text = Ui::Text::String(full.nameStyle, element.name); + const auto ratio = style::DevicePixelRatio(); + item->nameCacheColor = color->c; + item->nameCache = QImage( + QSize(available, font->height) * ratio, + QImage::Format_ARGB32_Premultiplied); + item->nameCache.setDevicePixelRatio(ratio); + item->nameCache.fill(Qt::transparent); + auto p = Painter(&item->nameCache); + p.setPen(color); + text.drawElided(p, 0, 0, available, 1, style::al_top); +} + +void List::wheelEvent(QWheelEvent *e) { + const auto phase = e->phase(); + const auto fullDelta = e->pixelDelta().isNull() + ? e->angleDelta() + : e->pixelDelta(); + if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) { + _scrollingLock = Qt::Orientation(); + if (fullDelta.isNull()) { + return; + } + } + const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y()); + if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) { + _scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal; + } + if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) { + _verticalScrollEvents.fire(e); + return; + } else if (_state == State::Small) { + e->ignore(); + return; + } + const auto delta = vertical + ? fullDelta.y() + : ((style::RightToLeft() ? -1 : 1) * fullDelta.x()); + + const auto now = _scrollLeft; + const auto used = now - delta; + const auto next = std::clamp(used, 0, _scrollLeftMax); + if (next != now) { + requestExpanded(true); + _scrollLeft = next; + updateSelected(); + checkLoadMore(); + update(); + } + e->accept(); +} + +void List::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } else if (_state == State::Small) { + requestExpanded(true); + return; + } else if (_state != State::Full) { + return; + } + _lastMousePosition = e->globalPos(); + updateSelected(); + + _mouseDownPosition = _lastMousePosition; + _pressed = _selected; +} + +void List::mouseMoveEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + updateSelected(); + + if (!_dragging && _mouseDownPosition && _state == State::Full) { + if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() + >= QApplication::startDragDistance()) { + _dragging = true; + _startDraggingLeft = _scrollLeft; + } + } + checkDragging(); +} + +void List::checkDragging() { + if (_dragging) { + const auto sign = (style::RightToLeft() ? -1 : 1); + const auto newLeft = std::clamp( + (sign * (_mouseDownPosition->x() - _lastMousePosition.x()) + + _startDraggingLeft), + 0, + _scrollLeftMax); + if (newLeft != _scrollLeft) { + _scrollLeft = newLeft; + checkLoadMore(); + update(); + } + } +} + +void List::checkLoadMore() { + if (_scrollLeftMax - _scrollLeft < width() * kPreloadPages) { + _loadMoreRequests.fire({}); + } +} + +void List::mouseReleaseEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + const auto guard = gsl::finally([&] { + _mouseDownPosition = std::nullopt; + }); + + const auto pressed = std::exchange(_pressed, -1); + if (finishDragging()) { + return; + } + updateSelected(); + if (_selected == pressed) { + if (!_expanded) { + requestExpanded(true); + } else if (_selected < _data.items.size()) { + _clicks.fire_copy(_data.items[_selected].element.id); + } + } +} + +void List::setExpandedHeight(int height, bool momentum) { + height = std::clamp(height, 0, _st.full.height); + if (_lastExpandedHeight == height) { + return; + } else if (momentum && _expandIgnored) { + return; + } else if (momentum && height > 0 && !_lastExpandedHeight) { + _expandIgnored = true; + return; + } else if (!momentum && _expandIgnored && height > 0) { + _expandIgnored = false; + _expandCatchUpAnimation.start([=] { + update(); + checkForFullState(); + }, 0., 1., kExpandCatchUpDuration); + } else if (!height && _expandCatchUpAnimation.animating()) { + _expandCatchUpAnimation.stop(); + } + _lastExpandedHeight = height; + if (!checkForFullState()) { + setState(!height ? State::Small : State::Changing); + } + update(); +} + +bool List::checkForFullState() { + if (_expandCatchUpAnimation.animating() + || _expandedAnimation.animating() + || _lastExpandedHeight < _st.full.height) { + return false; + } + setState(State::Full); + return true; +} + +void List::setLayoutConstraints( + QPoint positionSmall, + style::align alignSmall, + QRect geometryFull) { + _positionSmall = positionSmall; + _alignSmall = alignSmall; + _geometryFull = geometryFull; + updateGeometry(); + update(); +} + +List::CollapsedGeometry List::collapsedGeometryCurrent() const { + const auto expanded = _expandedAnimation.value(_expanded ? 2. : 0.); + if (expanded >= 1.) { + return { QRect(), 1. }; + } + const auto layout = computeLayout(0.); + const auto small = countSmallGeometry(); + const auto index = layout.smallSkip - layout.startIndexSmall; + const auto shift = x() + layout.geometryShift.x(); + const auto left = int(base::SafeRound( + shift + layout.left + layout.single * index)); + const auto width = small.x() + small.width() - left; + return { + QRect(left, small.y(), width, small.height()), + expanded, + }; +} + +rpl::producer<> List::collapsedGeometryChanged() const { + return _collapsedGeometryChanged.events(); +} + +void List::updateGeometry() { + switch (_state) { + case State::Small: setGeometry(countSmallGeometry()); break; + case State::Changing: { + _changingGeometryFrom = countSmallGeometry(); + setGeometry(_geometryFull.united(_changingGeometryFrom)); + } break; + case State::Full: setGeometry(_geometryFull); + } + update(); +} + +QRect List::countSmallGeometry() const { + const auto &st = _st.small; + const auto layout = computeLayout(0.); + const auto count = layout.endIndexSmall + - std::max(layout.startIndexSmall, layout.smallSkip); + const auto width = st.left + + st.photoLeft + + st.photo + (count - 1) * st.shift + + st.photoLeft + + st.left; + const auto left = ((_alignSmall & Qt::AlignRight) == Qt::AlignRight) + ? (_positionSmall.x() - width) + : ((_alignSmall & Qt::AlignCenter) == Qt::AlignCenter) + ? (_positionSmall.x() - (width / 2)) + : _positionSmall.x(); + return QRect( + left, + _positionSmall.y(), + width, + st.photoTop + st.photo + st.photoTop); +} + +void List::setState(State state) { + if (_state == state) { + return; + } + _state = state; + updateGeometry(); +} + +void List::contextMenuEvent(QContextMenuEvent *e) { + _menu = nullptr; + + if (e->reason() == QContextMenuEvent::Mouse) { + _lastMousePosition = e->globalPos(); + updateSelected(); + } + if (_selected < 0 || _data.empty() || !_expanded) { + return; + } + _menu = base::make_unique_q( + this, + st::popupMenuWithIcons); + _showMenuRequests.fire({ + _data.items[_selected].element.id, + Ui::Menu::CreateAddActionCallback(_menu), + }); + if (_menu->empty()) { + _menu = nullptr; + return; + } + const auto updateAfterMenuDestroyed = [=] { + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + _lastMousePosition = globalPosition; + updateSelected(); + } + }; + QObject::connect( + _menu.get(), + &QObject::destroyed, + crl::guard(&_menuGuard, updateAfterMenuDestroyed)); + _menu->popup(e->globalPos()); + e->accept(); +} + +bool List::finishDragging() { + if (!_dragging) { + return false; + } + checkDragging(); + _dragging = false; + updateSelected(); + return true; +} + +void List::updateSelected() { + if (_pressed >= 0) { + return; + } + const auto &st = _st.small; + const auto p = mapFromGlobal(_lastMousePosition); + const auto layout = computeLayout(); + const auto firstRightFull = layout.leftFull + + (layout.startIndexFull + 1) * layout.singleFull; + const auto secondLeftFull = firstRightFull; + const auto firstRightSmall = layout.leftSmall + + st.photoLeft + + st.photo; + const auto secondLeftSmall = layout.smallSkip + ? (layout.leftSmall + st.photoLeft + st.shift) + : firstRightSmall; + const auto lastRightAddFull = 0; + const auto lastRightAddSmall = st.photoLeft; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * layout.ratio; + }; + const auto firstRight = lerp(firstRightSmall, firstRightFull); + const auto secondLeft = lerp(secondLeftSmall, secondLeftFull); + const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull); + const auto activateFull = (layout.ratio >= 0.5); + const auto startIndex = activateFull + ? layout.startIndexFull + : layout.startIndexSmall; + const auto endIndex = activateFull + ? layout.endIndexFull + : layout.endIndexSmall; + const auto x = p.x(); + const auto infiniteIndex = (x < secondLeft) + ? 0 + : int( + std::floor((std::max(x - firstRight, 0.)) / layout.single) + 1); + const auto index = (endIndex == startIndex) + ? -1 + : (infiniteIndex == endIndex - startIndex + && x < firstRight + + (endIndex - startIndex - 1) * layout.single + + lastRightAdd) + ? (infiniteIndex - 1) // Last small part should still be clickable. + : (startIndex + infiniteIndex >= endIndex) + ? (_st.fullClickable ? (endIndex - 1) : -1) + : infiniteIndex; + const auto selected = (index < 0 + || startIndex + index >= layout.itemsCount) + ? -1 + : (startIndex + index); + if (_selected != selected) { + const auto over = (selected >= 0); + if (over != (_selected >= 0)) { + setCursor(over ? style::cur_pointer : style::cur_default); + } + _selected = selected; + } +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h new file mode 100644 index 000000000..3182d7229 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -0,0 +1,202 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/qt/qt_compare.h" +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/rp_widget.h" + +class QPainter; + +namespace style { +struct DialogsStories; +struct DialogsStoriesList; +} // namespace style + +namespace Ui { +class PopupMenu; +struct OutlineSegment; +} // namespace Ui + +namespace Dialogs::Stories { + +class Thumbnail { +public: + [[nodiscard]] virtual QImage image(int size) = 0; + virtual void subscribeToUpdates(Fn callback) = 0; +}; + +struct Element { + uint64 id = 0; + QString name; + std::shared_ptr thumbnail; + uint32 count : 15 = 0; + uint32 unreadCount : 15 = 0; + uint32 skipSmall : 1 = 0; + + friend inline bool operator==( + const Element &a, + const Element &b) = default; +}; + +struct Content { + std::vector elements; + + friend inline bool operator==( + const Content &a, + const Content &b) = default; +}; + +struct ShowMenuRequest { + uint64 id = 0; + Ui::Menu::MenuCallback callback; +}; + +class List final : public Ui::RpWidget { +public: + List( + not_null parent, + const style::DialogsStoriesList &st, + rpl::producer content); + ~List(); + + void setExpandedHeight(int height, bool momentum = false); + void setLayoutConstraints( + QPoint positionSmall, + style::align alignSmall, + QRect geometryFull = QRect()); + struct CollapsedGeometry { + QRect geometry; + float64 expanded = 0.; + }; + [[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const; + [[nodiscard]] rpl::producer<> collapsedGeometryChanged() const; + + [[nodiscard]] bool empty() const { + return _empty.current(); + } + [[nodiscard]] rpl::producer emptyValue() const { + return _empty.value(); + } + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer showMenuRequests() const; + [[nodiscard]] rpl::producer toggleExpandedRequests() const; + [[nodiscard]] rpl::producer<> entered() const; + [[nodiscard]] rpl::producer<> loadMoreRequests() const; + + [[nodiscard]] auto verticalScrollEvents() const + -> rpl::producer>; + +private: + struct Layout; + enum class State { + Small, + Changing, + Full, + }; + struct Item { + Element element; + QImage nameCache; + QColor nameCacheColor; + std::vector segments; + bool subscribed = false; + }; + struct Data { + std::vector items; + + [[nodiscard]] bool empty() const { + return items.empty(); + } + }; + + void showContent(Content &&content); + void enterEventHook(QEnterEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + void paint( + QPainter &p, + const Layout &layout, + float64 photo, + float64 line, + bool layered); + void ensureLayer(); + void validateThumbnail(not_null item); + void validateName(not_null item); + void updateScrollMax(); + void updateSelected(); + void checkDragging(); + bool finishDragging(); + void checkLoadMore(); + void requestExpanded(bool expanded); + + bool checkForFullState(); + void setState(State state); + void updateGeometry(); + [[nodiscard]] QRect countSmallGeometry() const; + void updateExpanding(int expandingHeight, int expandedHeight); + void validateSegments( + not_null item, + const QBrush &brush, + float64 line, + bool forUnread); + + [[nodiscard]] Layout computeLayout(); + [[nodiscard]] Layout computeLayout(float64 expanded) const; + + const style::DialogsStoriesList &_st; + Content _content; + Data _data; + rpl::event_stream _clicks; + rpl::event_stream _showMenuRequests; + rpl::event_stream _toggleExpandedRequests; + rpl::event_stream<> _entered; + rpl::event_stream<> _loadMoreRequests; + rpl::event_stream<> _collapsedGeometryChanged; + + QImage _layer; + QPoint _positionSmall; + style::align _alignSmall = {}; + QRect _geometryFull; + QRect _changingGeometryFrom; + State _state = State::Small; + rpl::variable _empty = true; + + QPoint _lastMousePosition; + std::optional _mouseDownPosition; + int _startDraggingLeft = 0; + int _scrollLeft = 0; + int _scrollLeftMax = 0; + bool _dragging = false; + Qt::Orientation _scrollingLock = {}; + + Ui::Animations::Simple _expandedAnimation; + Ui::Animations::Simple _expandCatchUpAnimation; + float64 _lastRatio = 0.; + int _lastExpandedHeight = 0; + bool _expandIgnored : 1 = false; + bool _expanded : 1 = false; + + int _selected = -1; + int _pressed = -1; + + rpl::event_stream> _verticalScrollEvents; + + base::unique_qptr _menu; + base::has_weak_ptr _menuGuard; + +}; + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp index cdebd9da4..bfc056243 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp @@ -7,13 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_video_userpic.h" -#include "ui/painter.h" #include "core/file_location.h" #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_file_origin.h" #include "data/data_session.h" +#include "dialogs/ui/dialogs_layout.h" +#include "ui/painter.h" +#include "styles/style_dialogs.h" namespace Dialogs::Ui { @@ -129,6 +131,29 @@ void VideoUserpic::clipCallback(Media::Clip::Notification notification) { } } +void PaintUserpic( + Painter &p, + not_null entry, + PeerData *peer, + VideoUserpic *videoUserpic, + PeerUserpicView &view, + const Ui::PaintContext &context) { + if (peer) { + PaintUserpic( + p, + peer, + videoUserpic, + view, + context.st->padding.left(), + context.st->padding.top(), + context.width, + context.st->photoSize, + context.paused); + } else { + entry->paintUserpic(p, view, context); + } +} + void PaintUserpic( Painter &p, not_null peer, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h index 6c285ce97..cc06a008c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h @@ -19,10 +19,16 @@ namespace Ui { struct PeerUserpicView; } // namespace Ui +namespace Dialogs { +class Entry; +} // namespace Dialogs + namespace Dialogs::Ui { using namespace ::Ui; +struct PaintContext; + class VideoUserpic final { public: VideoUserpic(not_null peer, Fn repaint); @@ -54,6 +60,14 @@ private: }; +void PaintUserpic( + Painter &p, + not_null entry, + PeerData *peer, + VideoUserpic *videoUserpic, + PeerUserpicView &view, + const Ui::PaintContext &context); + void PaintUserpic( Painter &p, not_null peer, diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp index 66fc0f997..2c0482c33 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp @@ -12,21 +12,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" // Window::GifPauseReason #include "styles/style_chat_helpers.h" +#include "styles/style_media_view.h" namespace Editor { StickersPanelController::StickersPanelController( not_null panelContainer, - not_null controller) + std::shared_ptr show) : _stickersPanel( base::make_unique_q( panelContainer, - controller, - object_ptr( - nullptr, - controller->uiShow(), - Window::GifPauseReason::Layer, - ChatHelpers::TabbedSelector::Mode::MediaEditor))) { + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = show, + .st = st::storiesComposeControls.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::MediaEditor, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + })) { _stickersPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h index 5404a8adb..9514ea23b 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h @@ -11,16 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { class RpWidget; } // namespace Ui -namespace Window { -class SessionController; -} // namespace Window - namespace Editor { class StickersPanelController final { @@ -34,7 +31,7 @@ public: StickersPanelController( not_null panelContainer, - not_null controller); + std::shared_ptr show); [[nodiscard]] auto stickerChosen() const -> rpl::producer>; diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index a11fe9760..801e12258 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -52,16 +52,34 @@ PhotoEditor::PhotoEditor( std::shared_ptr photo, PhotoModifications modifications, EditorData data) +: PhotoEditor( + parent, + controller->uiShow(), + (controller->sessionController() + ? controller->sessionController()->uiShow() + : nullptr), + std::move(photo), + std::move(modifications), + std::move(data)) { +} + +PhotoEditor::PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data) : RpWidget(parent) , _modifications(std::move(modifications)) , _controllers(std::make_shared( - controller->sessionController() + sessionShow ? std::make_unique( this, - controller->sessionController()) + std::move(sessionShow)) : nullptr, std::make_unique(), - controller->uiShow())) + std::move(show))) , _content(base::make_unique_q( this, photo, diff --git a/Telegram/SourceFiles/editor/photo_editor.h b/Telegram/SourceFiles/editor/photo_editor.h index e226b1ddb..4169339a6 100644 --- a/Telegram/SourceFiles/editor/photo_editor.h +++ b/Telegram/SourceFiles/editor/photo_editor.h @@ -15,8 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class LayerWidget; +class Show; } // namespace Ui +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Window { class Controller; } // namespace Window @@ -36,6 +41,13 @@ public: std::shared_ptr photo, PhotoModifications modifications, EditorData data = EditorData()); + PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data = EditorData()); void save(); [[nodiscard]] rpl::producer doneRequests() const; diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp index c58fb1cc0..daf93c08d 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp @@ -22,7 +22,7 @@ namespace Editor { void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback) { @@ -56,13 +56,14 @@ void OpenWithPreparedFile( const auto fileImage = std::make_shared(std::move(copy)); auto editor = base::make_unique_q( parent, - &controller->window(), + show, + show, fileImage, image->modifications); const auto raw = editor.get(); auto layer = std::make_unique(parent, std::move(editor)); InitEditorLayer(layer.get(), raw, std::move(callback)); - controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); + show->showLayer(std::move(layer), Ui::LayerOption::KeepOther); } void PrepareProfilePhoto( diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h index d7fd19a75..f1c2fd445 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h @@ -6,13 +6,13 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -// -//#include "ui/image/image.h" -//#include "editor/photo_editor_common.h" -//#include "base/unique_qptr.h" enum class ImageRoundRadius; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class RpWidget; struct PreparedFile; @@ -31,7 +31,7 @@ struct EditorData; void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index b0bd24268..6b9fc2997 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -42,6 +42,16 @@ QString PreparePhotoFileName(int index, TimeId date) { + ".jpg"; } +QString PrepareStoryFileName( + int index, + TimeId date, + const Utf8String &extension) { + return "story_" + + QString::number(index) + + PrepareFileNameDatePart(date) + + extension; +} + } // namespace int PeerColorIndex(BareId bareId) { @@ -295,7 +305,7 @@ void ParseAttributes( } result.width = data.vw().v; result.height = data.vh().v; - result.duration = data.vduration().v; + result.duration = int(data.vduration().v); }, [&](const MTPDdocumentAttributeAudio &data) { if (data.is_voice()) { result.isVoiceMessage = true; @@ -584,6 +594,98 @@ UserpicsSlice ParseUserpicsSlice( return result; } +File &Story::file() { + return media.file(); +} + +const File &Story::file() const { + return media.file(); +} + +Image &Story::thumb() { + return media.thumb(); +} + +const Image &Story::thumb() const { + return media.thumb(); +} + +StoriesSlice ParseStoriesSlice( + const MTPVector &data, + int baseIndex) { + const auto &list = data.v; + auto result = StoriesSlice(); + result.list.reserve(list.size()); + for (const auto &story : list) { + result.lastId = story.match([](const auto &data) { + return data.vid().v; + }); + ++result.skipped; + story.match([&](const MTPDstoryItem &data) { + const auto date = data.vdate().v; + auto media = Media(); + data.vmedia().match([&](const MTPDmessageMediaPhoto &data) { + const auto suggestedPath = "stories/" + + PrepareStoryFileName( + ++baseIndex, + date, + ".jpg"_q); + const auto photo = data.vphoto(); + auto content = photo + ? ParsePhoto(*photo, suggestedPath) + : Photo(); + media.content = content; + }, [&](const MTPDmessageMediaDocument &data) { + const auto document = data.vdocument(); + auto fake = ParseMediaContext(); + auto content = document + ? ParseDocument(fake, *document, "stories", date) + : Document(); + const auto extension = (content.mime == "image/jpeg") + ? ".jpg"_q + : (content.mime == "image/png") + ? ".png"_q + : [&] { + const auto mimeType = Core::MimeTypeForName( + content.mime); + QStringList patterns = mimeType.globPatterns(); + if (!patterns.isEmpty()) { + return patterns.front().replace( + '*', + QString()).toUtf8(); + } + return QByteArray(); + }(); + const auto path = content.file.suggestedPath = "stories/" + + PrepareStoryFileName( + ++baseIndex, + date, + extension); + content.thumb.file.suggestedPath = path + "_thumb.jpg"; + media.content = content; + }, [&](const auto &data) { + media.content = UnsupportedMedia(); + }); + if (!v::is(media.content)) { + result.list.push_back(Story{ + .id = data.vid().v, + .date = date, + .expires = data.vexpire_date().v, + .media = std::move(media), + .pinned = data.is_pinned(), + .caption = (data.vcaption() + ? ParseText( + *data.vcaption(), + data.ventities().value_or_empty()) + : std::vector()), + }); + --result.skipped; + } + }, [](const auto &) {}); + } + return result; +} + std::pair WriteImageThumb( const QString &basePath, const QString &largePath, @@ -954,6 +1056,8 @@ Media ParseMedia( result.content = ParsePoll(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice + }, [](const MTPDmessageMediaStory &data) { + // #TODO stories export }, [](const MTPDmessageMediaEmpty &data) {}); return result; } @@ -1260,6 +1364,8 @@ Message ParseMessage( if (result.replyToPeerId == result.peerId) { result.replyToPeerId = 0; } + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } } @@ -1307,6 +1413,8 @@ Message ParseMessage( result.replyToPeerId = data.vreply_to_peer_id() ? ParsePeerId(*data.vreply_to_peer_id()) : PeerId(0); + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } if (const auto viaBotId = data.vvia_bot_id()) { diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index d383fdb94..27fb14ea1 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -48,6 +48,10 @@ struct UserpicsInfo { int count = 0; }; +struct StoriesInfo { + int count = 0; +}; + struct FileLocation { int dcId = 0; MTPInputFileLocation data; @@ -663,9 +667,34 @@ struct FileOrigin { int split = 0; MTPInputPeer peer; int32 messageId = 0; + int32 storyId = 0; uint64 customEmojiId = 0; }; +struct Story { + int32 id = 0; + TimeId date = 0; + TimeId expires = 0; + Media media; + bool pinned = false; + std::vector caption; + + File &file(); + const File &file() const; + Image &thumb(); + const Image &thumb() const; +}; + +struct StoriesSlice { + std::vector list; + int32 lastId = 0; + int skipped = 0; +}; + +StoriesSlice ParseStoriesSlice( + const MTPVector &data, + int baseIndex); + Message ParseMessage( ParseMediaContext &context, const MTPMessage &data, diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 3e31914d4..1ff8d6774 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -30,6 +30,7 @@ constexpr auto kTopPeerSliceLimit = 100; constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024); constexpr auto kLocationCacheSize = 100'000; constexpr auto kMaxEmojiPerRequest = 100; +constexpr auto kStoriesSliceLimit = 100; struct LocationKey { uint64 type; @@ -109,6 +110,7 @@ struct ApiWrap::StartProcess { enum class Step { UserpicsCount, + StoriesCount, SplitRanges, DialogsCount, LeftChannelsCount, @@ -139,6 +141,19 @@ struct ApiWrap::UserpicsProcess { int fileIndex = 0; }; +struct ApiWrap::StoriesProcess { + FnMut start; + Fn fileProgress; + Fn handleSlice; + FnMut finish; + + int processed = 0; + std::optional slice; + int offsetId = 0; + bool lastSlice = false; + int fileIndex = 0; +}; + struct ApiWrap::OtherDataProcess { Data::File file; FnMut done; @@ -417,6 +432,9 @@ void ApiWrap::startExport( if (_settings->types & Settings::Type::Userpics) { _startProcess->steps.push_back(Step::UserpicsCount); } + if (_settings->types & Settings::Type::Stories) { + _startProcess->steps.push_back(Step::StoriesCount); + } if (_settings->types & Settings::Type::AnyChatsMask) { _startProcess->steps.push_back(Step::SplitRanges); } @@ -447,6 +465,8 @@ void ApiWrap::sendNextStartRequest() { switch (step) { case Step::UserpicsCount: return requestUserpicsCount(); + case Step::StoriesCount: + return requestStoriesCount(); case Step::SplitRanges: return requestSplitRanges(); case Step::DialogsCount: @@ -480,6 +500,22 @@ void ApiWrap::requestUserpicsCount() { }).send(); } +void ApiWrap::requestStoriesCount() { + Expects(_startProcess != nullptr); + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(0), // offset_id + MTP_int(0) // limit + )).done([=](const MTPstories_Stories &result) { + Expects(_settings != nullptr); + Expects(_startProcess != nullptr); + + _startProcess->info.storiesCount = result.data().vcount().v; + + sendNextStartRequest(); + }).send(); +} + void ApiWrap::requestSplitRanges() { Expects(_startProcess != nullptr); @@ -616,7 +652,8 @@ void ApiWrap::startMainSession(FnMut done) { using Type = Settings::Type; const auto sizeLimit = _settings->media.sizeLimit; const auto hasFiles = ((_settings->media.types != 0) && (sizeLimit > 0)) - || (_settings->types & Type::Userpics); + || (_settings->types & Type::Userpics) + || (_settings->types & Type::Stories); using Flag = MTPaccount_InitTakeoutSession::Flag; const auto flags = Flag(0) @@ -856,6 +893,171 @@ void ApiWrap::finishUserpics() { base::take(_userpicsProcess)->finish(); } +void ApiWrap::requestStories( + FnMut start, + Fn progress, + Fn slice, + FnMut finish) { + Expects(_storiesProcess == nullptr); + + _storiesProcess = std::make_unique(); + _storiesProcess->start = std::move(start); + _storiesProcess->fileProgress = std::move(progress); + _storiesProcess->handleSlice = std::move(slice); + _storiesProcess->finish = std::move(finish); + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(_storiesProcess->offsetId), + MTP_int(kStoriesSliceLimit) + )).done([=](const MTPstories_Stories &result) mutable { + Expects(_storiesProcess != nullptr); + + auto startInfo = Data::StoriesInfo{ result.data().vcount().v }; + if (!_storiesProcess->start(std::move(startInfo))) { + return; + } + + handleStoriesSlice(result); + }).send(); +} + +void ApiWrap::handleStoriesSlice(const MTPstories_Stories &result) { + Expects(_storiesProcess != nullptr); + + loadStoriesFiles(Data::ParseStoriesSlice( + result.data().vstories(), + _storiesProcess->processed)); +} + +void ApiWrap::loadStoriesFiles(Data::StoriesSlice &&slice) { + Expects(_storiesProcess != nullptr); + Expects(!_storiesProcess->slice.has_value()); + + if (!slice.lastId) { + _storiesProcess->lastSlice = true; + } + _storiesProcess->slice = std::move(slice); + _storiesProcess->fileIndex = 0; + loadNextStory(); +} + +void ApiWrap::loadNextStory() { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + + for (auto &list = _storiesProcess->slice->list + ; _storiesProcess->fileIndex < list.size() + ; ++_storiesProcess->fileIndex) { + auto &story = list[_storiesProcess->fileIndex]; + const auto origin = Data::FileOrigin{ .storyId = story.id }; + const auto ready = processFileLoad( + story.file(), + origin, + [=](FileProgress value) { return loadStoryProgress(value); }, + [=](const QString &path) { loadStoryDone(path); }); + if (!ready) { + return; + } + const auto thumbProgress = [=](FileProgress value) { + return loadStoryThumbProgress(value); + }; + const auto thumbReady = processFileLoad( + story.thumb().file, + origin, + thumbProgress, + [=](const QString &path) { loadStoryThumbDone(path); }, + nullptr, + &story); + if (!thumbReady) { + return; + } + } + finishStoriesSlice(); +} + +void ApiWrap::finishStoriesSlice() { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + + auto slice = *base::take(_storiesProcess->slice); + if (slice.lastId) { + _storiesProcess->processed += slice.list.size(); + _storiesProcess->offsetId = slice.lastId; + if (!_storiesProcess->handleSlice(std::move(slice))) { + return; + } + } + if (_storiesProcess->lastSlice) { + finishStories(); + return; + } + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(_storiesProcess->offsetId), + MTP_int(kStoriesSliceLimit) + )).done([=](const MTPstories_Stories &result) { + handleStoriesSlice(result); + }).send(); +} + +bool ApiWrap::loadStoryProgress(FileProgress progress) { + Expects(_fileProcess != nullptr); + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + return _storiesProcess->fileProgress(DownloadProgress{ + _fileProcess->randomId, + _fileProcess->relativePath, + _storiesProcess->fileIndex, + progress.ready, + progress.total }); +} + +void ApiWrap::loadStoryDone(const QString &relativePath) { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + const auto index = _storiesProcess->fileIndex; + auto &file = _storiesProcess->slice->list[index].file(); + file.relativePath = relativePath; + if (relativePath.isEmpty()) { + file.skipReason = Data::File::SkipReason::Unavailable; + } + loadNextStory(); +} + +bool ApiWrap::loadStoryThumbProgress(FileProgress progress) { + return loadStoryProgress(progress); +} + +void ApiWrap::loadStoryThumbDone(const QString &relativePath) { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + const auto index = _storiesProcess->fileIndex; + auto &file = _storiesProcess->slice->list[index].thumb().file; + file.relativePath = relativePath; + if (relativePath.isEmpty()) { + file.skipReason = Data::File::SkipReason::Unavailable; + } + loadNextStory(); +} + +void ApiWrap::finishStories() { + Expects(_storiesProcess != nullptr); + + base::take(_storiesProcess)->finish(); +} + void ApiWrap::requestContacts(FnMut done) { Expects(_contactsProcess == nullptr); @@ -1753,7 +1955,8 @@ bool ApiWrap::processFileLoad( const Data::FileOrigin &origin, Fn progress, FnMut done, - Data::Message *message) { + Data::Message *message, + Data::Story *story) { using SkipReason = Data::File::SkipReason; if (!file.relativePath.isEmpty() @@ -1767,7 +1970,12 @@ bool ApiWrap::processFileLoad( } using Type = MediaSettings::Type; - const auto type = message ? v::match(message->media.content, [&]( + const auto media = message + ? &message->media + : story + ? &story->media + : nullptr; + const auto type = media ? v::match(media->content, [&]( const Data::Document &data) { if (data.isSticker) { return Type::Sticker; @@ -1786,14 +1994,18 @@ bool ApiWrap::processFileLoad( return Type::Photo; }) : Type(0); - const auto limit = _settings->media.sizeLimit; + const auto fullSize = message + ? message->file().size + : story + ? story->file().size + : file.size; if (message && Data::SkipMessageByDate(*message, *_settings)) { file.skipReason = SkipReason::DateLimits; return true; - } else if ((_settings->media.types & type) != type) { + } else if (!story && (_settings->media.types & type) != type) { file.skipReason = SkipReason::FileType; return true; - } else if ((message ? message->file().size : file.size) >= limit) { + } else if (!story && fullSize >= _settings->media.sizeLimit) { // Don't load thumbs for large files that we skip. file.skipReason = SkipReason::FileSize; return true; @@ -1972,7 +2184,20 @@ void ApiWrap::filePartRefreshReference(int64 offset) { Expects(_fileProcess->requestId == 0); const auto &origin = _fileProcess->origin; - if (!origin.messageId) { + if (origin.storyId) { + _fileProcess->requestId = mainRequest(MTPstories_GetStoriesByID( + MTP_inputUserSelf(), + MTP_vector(1, MTP_int(origin.storyId)) + )).fail([=](const MTP::Error &error) { + _fileProcess->requestId = 0; + filePartUnavailable(); + return true; + }).done([=](const MTPstories_Stories &result) { + _fileProcess->requestId = 0; + filePartExtractReference(offset, result); + }).send(); + return; + } else if (!origin.messageId) { error("FILE_REFERENCE error for non-message file."); return; } @@ -2061,6 +2286,38 @@ void ApiWrap::filePartExtractReference( }); } +void ApiWrap::filePartExtractReference( + int64 offset, + const MTPstories_Stories &result) { + Expects(_fileProcess != nullptr); + Expects(_fileProcess->requestId == 0); + + const auto stories = Data::ParseStoriesSlice( + result.data().vstories(), + 0); + for (const auto &story : stories.list) { + if (story.id == _fileProcess->origin.storyId) { + const auto refresh1 = Data::RefreshFileReference( + _fileProcess->location, + story.file().location); + const auto refresh2 = Data::RefreshFileReference( + _fileProcess->location, + story.thumb().file.location); + if (refresh1 || refresh2) { + _fileProcess->requestId = fileRequest( + _fileProcess->location, + offset + ).done([=](const MTPupload_File &result) { + _fileProcess->requestId = 0; + filePartDone(offset, result); + }).send(); + return; + } + } + } + filePartUnavailable(); +} + void ApiWrap::filePartUnavailable() { Expects(_fileProcess != nullptr); Expects(!_fileProcess->requests.empty()); diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index 6384457d7..4723cd882 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -19,12 +19,15 @@ struct FileLocation; struct PersonalInfo; struct UserpicsInfo; struct UserpicsSlice; +struct StoriesInfo; +struct StoriesSlice; struct ContactsList; struct SessionsList; struct DialogsInfo; struct DialogInfo; struct MessagesSlice; struct Message; +struct Story; struct FileOrigin; } // namespace Data @@ -44,6 +47,7 @@ public: struct StartInfo { int userpicsCount = 0; + int storiesCount = 0; int dialogsCount = 0; }; void startExport( @@ -74,6 +78,12 @@ public: Fn slice, FnMut finish); + void requestStories( + FnMut start, + Fn progress, + Fn slice, + FnMut finish); + void requestContacts(FnMut done); void requestSessions(FnMut done); @@ -96,6 +106,7 @@ private: struct StartProcess; struct ContactsProcess; struct UserpicsProcess; + struct StoriesProcess; struct OtherDataProcess; struct FileProcess; struct FileProgress; @@ -107,6 +118,7 @@ private: void startMainSession(FnMut done); void sendNextStartRequest(); void requestUserpicsCount(); + void requestStoriesCount(); void requestSplitRanges(); void requestDialogsCount(); void requestLeftChannelsCount(); @@ -122,6 +134,16 @@ private: void finishUserpicsSlice(); void finishUserpics(); + void handleStoriesSlice(const MTPstories_Stories &result); + void loadStoriesFiles(Data::StoriesSlice &&slice); + void loadNextStory(); + bool loadStoryProgress(FileProgress value); + void loadStoryDone(const QString &relativePath); + bool loadStoryThumbProgress(FileProgress value); + void loadStoryThumbDone(const QString &relativePath); + void finishStoriesSlice(); + void finishStories(); + void otherDataDone(const QString &relativePath); bool useOnlyLastSplit() const; @@ -179,7 +201,8 @@ private: const Data::FileOrigin &origin, Fn progress, FnMut done, - Data::Message *message = nullptr); + Data::Message *message = nullptr, + Data::Story *story = nullptr); std::unique_ptr prepareFileProcess( const Data::File &file, const Data::FileOrigin &origin) const; @@ -198,6 +221,9 @@ private: void filePartExtractReference( int64 offset, const MTPmessages_Messages &result); + void filePartExtractReference( + int64 offset, + const MTPstories_Stories &result); template class RequestBuilder; @@ -228,6 +254,7 @@ private: std::unique_ptr _fileCache; std::unique_ptr _contactsProcess; std::unique_ptr _userpicsProcess; + std::unique_ptr _storiesProcess; std::unique_ptr _otherDataProcess; std::unique_ptr _fileProcess; std::unique_ptr _leftChannelsProcess; diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index 516c75cf7..b3d543f4c 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -75,6 +75,7 @@ private: void collectDialogsList(); void exportPersonalInfo(); void exportUserpics(); + void exportStories(); void exportContacts(); void exportSessions(); void exportOtherData(); @@ -89,6 +90,7 @@ private: ProcessingState stateDialogsList(int processed) const; ProcessingState statePersonalInfo() const; ProcessingState stateUserpics(const DownloadProgress &progress) const; + ProcessingState stateStories(const DownloadProgress &progress) const; ProcessingState stateContacts() const; ProcessingState stateSessions() const; ProcessingState stateOtherData() const; @@ -114,6 +116,9 @@ private: int _userpicsWritten = 0; int _userpicsCount = 0; + int _storiesWritten = 0; + int _storiesCount = 0; + // rpl::variable fails to compile in MSVC :( State _state; rpl::event_stream _stateChanges; @@ -273,6 +278,9 @@ void ControllerObject::fillExportSteps() { if (_settings.types & Type::Userpics) { _steps.push_back(Step::Userpics); } + if (_settings.types & Type::Stories) { + _steps.push_back(Step::Stories); + } if (_settings.types & Type::Contacts) { _steps.push_back(Step::Contacts); } @@ -306,6 +314,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) { if (_settings.types & Settings::Type::Userpics) { push(Step::Userpics, 1); } + if (_settings.types & Settings::Type::Stories) { + push(Step::Stories, 1); + } if (_settings.types & Settings::Type::Contacts) { push(Step::Contacts, 1); } @@ -344,6 +355,7 @@ void ControllerObject::exportNext() { case Step::DialogsList: return collectDialogsList(); case Step::PersonalInfo: return exportPersonalInfo(); case Step::Userpics: return exportUserpics(); + case Step::Stories: return exportStories(); case Step::Contacts: return exportContacts(); case Step::Sessions: return exportSessions(); case Step::OtherData: return exportOtherData(); @@ -416,6 +428,32 @@ void ControllerObject::exportUserpics() { }); } +void ControllerObject::exportStories() { + _api.requestStories([=](Data::StoriesInfo &&start) { + if (ioCatchError(_writer->writeStoriesStart(start))) { + return false; + } + _storiesWritten = 0; + _storiesCount = start.count; + return true; + }, [=](DownloadProgress progress) { + setState(stateStories(progress)); + return true; + }, [=](Data::StoriesSlice &&slice) { + if (ioCatchError(_writer->writeStoriesSlice(slice))) { + return false; + } + _storiesWritten += slice.list.size(); + setState(stateStories(DownloadProgress())); + return true; + }, [=] { + if (ioCatchError(_writer->writeStoriesEnd())) { + return; + } + exportNext(); + }); +} + void ControllerObject::exportContacts() { setState(stateContacts()); _api.requestContacts([=](Data::ContactsList &&result) { @@ -533,7 +571,21 @@ ProcessingState ControllerObject::stateUserpics( return prepareState(Step::Userpics, [&](ProcessingState &result) { result.entityIndex = _userpicsWritten + progress.itemIndex; result.entityCount = std::max(_userpicsCount, result.entityIndex); - result.bytesType = ProcessingState::FileType::Photo; + result.bytesRandomId = progress.randomId; + if (!progress.path.isEmpty()) { + const auto last = progress.path.lastIndexOf('/'); + result.bytesName = progress.path.mid(last + 1); + } + result.bytesLoaded = progress.ready; + result.bytesCount = progress.total; + }); +} + +ProcessingState ControllerObject::stateStories( + const DownloadProgress &progress) const { + return prepareState(Step::Stories, [&](ProcessingState &result) { + result.entityIndex = _storiesWritten + progress.itemIndex; + result.entityCount = std::max(_storiesCount, result.entityIndex); result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); @@ -586,7 +638,6 @@ void ControllerObject::fillMessagesState( : ProcessingState::EntityType::Chat; result.itemIndex = _messagesWritten + progress.itemIndex; result.itemCount = std::max(_messagesCount, result.itemIndex); - result.bytesType = ProcessingState::FileType::File; // TODO result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h index e9b08acdc..fae4b54a1 100644 --- a/Telegram/SourceFiles/export/export_controller.h +++ b/Telegram/SourceFiles/export/export_controller.h @@ -38,21 +38,12 @@ struct ProcessingState { DialogsList, PersonalInfo, Userpics, + Stories, Contacts, Sessions, OtherData, Dialogs, }; - enum class FileType { - None, - Photo, - Video, - VoiceMessage, - VideoMessage, - Sticker, - GIF, - File, - }; enum class EntityType { Chat, SavedMessages, @@ -75,7 +66,6 @@ struct ProcessingState { int itemCount = 0; uint64 bytesRandomId = 0; - FileType bytesType = FileType::None; QString bytesName; int64 bytesLoaded = 0; int64 bytesCount = 0; diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h index beac47760..be2e1b782 100644 --- a/Telegram/SourceFiles/export/export_settings.h +++ b/Telegram/SourceFiles/export/export_settings.h @@ -57,13 +57,18 @@ struct Settings { PublicGroups = 0x100, PrivateChannels = 0x200, PublicChannels = 0x400, + Stories = 0x800, GroupsMask = PrivateGroups | PublicGroups, ChannelsMask = PrivateChannels | PublicChannels, GroupsChannelsMask = GroupsMask | ChannelsMask, NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups, AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask, - NonChatsMask = PersonalInfo | Userpics | Contacts | Sessions, + NonChatsMask = (PersonalInfo + | Userpics + | Contacts + | Stories + | Sessions), AllMask = NonChatsMask | OtherData | AnyChatsMask, }; using Types = base::flags; @@ -91,6 +96,7 @@ struct Settings { return Type::PersonalInfo | Type::Userpics | Type::Contacts + | Type::Stories | Type::PersonalChats | Type::PrivateGroups; } diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h index b98f65422..7a2bb65ea 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.h +++ b/Telegram/SourceFiles/export/output/export_output_abstract.h @@ -14,6 +14,8 @@ namespace Data { struct PersonalInfo; struct UserpicsInfo; struct UserpicsSlice; +struct StoriesInfo; +struct StoriesSlice; struct ContactsList; struct SessionsList; struct DialogsInfo; @@ -55,6 +57,12 @@ public: const Data::UserpicsSlice &data) = 0; [[nodiscard]] virtual Result writeUserpicsEnd() = 0; + [[nodiscard]] virtual Result writeStoriesStart( + const Data::StoriesInfo &data) = 0; + [[nodiscard]] virtual Result writeStoriesSlice( + const Data::StoriesSlice &data) = 0; + [[nodiscard]] virtual Result writeStoriesEnd() = 0; + [[nodiscard]] virtual Result writeContactsList( const Data::ContactsList &data) = 0; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 26fdc24f2..b45f37621 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -35,11 +35,23 @@ constexpr auto kStickerMaxWidth = 384; constexpr auto kStickerMaxHeight = 384; constexpr auto kStickerMinWidth = 80; constexpr auto kStickerMinHeight = 80; +constexpr auto kStoryThumbWidth = 45; +constexpr auto kStoryThumbHeight = 80; + +constexpr auto kChatsPriority = 0; +constexpr auto kContactsPriority = 2; +constexpr auto kFrequentContactsPriority = 3; +constexpr auto kUserpicsPriority = 4; +constexpr auto kStoriesPriority = 5; +constexpr auto kSessionsPriority = 6; +constexpr auto kWebSessionsPriority = 7; +constexpr auto kOtherPriority = 8; const auto kLineBreak = QByteArrayLiteral("
"); using Context = details::HtmlContext; using UserpicData = details::UserpicData; +using StoryData = details::StoryData; using PeersMap = details::PeersMap; using MediaData = details::MediaData; @@ -347,6 +359,11 @@ struct UserpicData { QByteArray lastName; }; +struct StoryData { + QString imageLink; + QString largeLink; +}; + class PeersMap { public: using Peer = Data::Peer; @@ -503,6 +520,14 @@ public: const QByteArray &details, const QByteArray &info, const QString &link = QString()); + [[nodiscard]] QByteArray pushStoriesListEntry( + const StoryData &story, + const QByteArray &name, + const QByteArrayList &details, + const QByteArray &info, + const std::vector &caption, + const QString &internalLinksDomain, + const QString &link = QString()); [[nodiscard]] QByteArray pushSessionListEntry( int apiId, const QByteArray &name, @@ -750,6 +775,75 @@ QByteArray HtmlWriter::Wrap::pushListEntry( info); } +QByteArray HtmlWriter::Wrap::pushStoriesListEntry( + const StoryData &story, + const QByteArray &name, + const QByteArrayList &details, + const QByteArray &info, + const std::vector &caption, + const QString &internalLinksDomain, + const QString &link) { + auto result = pushDiv("entry clearfix"); + if (!link.isEmpty()) { + result.append(pushTag("a", { + { "class", "pull_left userpic_wrap" }, + { "href", relativePath(link).toUtf8() + "#allow_back" }, + })); + } else { + result.append(pushDiv("pull_left userpic_wrap")); + } + if (!story.imageLink.isEmpty()) { + const auto sizeStyle = "width: " + + Data::NumberToString(kStoryThumbWidth) + + "px; height: " + + Data::NumberToString(kStoryThumbHeight) + + "px"; + result.append(pushTag("img", { + { "class", "story" }, + { "style", sizeStyle }, + { "src", relativePath(story.imageLink).toUtf8() }, + { "empty", "" } + })); + } + result.append(popTag()); + result.append(pushDiv("body")); + if (!info.isEmpty()) { + result.append(pushDiv("pull_right info details")); + result.append(SerializeString(info)); + result.append(popTag()); + } + if (!name.isEmpty()) { + if (!link.isEmpty()) { + result.append(pushTag("a", { + { "class", "block_link expanded" }, + { "href", relativePath(link).toUtf8() + "#allow_back" }, + })); + } + result.append(pushDiv("name bold")); + result.append(SerializeString(name)); + result.append(popTag()); + if (!link.isEmpty()) { + result.append(popTag()); + } + } + const auto text = caption.empty() + ? QByteArray() + : FormatText(caption, internalLinksDomain, _base); + if (!text.isEmpty()) { + result.append(pushDiv("text")); + result.append(text); + result.append(popTag()); + } + for (const auto &detail : details) { + result.append(pushDiv("details_entry details")); + result.append(SerializeString(detail)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + return result; +} + QByteArray HtmlWriter::Wrap::pushSessionListEntry( int apiId, const QByteArray &name, @@ -1980,6 +2074,7 @@ Result HtmlWriter::start( "images/section_other.png", "images/section_photos.png", "images/section_sessions.png", + "images/section_stories.png", "images/section_web.png", "js/script.js", }; @@ -2176,13 +2271,114 @@ QString HtmlWriter::userpicsFilePath() const { void HtmlWriter::pushUserpicsSection() { pushSection( - 4, + kUserpicsPriority, "Profile pictures", "photos", _userpicsCount, userpicsFilePath()); } +Result HtmlWriter::writeStoriesStart(const Data::StoriesInfo &data) { + Expects(_summary != nullptr); + Expects(_stories == nullptr); + + _storiesCount = data.count; + if (!_storiesCount) { + return Result::Success(); + } + _stories = fileWithRelativePath(storiesFilePath()); + + auto block = _stories->pushHeader( + "Stories archive", + mainFileRelativePath()); + block.append(_stories->pushDiv("page_body list_page")); + block.append(_stories->pushDiv("entry_list")); + if (const auto result = _stories->writeBlock(block); !result) { + return result; + } + return Result::Success(); +} + +Result HtmlWriter::writeStoriesSlice(const Data::StoriesSlice &data) { + Expects(_stories != nullptr); + + _storiesCount -= data.skipped; + if (data.list.empty()) { + return Result::Success(); + } + auto block = QByteArray(); + for (const auto &story : data.list) { + auto data = StoryData{}; + using SkipReason = Data::File::SkipReason; + const auto &file = story.file(); + Assert(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + auto status = QByteArrayList(); + if (story.pinned) { + status.append("Saved to Profile"); + } + if (story.expires > 0) { + status.append("Expiring: " + Data::FormatDateTime(story.expires)); + } + status.append([&]() -> Data::Utf8String { + switch (file.skipReason) { + case SkipReason::Unavailable: + return "(Story unavailable, please try again later)"; + case SkipReason::FileSize: + return "(Story exceeds maximum size. " + "Change data exporting settings to download.)"; + case SkipReason::FileType: + return "(Story not included. " + "Change data exporting settings to download.)"; + case SkipReason::None: return Data::FormatFileSize(file.size); + } + Unexpected("Skip reason while writing story path."); + }()); + const auto &path = story.file().relativePath; + const auto &image = story.thumb().file.relativePath.isEmpty() + ? story.file().relativePath + : story.thumb().file.relativePath; + data.imageLink = Data::WriteImageThumb( + _settings.path, + image, + kStoryThumbWidth * 2, + kStoryThumbHeight * 2); + const auto info = (story.date > 0) + ? Data::FormatDateTime(story.date) + : QByteArray(); + block.append(_stories->pushStoriesListEntry( + data, + (path.isEmpty() ? QString("Story unavailable") : path).toUtf8(), + status, + info, + story.caption, + _environment.internalLinksDomain, + path)); + } + return _stories->writeBlock(block); +} + +Result HtmlWriter::writeStoriesEnd() { + pushStoriesSection(); + if (_stories) { + return base::take(_stories)->close(); + } + return Result::Success(); +} + +QString HtmlWriter::storiesFilePath() const { + return "lists/stories.html"; +} + +void HtmlWriter::pushStoriesSection() { + pushSection( + kStoriesPriority, + "Stories archive", + "stories", + _storiesCount, + storiesFilePath()); +} + Result HtmlWriter::writeContactsList(const Data::ContactsList &data) { Expects(_summary != nullptr); @@ -2228,7 +2424,7 @@ Result HtmlWriter::writeSavedContacts(const Data::ContactsList &data) { } pushSection( - 2, + kContactsPriority, "Contacts", "contacts", data.list.size(), @@ -2294,7 +2490,7 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) { } pushSection( - 3, + kFrequentContactsPriority, "Frequent contacts", "frequent", size, @@ -2360,7 +2556,7 @@ Result HtmlWriter::writeSessions(const Data::SessionsList &data) { } pushSection( - 5, + kSessionsPriority, "Sessions", "sessions", data.list.size(), @@ -2406,7 +2602,7 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) { } pushSection( - 6, + kWebSessionsPriority, "Web sessions", "web", data.webList.size(), @@ -2418,7 +2614,7 @@ Result HtmlWriter::writeOtherData(const Data::File &data) { Expects(_summary != nullptr); pushSection( - 7, + kOtherPriority, "Other data", "other", 1, @@ -2447,7 +2643,7 @@ Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) { } pushSection( - 0, + kChatsPriority, "Chats", "chats", data.chats.size() + data.left.size(), diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index a2235d130..c1dee2ffd 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -35,6 +35,7 @@ private: }; struct UserpicData; +struct StoryData; class PeersMap; struct MediaData; @@ -59,6 +60,10 @@ public: Result writeUserpicsSlice(const Data::UserpicsSlice &data) override; Result writeUserpicsEnd() override; + Result writeStoriesStart(const Data::StoriesInfo &data) override; + Result writeStoriesSlice(const Data::StoriesSlice &data) override; + Result writeStoriesEnd() override; + Result writeContactsList(const Data::ContactsList &data) override; Result writeSessionsList(const Data::SessionsList &data) override; @@ -125,8 +130,10 @@ private: const Data::PersonalInfo &data, const QString &userpicPath); void pushUserpicsSection(); + void pushStoriesSection(); [[nodiscard]] QString userpicsFilePath() const; + [[nodiscard]] QString storiesFilePath() const; [[nodiscard]] QByteArray wrapMessageLink( int messageId, @@ -149,6 +156,9 @@ private: int _userpicsCount = 0; std::unique_ptr _userpics; + int _storiesCount = 0; + std::unique_ptr _stories; + QString _dialogsRelativePath; Data::DialogInfo _dialog; DialogsMode _dialogsMode = DialogsMode::None; diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 1c6ba2c87..9dfd21620 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -887,6 +887,77 @@ Result JsonWriter::writeUserpicsEnd() { return _output->writeBlock(popNesting()); } +Result JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("stories"); + return _output->writeBlock(block + pushNesting(Context::kArray)); +} + +Result JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) { + Expects(_output != nullptr); + + if (data.list.empty()) { + return Result::Success(); + } + + auto block = QByteArray(); + for (const auto &story : data.list) { + using SkipReason = Data::File::SkipReason; + const auto &file = story.file(); + Assert(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + const auto path = [&]() -> Data::Utf8String { + switch (file.skipReason) { + case SkipReason::Unavailable: + return "(Photo unavailable, please try again later)"; + case SkipReason::FileSize: + return "(Photo exceeds maximum size. " + "Change data exporting settings to download.)"; + case SkipReason::FileType: + return "(Photo not included. " + "Change data exporting settings to download.)"; + case SkipReason::None: return FormatFilePath(file); + } + Unexpected("Skip reason while writing story path."); + }(); + block.append(prepareArrayItemStart()); + block.append(SerializeObject(_context, { + { + "date", + story.date ? SerializeDate(story.date) : QByteArray() + }, + { + "date_unixtime", + story.date ? SerializeDateRaw(story.date) : QByteArray() + }, + { + "expires", + story.expires ? SerializeDate(story.expires) : QByteArray() + }, + { + "expires_unixtime", + story.expires ? SerializeDateRaw(story.expires) : QByteArray() + }, + { + "pinned", + story.pinned ? "true" : "false" + }, + { + "media", + SerializeString(path) + }, + })); + } + return _output->writeBlock(block); +} + +Result JsonWriter::writeStoriesEnd() { + Expects(_output != nullptr); + + return _output->writeBlock(popNesting()); +} + Result JsonWriter::writeContactsList(const Data::ContactsList &data) { Expects(_output != nullptr); diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h index 49f7035b0..879918fce 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.h +++ b/Telegram/SourceFiles/export/output/export_output_json.h @@ -44,6 +44,10 @@ public: Result writeUserpicsSlice(const Data::UserpicsSlice &data) override; Result writeUserpicsEnd() override; + Result writeStoriesStart(const Data::StoriesInfo &data) override; + Result writeStoriesSlice(const Data::StoriesSlice &data) override; + Result writeStoriesEnd() override; + Result writeContactsList(const Data::ContactsList &data) override; Result writeSessionsList(const Data::SessionsList &data) override; diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp index fac8b3d76..9813321ef 100644 --- a/Telegram/SourceFiles/export/view/export_view_content.cpp +++ b/Telegram/SourceFiles/export/view/export_view_content.cpp @@ -89,6 +89,13 @@ Content ContentFromState( case Step::Contacts: pushMain(tr::lng_export_option_contacts(tr::now)); break; + case Step::Stories: + pushMain(tr::lng_export_option_stories(tr::now)); + pushBytes( + "story" + QString::number(state.entityIndex), + state.bytesName, + state.bytesRandomId); + break; case Step::Sessions: pushMain(tr::lng_export_option_sessions(tr::now)); break; diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index d01d4eb53..94e00b43a 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -173,6 +173,11 @@ void SettingsWidget::setupFullExportOptions( tr::lng_export_option_contacts(tr::now), Type::Contacts, tr::lng_export_option_contacts_about(tr::now)); + addOptionWithAbout( + container, + tr::lng_export_option_stories(tr::now), + Type::Stories, + tr::lng_export_option_stories_about(tr::now)); addHeader(container, tr::lng_export_header_chats(tr::now)); addOption( container, diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index a674a4b82..d1642c0d4 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -230,6 +230,7 @@ FormatPointer MakeFormatPointer( if (!io) { return {}; } + io->seekable = (seek != nullptr); auto result = avformat_alloc_context(); if (!result) { LogError(u"avformat_alloc_context"_q); @@ -250,7 +251,9 @@ FormatPointer MakeFormatPointer( LogError(u"avformat_open_input"_q, error); return {}; } - result->flags |= AVFMT_FLAG_FAST_SEEK; + if (seek) { + result->flags |= AVFMT_FLAG_FAST_SEEK; + } // Now FormatPointer will own and free the IO context. io.release(); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index ec8f4c593..f6e5c92d5 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -610,14 +610,14 @@ void InnerWidget::elementShowPollResults( void InnerWidget::elementOpenPhoto( not_null photo, FullMsgId context) { - _controller->openPhoto(photo, context, MsgId(0)); + _controller->openPhoto(photo, { context }); } void InnerWidget::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - _controller->openDocument(document, context, MsgId(0), showInMediaView); + _controller->openDocument(document, showInMediaView, { context }); } void InnerWidget::elementCancelUpload(const FullMsgId &context) { @@ -1380,7 +1380,7 @@ void InnerWidget::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { - _controller->openDocument(document, itemId, MsgId(), true); + _controller->openDocument(document, true, { itemId }); } } } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index ba2442308..4e2e3cd41 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -820,7 +820,7 @@ void GenerateItems( const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { const auto bodyFlags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry; - const auto bodyReplyTo = MsgId(); + const auto bodyReplyTo = FullReplyTo(); const auto bodyViaBotId = UserId(); const auto bodyGroupedId = uint64(); return history->makeMessage( diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp index fb504a8b3..a64f98f47 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index ae7727aed..73aa7a83e 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -681,7 +681,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -729,7 +729,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -755,7 +755,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -781,7 +781,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -1194,6 +1194,8 @@ void History::applyServiceChanges( topic->setHasPinnedMessages(true); } } + }, [&](const MTPDmessageReplyStoryHeader &data) { + LOG(("API Error: story reply in messageActionPinMessage.")); }); } }, [&](const MTPDmessageActionGroupCall &data) { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index d2b80be10..933832d79 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -148,7 +148,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -168,7 +168,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -179,7 +179,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -190,7 +190,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, diff --git a/Telegram/SourceFiles/history/history_drag_area.cpp b/Telegram/SourceFiles/history/history_drag_area.cpp index 8b64a46bf..5c2cebf38 100644 --- a/Telegram/SourceFiles/history/history_drag_area.cpp +++ b/Telegram/SourceFiles/history/history_drag_area.cpp @@ -206,7 +206,8 @@ DragArea::Areas DragArea::SetupDragAreaToContainer( *attachDragState = DragState::None; updateDragAreas(); - e->acceptProposedAction(); + e->setDropAction(Qt::CopyAction); + e->accept(); }; const auto processDragEvents = [=](not_null event) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index a60dc62ab..07dcb96ef 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -393,7 +393,7 @@ bool HistoryInner::BotAbout::refresh() { | MessageFlag::Local; const auto postAuthor = QString(); const auto date = TimeId(0); - const auto replyTo = MsgId(0); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(0); const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( @@ -1843,13 +1843,12 @@ void HistoryInner::performDrag() { } void HistoryInner::itemRemoved(not_null item) { - if (_history != item->history() && _migrated != item->history()) { - return; - } - if (_pinnedItem == item) { _pinnedItem = nullptr; } + if (_history != item->history() && _migrated != item->history()) { + return; + } if (_reactionsItem.current() == item) { _reactionsItem = nullptr; } @@ -2352,7 +2351,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto item = _dragStateItem; const auto itemId = item ? item->fullId() : FullMsgId(); if (isUponSelected > 0) { - if (!hasCopyRestrictionForSelected()) { + if (!hasCopyRestrictionForSelected() + && !getSelectedText().empty()) { _menu->addAction( (isUponSelected > 1 ? tr::lng_context_copy_selected_items(tr::now) @@ -2453,7 +2453,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : QString(); if (isUponSelected > 0) { - if (!hasCopyRestrictionForSelected()) { + if (!hasCopyRestrictionForSelected() + && !getSelectedText().empty()) { _menu->addAction( ((isUponSelected > 1) ? tr::lng_context_copy_selected_items(tr::now) @@ -2790,7 +2791,7 @@ void HistoryInner::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { - _controller->openDocument(document, itemId, MsgId(), true); + _controller->openDocument(document, true, { itemId }); } } } @@ -2926,6 +2927,9 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) { && selectedState.canDeleteCount == selectedState.count) { _widget->confirmDeleteSelected(); } + } else if (!(e->modifiers() & ~Qt::ShiftModifier) + && e->key() != Qt::Key_Shift) { + _widget->tryProcessKeyInput(e); } else { e->ignore(); } @@ -3387,14 +3391,14 @@ void HistoryInner::elementShowPollResults( void HistoryInner::elementOpenPhoto( not_null photo, FullMsgId context) { - _controller->openPhoto(photo, context, MsgId(0)); + _controller->openPhoto(photo, { context }); } void HistoryInner::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - _controller->openDocument(document, context, MsgId(0), showInMediaView); + _controller->openDocument(document, showInMediaView, { context }); } void HistoryInner::elementCancelUpload(const FullMsgId &context) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 63b7923c9..537a8a66c 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. #include "data/data_sponsored_messages.h" +#include "data/data_stories.h" #include "data/data_wall_paper.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -133,6 +134,7 @@ struct HistoryItem::CreateConfig { PeerId replyToPeer = 0; MsgId replyTo = 0; MsgId replyToTop = 0; + StoryId replyToStory = 0; bool replyIsTopicPost = false; UserId viaBotId = 0; int viewsCount = -1; @@ -290,6 +292,11 @@ std::unique_ptr HistoryItem::CreateMedia( item, qs(media.vemoticon()), media.vvalue().v); + }, [&](const MTPDmessageMediaStory &media) -> Result { + return std::make_unique(item, FullStoryId{ + peerFromUser(media.vuser_id()), + media.vid().v, + }, media.is_via_mention()); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -325,6 +332,10 @@ HistoryItem::HistoryItem( } else if (checked == MediaCheckResult::HasTimeToLive) { createServiceFromMtp(data); applyTTL(data); + } else if (checked == MediaCheckResult::HasStoryMention) { + setMedia(*data.vmedia()); + createServiceFromMtp(data); + applyTTL(data); } else { createComponents(data); if (const auto media = data.vmedia()) { @@ -508,7 +519,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -543,7 +554,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -578,7 +589,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -608,7 +619,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -650,7 +661,7 @@ HistoryItem::HistoryItem( /*from.peer ? from.peer->id : */PeerId(0)) { createComponentsHelper( _flags, - MsgId(0), // replyTo + FullReplyTo(), UserId(0), // viaBotId QString(), // postAuthor HistoryMessageMarkupData()); @@ -674,6 +685,20 @@ HistoryItem::HistoryItem( } } +HistoryItem::HistoryItem( + not_null history, + not_null story) +: id(StoryIdToMsgId(story->id())) +, _history(history) +, _from(history->peer) +, _flags(MessageFlag::Local + | MessageFlag::Outgoing + | MessageFlag::FakeHistoryItem + | MessageFlag::StoryItem) +, _date(story->date()) { + setStoryFields(story); +} + HistoryItem::~HistoryItem() { _media = nullptr; clearSavedMedia(); @@ -726,6 +751,18 @@ void HistoryItem::dependencyItemRemoved(not_null dependency) { } } +void HistoryItem::dependencyStoryRemoved( + not_null dependency) { + if (const auto reply = Get()) { + const auto documentId = reply->replyToDocumentId; + reply->storyRemoved(this, dependency); + if (documentId != reply->replyToDocumentId + && generateLocalEntitiesByReply()) { + _history->owner().requestItemTextRefresh(this); + } + } +} + void HistoryItem::updateDependencyItem() { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; @@ -1024,6 +1061,10 @@ void HistoryItem::updateServiceText(PreparedServiceText &&text) { _history->owner().updateDependentMessages(this); } +void HistoryItem::updateStoryMentionText() { + setServiceText(prepareStoryMentionText()); +} + HistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() { if (const auto markup = Get()) { if (markup->data.flags & ReplyMarkupFlag::Inline) { @@ -1480,6 +1521,30 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { finishEdition(keyboardTop); } +void HistoryItem::applyChanges(not_null story) { + Expects(_flags & MessageFlag::StoryItem); + Expects(StoryIdFromMsgId(id) == story->id()); + + _media = nullptr; + setStoryFields(story); + + finishEdition(-1); +} + +void HistoryItem::setStoryFields(not_null story) { + const auto spoiler = false; + if (const auto photo = story->photo()) { + _media = std::make_unique(this, photo, spoiler); + } else if (const auto document = story->document()) { + _media = std::make_unique( + this, + document, + /*skipPremiumEffect=*/false, + spoiler); + } + setText(story->caption()); +} + void HistoryItem::applyEdition(const MTPDmessageService &message) { if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); @@ -1544,6 +1609,7 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { data.vreply_to_top_id().value_or( data.vreply_to_msg_id().v), data.is_forum_topic()); + }, [](const MTPDmessageReplyStoryHeader &data) { }); } setPostAuthor(data.vpost_author().value_or_empty()); @@ -1923,6 +1989,8 @@ bool HistoryItem::forbidsSaving() const { bool HistoryItem::canDelete() const { if (isSponsored()) { return false; + } else if (IsStoryMsgId(id)) { + return false; } else if (isService() && !isRegular()) { return false; } else if (topicRootId() == id) { @@ -2556,7 +2624,7 @@ void HistoryItem::setReplyFields( && !IsServerMsgId(reply->replyToMsgId)) { reply->replyToMsgId = replyTo; if (!reply->updateData(this)) { - RequestDependentMessageData( + RequestDependentMessageItem( this, reply->replyToPeerId, reply->replyToMsgId); @@ -2756,6 +2824,26 @@ MsgId HistoryItem::topicRootId() const { return Data::ForumTopic::kGeneralId; } +FullStoryId HistoryItem::replyToStory() const { + if (const auto reply = Get()) { + if (reply->replyToStoryId) { + const auto peerId = reply->replyToPeerId + ? reply->replyToPeerId + : _history->peer->id; + return { .peer = peerId, .story = reply->replyToStoryId }; + } + } + return {}; +} + +FullReplyTo HistoryItem::replyTo() const { + return { + .msgId = replyToId(), + .topicRootId = topicRootId(), + .storyId = replyToStory(), + }; +} + void HistoryItem::setText(const TextWithEntities &textWithEntities) { for (const auto &entity : textWithEntities.entities) { auto type = entity.type(); @@ -2917,7 +3005,7 @@ const std::vector &HistoryItem::customTextLinks() const { void HistoryItem::createComponents(CreateConfig &&config) { uint64 mask = 0; - if (config.replyTo) { + if (config.replyTo || config.replyToStory) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { @@ -2958,12 +3046,21 @@ void HistoryItem::createComponents(CreateConfig &&config) { reply->replyToPeerId = config.replyToPeer; reply->replyToMsgId = config.replyTo; reply->replyToMsgTop = isScheduled() ? 0 : config.replyToTop; + reply->replyToStoryId = config.replyToStory; + reply->storyReply = (config.replyToStory != 0); reply->topicPost = config.replyIsTopicPost; if (!reply->updateData(this)) { - RequestDependentMessageData( - this, - reply->replyToPeerId, - reply->replyToMsgId); + if (reply->replyToMsgId) { + RequestDependentMessageItem( + this, + reply->replyToPeerId, + reply->replyToMsgId); + } else if (reply->replyToStoryId) { + RequestDependentMessageStory( + this, + reply->replyToPeerId, + reply->replyToStoryId); + } } } if (const auto via = Get()) { @@ -3126,7 +3223,9 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { } using Type = HistoryMessageSponsored::Type; - sponsored->type = from.isExactPost + sponsored->type = from.isExternalLink + ? Type::ExternalLink + : from.isExactPost ? Type::Post : from.isBot ? Type::Bot @@ -3139,17 +3238,19 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { void HistoryItem::createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup) { auto config = CreateConfig(); config.viaBotId = viaBotId; if (flags & MessageFlag::HasReplyInfo) { - config.replyTo = replyTo; - const auto to = LookupReplyTo(_history, replyTo); + config.replyTo = replyTo.msgId; + config.replyToStory = replyTo.storyId.story; + config.replyToPeer = replyTo.storyId ? replyTo.storyId.peer : 0; + const auto to = LookupReplyTo(_history, replyTo.msgId); const auto replyToTop = LookupReplyToTop(to); - config.replyToTop = replyToTop ? replyToTop : replyTo; + config.replyToTop = replyToTop ? replyToTop : replyTo.msgId; const auto forum = _history->asForum(); config.replyIsTopicPost = LookupReplyIsTopicPost(to) || (to && to->Has()) @@ -3213,7 +3314,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { const auto &recent = data.vrecent_reactions().value_or_empty(); if (min && hasUnreadReaction()) { // We can't update reactions from min if we have unread. - if (_reactions->checkIfChanged(list, recent)) { + if (_reactions->checkIfChanged(list, recent, min)) { updateReactionsUnknown(); } return false; @@ -3259,6 +3360,9 @@ void HistoryItem::createComponents(const MTPDmessage &data) { : id; config.replyToTop = data.vreply_to_top_id().value_or(id); config.replyIsTopicPost = data.is_forum_topic(); + }, [&](const MTPDmessageReplyStoryHeader &data) { + config.replyToPeer = peerFromUser(data.vuser_id()); + config.replyToStory = data.vstory_id().v; }); } config.viaBotId = data.vvia_bot_id().value_or_empty(); @@ -3304,15 +3408,13 @@ void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) { void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { AddComponents(HistoryServiceData::Bit()); + const auto unread = message.is_media_unread(); const auto media = message.vmedia(); Assert(media != nullptr); - const auto mediaType = media->type(); - switch (mediaType) { - case mtpc_messageMediaPhoto: { - if (message.is_media_unread()) { - const auto &photo = media->c_messageMediaPhoto(); - const auto ttl = photo.vttl_seconds(); + media->match([&](const MTPDmessageMediaPhoto &data) { + if (unread) { + const auto ttl = data.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, ttl->v); @@ -3335,11 +3437,9 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) }); } - } break; - case mtpc_messageMediaDocument: { - if (message.is_media_unread()) { - const auto &document = media->c_messageMediaDocument(); - const auto ttl = document.vttl_seconds(); + }, [&](const MTPDmessageMediaDocument &data) { + if (unread) { + const auto ttl = data.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, ttl->v); @@ -3362,10 +3462,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities) }); } - } break; - - default: Unexpected("Media type in HistoryItem::createServiceFromMtp()"); - } + }, [&](const MTPDmessageMediaStory &data) { + setServiceText(prepareStoryMentionText()); + }, [](const auto &) { + Unexpected("Media type in HistoryItem::createServiceFromMtp()"); + }); if (const auto reactions = message.vreactions()) { updateReactions(reactions); @@ -3505,7 +3606,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->topicPost = data.is_forum_topic() || Has(); if (!updateServiceDependent()) { - RequestDependentMessageData( + RequestDependentMessageItem( this, (dependent->peerId ? dependent->peerId @@ -3513,6 +3614,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->msgId); } } + }, [](const MTPDmessageReplyStoryHeader &data) { }); } setServiceMessageByAction(action); @@ -3520,9 +3622,29 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { void HistoryItem::setMedia(const MTPMessageMedia &media) { _media = CreateMedia(this, media); + checkStoryForwardInfo(); checkBuyButton(); } +void HistoryItem::checkStoryForwardInfo() { + if (const auto storyId = _media ? _media->storyId() : FullStoryId()) { + const auto adding = !Has(); + if (adding) { + AddComponents(HistoryMessageForwarded::Bit()); + } + const auto forwarded = Get(); + if (forwarded->story || adding) { + const auto peer = history()->owner().peer(storyId.peer); + forwarded->story = true; + forwarded->originalSender = peer; + } + } else if (const auto forwarded = Get()) { + if (forwarded->story) { + RemoveComponents(HistoryMessageForwarded::Bit()); + } + } +} + void HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) { const auto date = data.vdate().v; if (_date == date) { @@ -4713,6 +4835,28 @@ PreparedServiceText HistoryItem::preparePaymentSentText() { return result; } +PreparedServiceText HistoryItem::prepareStoryMentionText() { + auto result = PreparedServiceText(); + const auto peer = history()->peer; + result.links.push_back(peer->createOpenLink()); + const auto phrase = (this->media() && this->media()->storyExpired(true)) + ? (out() + ? tr::lng_action_story_mention_me_unavailable + : tr::lng_action_story_mention_unavailable) + : (out() + ? tr::lng_action_story_mention_me + : tr::lng_action_story_mention); + result.text = phrase( + tr::now, + lt_user, + Ui::Text::Wrapped( + Ui::Text::Bold(peer->shortName()), + EntityType::CustomUrl, + u"internal:index"_q + QChar(1)), + Ui::Text::WithEntities); + return result; +} + PreparedServiceText HistoryItem::prepareCallScheduledText( TimeId scheduleDate) { const auto call = Get(); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b26793963..973141ba5 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -57,6 +57,7 @@ class MessageReactions; class ForumTopic; class Thread; struct SponsoredFrom; +class Story; } // namespace Data namespace Main { @@ -117,7 +118,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -147,7 +148,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -159,7 +160,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -171,13 +172,14 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup); + HistoryItem(not_null history, not_null story); ~HistoryItem(); struct Destroyer { @@ -185,13 +187,16 @@ public: }; void dependencyItemRemoved(not_null dependency); + void dependencyStoryRemoved(not_null dependency); void updateDependencyItem(); [[nodiscard]] MsgId dependencyMsgId() const; [[nodiscard]] bool notificationReady() const; [[nodiscard]] PeerData *specialNotificationPeer() const; + void checkStoryForwardInfo(); void checkBuyButton(); void updateServiceText(PreparedServiceText &&text); + void updateStoryMentionText(); [[nodiscard]] UserData *viaBot() const; [[nodiscard]] UserData *getMessageBot() const; @@ -330,6 +335,7 @@ public: [[nodiscard]] bool isService() const; void applyEdition(HistoryMessageEdition &&edition); + void applyChanges(not_null story); void applyEdition(const MTPDmessageService &message); void applyEdition(const MTPMessageExtendedMedia &media); @@ -458,6 +464,8 @@ public: [[nodiscard]] MsgId replyToId() const; [[nodiscard]] MsgId replyToTop() const; [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] FullStoryId replyToStory() const; + [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] bool inThread(MsgId rootId) const; [[nodiscard]] not_null author() const; @@ -518,7 +526,7 @@ private: void createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup); @@ -555,6 +563,7 @@ private: bool updateServiceDependent(bool force = false); void setServiceText(PreparedServiceText &&prepared); + void setStoryFields(not_null story); void finishEdition(int oldKeyboardTop); void finishEditionToEmpty(); @@ -600,6 +609,7 @@ private: [[nodiscard]] PreparedServiceText preparePinnedText(); [[nodiscard]] PreparedServiceText prepareGameScoreText(); [[nodiscard]] PreparedServiceText preparePaymentSentText(); + [[nodiscard]] PreparedServiceText prepareStoryMentionText(); [[nodiscard]] PreparedServiceText prepareInvitedToCallText( const std::vector> &users, CallId linkCallId); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 0cecbe2fe..05289283e 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_file_click_handler.h" +#include "data/data_stories.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" @@ -191,7 +192,13 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } else { phrase = name; } - if (via && psaType.isEmpty()) { + if (story) { + phrase = tr::lng_forwarded_story( + tr::now, + lt_user, + Ui::Text::Link(phrase.text, QString()), // Link 1. + Ui::Text::WithEntities); + } else if (via && psaType.isEmpty()) { if (fromChannel) { phrase = tr::lng_forwarded_channel_via( tr::now, @@ -257,15 +264,17 @@ bool HistoryMessageReply::updateData( bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { - if (replyToMsg || !replyToMsgId) { + if ((replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId)) { return true; } } - if (!replyToMsg) { + const auto peerId = replyToPeerId + ? replyToPeerId + : holder->history()->peer->id; + if (!replyToMsg && replyToMsgId) { replyToMsg = holder->history()->owner().message( - (replyToPeerId - ? replyToPeerId - : holder->history()->peer->id), + peerId, replyToMsgId); if (replyToMsg) { if (replyToMsg->isEmpty()) { @@ -279,8 +288,22 @@ bool HistoryMessageReply::updateData( } } } + if (!replyToStory && replyToStoryId) { + const auto maybe = holder->history()->owner().stories().lookup({ + peerId, + replyToStoryId, + }); + if (maybe) { + replyToStory = *maybe; + holder->history()->owner().stories().registerDependentMessage( + holder, + replyToStory.get()); + } else if (maybe.error() == Data::NoStory::Deleted) { + force = true; + } + } - if (replyToMsg) { + if (replyToMsg || replyToStory) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), @@ -288,14 +311,16 @@ bool HistoryMessageReply::updateData( }; replyToText.setMarkedText( st::messageTextStyle, - replyToMsg->inReplyText(), + (replyToMsg + ? replyToMsg->inReplyText() + : replyToStory->inReplyText()), Ui::DialogTextOptions(), context); updateName(holder); setReplyToLinkFrom(holder); - if (!replyToMsg->Has()) { + if (replyToMsg && !replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); replyToVia->create( @@ -304,15 +329,17 @@ bool HistoryMessageReply::updateData( } } - { + if (replyToMsg) { const auto peer = replyToMsg->history()->peer; replyToColorKey = (!holder->out() && (peer->isMegagroup() || peer->isChat())) ? replyToMsg->from()->id : PeerId(0); + } else { + replyToColorKey = PeerId(0); } - const auto media = replyToMsg->media(); + const auto media = replyToMsg ? replyToMsg->media() : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { @@ -320,19 +347,23 @@ bool HistoryMessageReply::updateData( } } else if (force) { replyToMsgId = 0; + replyToStoryId = 0; replyToColorKey = PeerId(0); spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } - return (replyToMsg || !replyToMsgId); + return (replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId); } void HistoryMessageReply::setReplyToLinkFrom( not_null holder) { replyToLnk = replyToMsg ? JumpToMessageClickHandler(replyToMsg.get(), holder->fullId()) + : replyToStory + ? JumpToStoryClickHandler(replyToStory.get()) : nullptr; } @@ -344,7 +375,14 @@ void HistoryMessageReply::clearData(not_null holder) { replyToMsg.get()); replyToMsg = nullptr; } + if (replyToStory) { + holder->history()->owner().stories().unregisterDependentMessage( + holder, + replyToStory.get()); + replyToStory = nullptr; + } replyToMsgId = 0; + replyToStoryId = 0; refreshReplyToMedia(); } @@ -365,7 +403,9 @@ PeerData *HistoryMessageReply::replyToFrom( QString HistoryMessageReply::replyToFromName( not_null holder) const { - if (!replyToMsg) { + if (replyToStory) { + return replyToFromName(replyToStory->peer()); + } else if (!replyToMsg) { return QString(); } else if (holder->Has()) { if (const auto fwd = replyToMsg->Get()) { @@ -405,10 +445,15 @@ void HistoryMessageReply::updateName( replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); if (const auto from = replyToFrom(holder)) { replyToVersion = from->nameVersion(); - } else { + } else if (replyToMsg) { replyToVersion = replyToMsg->author()->nameVersion(); + } else { + replyToVersion = replyToStory->peer()->nameVersion(); } - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); if (replyToVia) { @@ -417,28 +462,40 @@ void HistoryMessageReply::updateName( maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize))); } else { - maxReplyWidth = st::msgDateFont->width(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now)); + maxReplyWidth = st::msgDateFont->width(statePhrase()); } maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (replyToVia) { - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); } } void HistoryMessageReply::itemRemoved( - HistoryItem *holder, - HistoryItem *removed) { + not_null holder, + not_null removed) { if (replyToMsg.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } +void HistoryMessageReply::storyRemoved( + not_null holder, + not_null removed) { + if (replyToStory.get() == removed) { + clearData(holder); + holder->history()->owner().requestItemResize(holder); + } +} + void HistoryMessageReply::paint( Painter &p, not_null holder, @@ -471,16 +528,19 @@ void HistoryMessageReply::paint( const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (w > st::msgReplyBarSkip) { - if (replyToMsg) { - const auto media = replyToMsg->media(); - auto hasPreview = media && media->hasReplyPreview(); + if (replyToMsg || replyToStory) { + const auto media = replyToMsg ? replyToMsg->media() : nullptr; + auto hasPreview = (replyToStory && replyToStory->hasReplyPreview()) || (media && media->hasReplyPreview()); if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) { hasPreview = false; } auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { - if (const auto image = media->replyPreview()) { + const auto image = media + ? media->replyPreview() + : replyToStory->replyPreview(); + if (image) { auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x); const auto preview = image->pixSingle( image->size() / style::DevicePixelRatio(), @@ -542,11 +602,19 @@ void HistoryMessageReply::paint( p.setPen(inBubble ? stm->msgDateFg : st->msgDateImgFg()); - p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now), w - st::msgReplyBarSkip)); + p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(statePhrase(), w - st::msgReplyBarSkip)); } } } +QString HistoryMessageReply::statePhrase() const { + return (replyToMsgId || replyToStoryId) + ? tr::lng_profile_loading(tr::now) + : storyReply + ? tr::lng_deleted_story(tr::now) + : tr::lng_deleted_message(tr::now); +} + void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 5b92c6cfc..5a8db07c9 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -25,6 +25,7 @@ struct PeerUserpicView; namespace Data { class Session; +class Story; } // namespace Data namespace Media::Player { @@ -128,6 +129,7 @@ struct HistoryMessageForwarded : public RuntimeComponent { @@ -137,6 +139,7 @@ struct HistoryMessageSponsored : public RuntimeComponent sender; Type type = Type::User; @@ -182,6 +185,44 @@ private: }; +class ReplyToStoryPointer final { +public: + ReplyToStoryPointer(Data::Story *story = nullptr) : _data(story) { + } + ReplyToStoryPointer(ReplyToStoryPointer &&other) + : _data(base::take(other._data)) { + } + ReplyToStoryPointer &operator=(ReplyToStoryPointer &&other) { + _data = base::take(other._data); + return *this; + } + ReplyToStoryPointer &operator=(Data::Story *item) { + _data = item; + return *this; + } + + [[nodiscard]] bool empty() const { + return !_data; + } + [[nodiscard]] Data::Story *get() const { + return _data; + } + explicit operator bool() const { + return !empty(); + } + + [[nodiscard]] Data::Story *operator->() const { + return _data; + } + [[nodiscard]] Data::Story &operator*() const { + return *_data; + } + +private: + Data::Story *_data = nullptr; + +}; + struct HistoryMessageReply : public RuntimeComponent { HistoryMessageReply() = default; @@ -210,7 +251,12 @@ struct HistoryMessageReply [[nodiscard]] bool isNameUpdated(not_null holder) const; void updateName(not_null holder) const; void resize(int width) const; - void itemRemoved(HistoryItem *holder, HistoryItem *removed); + void itemRemoved( + not_null holder, + not_null removed); + void storyRemoved( + not_null holder, + not_null removed); void paint( Painter &p, @@ -236,6 +282,7 @@ struct HistoryMessageReply [[nodiscard]] ClickHandlerPtr replyToLink() const { return replyToLnk; } + [[nodiscard]] QString statePhrase() const; void setReplyToLinkFrom(not_null holder); void refreshReplyToMedia(); @@ -243,11 +290,13 @@ struct HistoryMessageReply PeerId replyToPeerId = 0; MsgId replyToMsgId = 0; MsgId replyToMsgTop = 0; + StoryId replyToStoryId = 0; using ColorKey = PeerId; ColorKey replyToColorKey = 0; DocumentId replyToDocumentId = 0; WebPageId replyToWebPageId = 0; ReplyToMessagePointer replyToMsg; + ReplyToStoryPointer replyToStory; std::unique_ptr replyToVia; std::unique_ptr spoiler; ClickHandlerPtr replyToLnk; @@ -256,6 +305,7 @@ struct HistoryMessageReply mutable int maxReplyWidth = 0; int toWidth = 0; bool topicPost = false; + bool storyReply = false; }; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 7042cb557..c89788248 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_types.h" #include "data/data_message_reactions.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" @@ -63,6 +64,11 @@ QString GetErrorTextForSending( const auto thread = topic ? not_null(topic) : peer->owner().history(peer); + if (request.story) { + if (const auto error = request.story->errorTextForForward(thread)) { + return *error; + } + } if (request.forward) { for (const auto &item : *request.forward) { if (const auto error = item->errorTextForForward(thread)) { @@ -83,6 +89,7 @@ QString GetErrorTextForSending( } if (peer->slowmodeApplied()) { const auto count = (hasText ? 1 : 0) + + (request.story ? 1 : 0) + (request.forward ? int(request.forward->size()) : 0); if (const auto history = peer->owner().historyLoaded(peer)) { if (!request.ignoreSlowmodeCountdown @@ -93,7 +100,7 @@ QString GetErrorTextForSending( } if (request.text && request.text->text.size() > MaxMessageSize) { return tr::lng_slowmode_too_long(tr::now); - } else if (hasText && count > 1) { + } else if ((hasText || request.story) && count > 1) { return tr::lng_slowmode_no_many(tr::now); } else if (count > 1) { const auto albumForward = [&] { @@ -132,7 +139,7 @@ QString GetErrorTextForSending( return GetErrorTextForSending(thread->peer(), std::move(request)); } -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId) { @@ -153,6 +160,23 @@ void RequestDependentMessageData( done); } +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId) { + const auto fullId = item->fullId(); + const auto history = item->history(); + const auto session = &history->session(); + const auto done = [=] { + if (const auto item = session->data().message(fullId)) { + item->updateDependencyItem(); + } + }; + history->owner().stories().resolve( + { peerId ? peerId : history->peer->id, storyId }, + done); +} + MessageFlags NewMessageFlags(not_null peer) { return MessageFlag::BeingSent | (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing); @@ -266,6 +290,27 @@ ClickHandlerPtr JumpToMessageClickHandler( }); } +ClickHandlerPtr JumpToStoryClickHandler(not_null story) { + return JumpToStoryClickHandler(story->peer(), story->id()); +} + +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId) { + return std::make_shared([=] { + const auto separate = Core::App().separateWindowForPeer(peer); + const auto controller = separate + ? separate->sessionController() + : peer->session().tryResolveWindow(); + if (controller) { + controller->openPeerStory( + peer, + storyId, + { Data::StoriesContextSingle() }); + } + }); +} + MessageFlags FlagsFromMTP( MsgId id, MTPDmessage::Flags flags, @@ -309,8 +354,13 @@ MessageFlags FlagsFromMTP( } MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { - if (const auto id = action.replyTo) { - const auto to = LookupReplyTo(action.history, id); + if (const auto replyTo = action.replyTo) { + if (replyTo.storyId) { + return MTP_messageReplyStoryHeader( + MTP_long(peerToUser(replyTo.storyId.peer).bare), + MTP_int(replyTo.storyId.story)); + } + const auto to = LookupReplyTo(action.history, replyTo.msgId); if (const auto replyToTop = LookupReplyToTop(to)) { using Flag = MTPDmessageReplyHeader::Flag; return MTP_messageReplyHeader( @@ -318,13 +368,13 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { | (LookupReplyIsTopicPost(to) ? Flag::f_forum_topic : Flag(0))), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTP_int(replyToTop)); } return MTP_messageReplyHeader( MTP_flags(0), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTPint()); } @@ -399,6 +449,10 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; + }, [](const MTPDmessageMediaStory &data) { + return data.is_via_mention() + ? Result::HasStoryMention + : Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 46b772517..e7ed74290 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -15,6 +15,7 @@ struct SendAction; } // namespace Api namespace Data { +class Story; class Thread; } // namespace Data @@ -43,6 +44,7 @@ enum class MediaCheckResult { Unsupported, Empty, HasTimeToLive, + HasStoryMention, }; [[nodiscard]] MediaCheckResult CheckMessageMedia( const MTPMessageMedia &media); @@ -69,10 +71,14 @@ void CheckReactionNotificationSchedule( const TextWithEntities &text = TextWithEntities()); [[nodiscard]] TextWithEntities UnsupportedMessageText(); -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId); +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId); [[nodiscard]] MessageFlags NewMessageFlags(not_null peer); [[nodiscard]] bool ShouldSendSilent( not_null peer, @@ -86,6 +92,7 @@ void RequestDependentMessageData( struct SendingErrorRequest { MsgId topicRootId = 0; const HistoryItemsList *forward = nullptr; + const Data::Story *story = nullptr; const TextWithTags *text = nullptr; bool ignoreSlowmodeCountdown = false; }; @@ -115,6 +122,11 @@ struct SendingErrorRequest { [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( not_null item, FullMsgId returnToId = FullMsgId()); +[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( + not_null story); +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId); [[nodiscard]] not_null GenerateJoinedMessage( not_null history, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index d3f09a32d..0cf17c93c 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -232,7 +232,7 @@ HistoryWidget::HistoryWidget( , _supportAutocomplete(session().supportMode() ? object_ptr(this, &session()) : nullptr) -, _send(std::make_shared(this)) +, _send(std::make_shared(this, st::historySend)) , _unblock(this, tr::lng_unblock_button(tr::now).toUpper(), st::historyUnblock) , _botStart(this, tr::lng_bot_start(tr::now).toUpper(), st::historyComposeButton) , _joinChannel( @@ -856,7 +856,7 @@ HistoryWidget::HistoryWidget( }) | rpl::start_with_next([=](const Api::SendAction &action) { const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId( action.history->peer->id, - action.replyTo)); + action.replyTo.msgId)); if (action.replaceMediaOf) { } else if (action.options.scheduled) { cancelReply(lastKeyboardUsed); @@ -946,18 +946,6 @@ void HistoryWidget::refreshTabbedPanel() { } void HistoryWidget::initVoiceRecordBar() { - { - auto scrollHeight = rpl::combine( - _scroll->topValue(), - _scroll->heightValue() - ) | rpl::map([](int top, int height) { - return top + height - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(scrollHeight)); - } - - _voiceRecordBar->setSendButtonGeometryValue(_send->geometryValue()); - _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { if (_peer) { @@ -1391,7 +1379,7 @@ AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { const auto result = (isChoosingTheme() || (_inlineBot && !_inlineLookingUpBot)) ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, {}); if (result.query.isEmpty()) { return result; } else if (result.query[0] == '#' @@ -1482,9 +1470,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { if (result.open) { const auto request = result.result->openRequest(); if (const auto photo = request.photo()) { - controller()->openPhoto(photo, {}, {}); + controller()->openPhoto(photo, {}); } else if (const auto document = request.document()) { - controller()->openDocument(document, {}, {}); + controller()->openDocument(document, false, {}); } } else { sendInlineResult(result); @@ -1818,6 +1806,15 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived( return false; } +void HistoryWidget::tryProcessKeyInput(not_null e) { + e->accept(); + keyPressEvent(e); + if (!e->isAccepted() && _canSendTexts && _field->isVisible()) { + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + } +} + void HistoryWidget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { @@ -3856,8 +3853,7 @@ void HistoryWidget::hideSelectorControlsAnimated() { Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = 0; + result.replyTo = { .msgId = replyToId() }; result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -3905,7 +3901,7 @@ void HistoryWidget::send(Api::SendOptions options) { ? _previewData->id : WebPageId(0)); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _field->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -4040,7 +4036,8 @@ void HistoryWidget::reportSelectedMessages() { const auto reason = _chooseForReport->reason; const auto weak = Ui::MakeWeak(_list.data()); controller()->window().show(Box([=](not_null box) { - Ui::ReportDetailsBox(box, [=](const QString &text) { + const auto &st = st::defaultReportBox; + Ui::ReportDetailsBox(box, st, [=](const QString &text) { if (weak) { clearSelected(); controller()->clearChooseReportMessages(); @@ -4378,7 +4375,7 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { toSend, TextWithTags::Tags() }; - message.action.replyTo = request.replyTo + message.action.replyTo.msgId = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? request.replyTo : replyToId()) @@ -5172,7 +5169,8 @@ bool HistoryWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } controller()->showToast(text); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 46d8287b9..ae1f9bf54 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -280,6 +280,8 @@ public: bool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo); + void tryProcessKeyInput(not_null e); + ~HistoryWidget(); protected: diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 4146e8eb8..86d412b2a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -58,7 +58,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/send_as_peers.h" #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" -#include "styles/style_chat.h" #include "ui/text/text_options.h" #include "ui/ui_utility.h" #include "ui/widgets/input_fields.h" @@ -72,6 +71,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -355,6 +356,7 @@ public: rpl::producer title, rpl::producer description, rpl::producer page); + void previewUnregister(); [[nodiscard]] bool isDisplayed() const; [[nodiscard]] bool isEditingMessage() const; @@ -413,6 +415,7 @@ private: rpl::event_stream<> _replyCancelled; rpl::event_stream<> _forwardCancelled; rpl::event_stream<> _previewCancelled; + rpl::lifetime _previewLifetime; rpl::variable _editMsgId; rpl::variable _replyToId; @@ -677,17 +680,18 @@ void FieldHeader::resolveMessageData() { } void FieldHeader::previewRequested( - rpl::producer title, - rpl::producer description, - rpl::producer page) { + rpl::producer title, + rpl::producer description, + rpl::producer page) { + _previewLifetime.destroy(); std::move( title ) | rpl::filter([=] { return !_preview.cancelled; - }) | start_with_next([=](const QString &t) { + }) | rpl::start_with_next([=](const QString &t) { _title = t; - }, lifetime()); + }, _previewLifetime); std::move( description @@ -695,7 +699,7 @@ void FieldHeader::previewRequested( return !_preview.cancelled; }) | rpl::start_with_next([=](const QString &d) { _description = d; - }, lifetime()); + }, _previewLifetime); std::move( page @@ -704,8 +708,11 @@ void FieldHeader::previewRequested( }) | rpl::start_with_next([=](WebPageData *p) { _preview.data = p; updateVisible(); - }, lifetime()); + }, _previewLifetime); +} +void FieldHeader::previewUnregister() { + _previewLifetime.destroy(); } void FieldHeader::paintWebPage(Painter &p, not_null context) { @@ -932,7 +939,11 @@ ComposeControls::ComposeControls( ComposeControls::ComposeControls( not_null parent, ComposeControlsDescriptor descriptor) -: _parent(parent) +: _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _features(descriptor.features) +, _parent(parent) , _show(std::move(descriptor.show)) , _session(&_show->session()) , _regularWindow(descriptor.regularWindow) @@ -940,42 +951,63 @@ ComposeControls::ComposeControls( ? nullptr : std::make_unique( _parent, - _show, - Window::GifPauseReason::TabbedPanel)) + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::TabbedPanel, + .mode = ChatHelpers::TabbedSelector::Mode::Full, + .features = _features, + })) , _selector(_regularWindow ? _regularWindow->tabbedSelector() : not_null(_ownedSelector.get())) , _mode(descriptor.mode) , _wrap(std::make_unique(parent)) , _writeRestricted(std::make_unique(parent)) -, _send(std::make_shared(_wrap.get())) +, _send(std::make_shared(_wrap.get(), _st.send)) , _attachToggle(Ui::CreateChild( _wrap.get(), - st::historyAttach)) + _st.attach)) , _tabbedSelectorToggle(Ui::CreateChild( _wrap.get(), - st::historyAttachEmoji)) + _st.emoji)) +, _fieldCustomPlaceholder(std::move(descriptor.customPlaceholder)) , _field( Ui::CreateChild( _wrap.get(), - st::historyComposeField, + _st.field, Ui::InputField::Mode::MultiLine, - tr::lng_message_ph())) -, _botCommandStart(Ui::CreateChild( - _wrap.get(), - st::historyBotCommandStart)) -, _autocomplete(std::make_unique(parent, _show)) + (_fieldCustomPlaceholder + ? rpl::duplicate(_fieldCustomPlaceholder) + : tr::lng_message_ph()))) +, _botCommandStart(_features.botCommandSend + ? Ui::CreateChild( + _wrap.get(), + st::historyBotCommandStart) + : nullptr) +, _autocomplete(std::make_unique( + parent, + _show, + &_st.tabbed)) , _header(std::make_unique(_wrap.get(), _show)) , _voiceRecordBar(std::make_unique( _wrap.get(), - parent, - _show, - _send, - st::historySendSize.height())) + Controls::VoiceRecordBarDescriptor{ + .outerContainer = parent, + .show = _show, + .send = _send, + .customCancelText = descriptor.voiceCustomCancelText, + .stOverride = &_st.record, + .recorderHeight = st::historySendSize.height(), + .lockFromBottom = descriptor.voiceLockFromBottom, + })) , _sendMenuType(descriptor.sendMenuType) , _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted)) , _saveDraftTimer([=] { saveDraft(); }) , _saveCloudDraftTimer([=] { saveCloudDraft(); }) { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } if (descriptor.stickerOrEmojiChosen) { std::move( descriptor.stickerOrEmojiChosen @@ -996,10 +1028,6 @@ Main::Session &ComposeControls::session() const { } void ComposeControls::setHistory(SetHistoryArgs &&args) { - // Right now only single non-null set of history is supported. - // Otherwise initWebpageProcess should be updated / rewritten. - Expects(!_history && (*args.history)); - _showSlowmodeError = std::move(args.showSlowmodeError); _sendActionFactory = std::move(args.sendActionFactory); _slowmodeSecondsLeft = rpl::single(0) @@ -1014,6 +1042,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { } unregisterDraftSources(); _history = history; + _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); _selector->setCurrentPeer(history ? history->peer.get() : nullptr); @@ -1025,9 +1054,12 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { updateControlsVisibility(); updateFieldPlaceholder(); updateAttachBotsMenu(); - //if (!_history) { - // return; - //} + + _sendAs = nullptr; + _silent = nullptr; + if (!_history) { + return; + } const auto peer = _history->peer; initSendAsButton(peer); if (peer->isChat() && peer->asChat()->noParticipantInfo()) { @@ -1102,6 +1134,15 @@ bool ComposeControls::focus() { return true; } +rpl::producer ComposeControls::focusedValue() const { + return rpl::single(Ui::InFocusChain(_wrap.get())) + | rpl::then(_focusChanges.events()); +} + +rpl::producer ComposeControls::tabbedPanelShownValue() const { + return _tabbedPanel ? _tabbedPanel->shownValue() : rpl::single(false); +} + rpl::producer<> ComposeControls::cancelRequests() const { return _cancelRequests.events(); } @@ -1363,7 +1404,7 @@ void ComposeControls::checkAutocomplete() { const auto peer = _history->peer; const auto autocomplete = _isInlineBot ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, _features); if (!autocomplete.query.isEmpty()) { if (autocomplete.query[0] == '#' && cRecentWriteHashtags().isEmpty() @@ -1410,7 +1451,9 @@ void ComposeControls::init() { updateWrappingVisibility(); }, _wrap->lifetime()); - _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); + if (_botCommandStart) { + _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); + } _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { @@ -1590,6 +1633,8 @@ void ComposeControls::initKeyHandler() { }); return Result::Cancel; } + } else if (k->key() == Qt::Key_Escape) { + return Result::Cancel; } return Result::Continue; }); @@ -1602,7 +1647,12 @@ void ComposeControls::initField() { Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); }); Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); }); Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); }); - //Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); }); + Ui::Connect(_field, &Ui::InputField::focused, [=] { + _focusChanges.fire(true); + }); + Ui::Connect(_field, &Ui::InputField::blurred, [=] { + _focusChanges.fire(false); + }); Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); }); InitMessageField(_show, _field, [=](not_null emoji) { if (_history && Data::AllowEmojiWithoutPremium(_history->peer)) { @@ -1621,7 +1671,11 @@ void ComposeControls::initField() { _parent, _field, _session, - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); _raiseEmojiSuggestions = [=] { suggestions->raise(); }; const auto rawTextEdit = _field->rawTextEdit().get(); @@ -1766,7 +1820,9 @@ void ComposeControls::updateFieldPlaceholder() { } _field->setPlaceholder([&] { - if (isEditingMessage()) { + if (_fieldCustomPlaceholder) { + return rpl::duplicate(_fieldCustomPlaceholder); + } else if (isEditingMessage()) { return tr::lng_edit_message_text(); } else if (!_history) { return tr::lng_message_ph(); @@ -1804,7 +1860,8 @@ void ComposeControls::fieldChanged() { && !_header->isEditingMessage() && (_textUpdateEvents & TextUpdateEvent::SendTyping)); updateSendButtonType(); - if (!HasSendText(_field) && _preview) { + _hasSendText = HasSendText(_field); + if (!_hasSendText.current() && _preview) { _preview->setState(Data::PreviewState::Allowed); } if (updateBotCommandShown()) { @@ -1967,9 +2024,12 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { updateControlsGeometry(_wrap->size()); }); + const auto hadFocus = Ui::InFocusChain(_field); if (!draft) { clearFieldText(0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } _header->editMessage({}); _header->replyToMessage({}); _canReplaceMedia = false; @@ -1979,7 +2039,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { _textUpdateEvents = 0; setFieldText(draft->textWithTags, 0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } draft->cursor.applyTo(_field); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; if (_preview) { @@ -2134,7 +2196,7 @@ void ComposeControls::initSendAsButton(not_null peer) { updateControlsGeometry(_wrap->size()); orderControls(); } - }, _wrap->lifetime()); + }, _historyLifetime); updateSendAsButton(); } @@ -2207,18 +2269,20 @@ void ComposeControls::initVoiceRecordBar() { _voiceRecordBar->recordingStateChanges( ) | rpl::start_with_next([=](bool active) { if (active) { + _recording = true; changeFocusedControl(); } _field->setVisible(!active); if (!active) { changeFocusedControl(); + _recording = false; } }, _wrap->lifetime()); _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { const auto peer = _history ? _history->peer.get() : nullptr; - if (!peer) { + if (peer) { if (const auto error = Data::RestrictionError( peer, ChatRestriction::SendVoiceMessages)) { @@ -2228,7 +2292,7 @@ void ComposeControls::initVoiceRecordBar() { return std::nullopt; }(); if (error) { - _show->showBox(Ui::MakeInformBox(*error)); + _show->showToast(*error); return true; } else if (_showSlowmodeError && _showSlowmodeError()) { return true; @@ -2236,26 +2300,6 @@ void ComposeControls::initVoiceRecordBar() { return false; }); - { - auto geometry = rpl::merge( - _wrap->geometryValue(), - _send->geometryValue() - ) | rpl::map([=](QRect geometry) { - auto r = _send->geometry(); - r.setY(r.y() + _wrap->y()); - return r; - }); - _voiceRecordBar->setSendButtonGeometryValue(std::move(geometry)); - } - - { - auto bottom = _wrap->geometryValue( - ) | rpl::map([=](QRect geometry) { - return geometry.y() - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(bottom)); - } - _voiceRecordBar->updateSendButtonTypeRequests( ) | rpl::start_with_next([=] { updateSendButtonType(); @@ -2364,9 +2408,11 @@ void ComposeControls::updateControlsGeometry(QSize size) { right += _send->width(); _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); - _botCommandStart->moveToRight(right, buttonsTop); - if (_botCommandShown) { - right += _botCommandStart->width(); + if (_botCommandStart) { + _botCommandStart->moveToRight(right, buttonsTop); + if (_botCommandShown) { + right += _botCommandStart->width(); + } } if (_silent) { _silent->moveToRight(right, buttonsTop); @@ -2383,7 +2429,9 @@ void ComposeControls::updateControlsGeometry(QSize size) { } void ComposeControls::updateControlsVisibility() { - _botCommandStart->setVisible(_botCommandShown); + if (_botCommandStart) { + _botCommandStart->setVisible(_botCommandShown); + } if (_ttlInfo) { _ttlInfo->show(); } @@ -2401,7 +2449,8 @@ void ComposeControls::updateControlsVisibility() { bool ComposeControls::updateBotCommandShown() { auto shown = false; const auto peer = _history ? _history->peer.get() : nullptr; - if (peer + if (_botCommandStart + && peer && ((peer->isChat() && peer->asChat()->botStatus > 0) || (peer->isMegagroup() && peer->asChannel()->mgInfo->botStatus > 0) || (peer->isUser() && peer->asUser()->isBot()))) { @@ -2431,7 +2480,9 @@ void ComposeControls::updateOuterGeometry(QRect rect) { void ComposeControls::updateMessagesTTLShown() { const auto peer = _history ? _history->peer.get() : nullptr; - const auto shown = peer && (peer->messagesTTL() > 0); + const auto shown = _features.ttlInfo + && peer + && (peer->messagesTTL() > 0); if (!shown && _ttlInfo) { _ttlInfo = nullptr; updateControlsVisibility(); @@ -2448,10 +2499,10 @@ void ComposeControls::updateMessagesTTLShown() { } bool ComposeControls::updateSendAsButton() { - Expects(_history != nullptr); - - const auto peer = _history->peer; - if (!_regularWindow + const auto peer = _history ? _history->peer.get() : nullptr; + if (!_features.sendAs + || !peer + || !_regularWindow || isEditingMessage() || !session().sendAsPeers().shouldChoose(peer)) { if (!_sendAs) { @@ -2467,14 +2518,17 @@ bool ComposeControls::updateSendAsButton() { st::sendAsButton); Ui::SetupSendAsButton( _sendAs.get(), - rpl::single(peer.get()), + rpl::single(peer), _regularWindow); return true; } void ComposeControls::updateAttachBotsMenu() { _attachBotsMenu = nullptr; - if (!_history || !_sendActionFactory || !_regularWindow) { + if (!_features.attachBotsMenu + || !_history + || !_sendActionFactory + || !_regularWindow) { return; } _attachBotsMenu = InlineBots::MakeAttachBotsMenu( @@ -2498,7 +2552,18 @@ void ComposeControls::updateAttachBotsMenu() { void ComposeControls::paintBackground(QRect clip) { Painter p(_wrap.get()); - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + //p.setCompositionMode(QPainter::CompositionMode_Source); + //p.fillRect(clip, Qt::transparent); + //p.setCompositionMode(QPainter::CompositionMode_SourceOver); + //_backgroundRect->paint(p, _wrap->rect()); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(_st.bg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(_wrap->rect(), _st.radius, _st.radius); + } else { + p.fillRect(clip, _st.bg); + } } void ComposeControls::escape() { @@ -2538,13 +2603,21 @@ bool ComposeControls::returnTabbedSelector() { } void ComposeControls::createTabbedPanel() { - auto descriptor = ChatHelpers::TabbedPanelDescriptor{ + using namespace ChatHelpers; + auto descriptor = TabbedPanelDescriptor{ .regularWindow = _regularWindow, - .nonOwnedSelector = _selector, + .ownedSelector = (_ownedSelector + ? object_ptr::fromRaw(_ownedSelector.release()) + : object_ptr(nullptr)), + .nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(), }; - setTabbedPanel(std::make_unique( + setTabbedPanel(std::make_unique( _parent, std::move(descriptor))); + _tabbedPanel->setDesiredHeightValues( + st::emojiPanHeightRatio, + _st.tabbedHeightMin, + _st.tabbedHeightMax); } void ComposeControls::setTabbedPanel( @@ -2724,24 +2797,24 @@ void ComposeControls::replyToMessage(FullMsgId id) { } void ComposeControls::cancelReplyMessage() { - Expects(_history != nullptr); - const auto wasReply = replyingToMessage(); _header->replyToMessage({}); - const auto key = draftKey(DraftType::Normal); - if (const auto localDraft = _history->draft(key)) { - if (localDraft->msgId) { - if (localDraft->textWithTags.text.isEmpty()) { - _history->clearDraft(key); - } else { - localDraft->msgId = 0; + if (_history) { + const auto key = draftKey(DraftType::Normal); + if (const auto localDraft = _history->draft(key)) { + if (localDraft->msgId) { + if (localDraft->textWithTags.text.isEmpty()) { + _history->clearDraft(key); + } else { + localDraft->msgId = 0; + } } } - } - if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + if (wasReply) { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); + } } } @@ -2776,16 +2849,26 @@ bool ComposeControls::handleCancelRequest() { return false; } -void ComposeControls::initWebpageProcess() { - Expects(_history); +void ComposeControls::tryProcessKeyInput(not_null e) { + if (_field->isVisible()) { + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + } +} + +void ComposeControls::initWebpageProcess() { + if (!_history) { + _preview = nullptr; + _header->previewUnregister(); + return; + } - auto &lifetime = _wrap->lifetime(); _preview = std::make_unique(_history, _field); _preview->paintRequests( ) | rpl::start_with_next(crl::guard(_header.get(), [=] { _header->update(); - }), lifetime); + }), _historyLifetime); session().changes().peerUpdates( Data::PeerUpdate::Flag::Rights @@ -2814,7 +2897,7 @@ void ComposeControls::initWebpageProcess() { updateControlsGeometry(_wrap->size()); } } - }, lifetime); + }, _historyLifetime); _header->previewRequested( _preview->titleChanges(), @@ -2880,6 +2963,25 @@ bool ComposeControls::isRecording() const { return _voiceRecordBar->isRecording(); } +bool ComposeControls::isRecordingPressed() const { + return !_voiceRecordBar->isRecordingLocked() + && (!_voiceRecordBar->isHidden() + || (_send->type() == Ui::SendButton::Type::Record + && _send->isDown())); +} + +rpl::producer ComposeControls::recordingActiveValue() const { + return _voiceRecordBar->shownValue(); +} + +rpl::producer ComposeControls::hasSendTextValue() const { + return _hasSendText.value(); +} + +rpl::producer ComposeControls::fieldMenuShownValue() const { + return _field->menuShownValue(); +} + bool ComposeControls::preventsClose(Fn &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); @@ -2889,7 +2991,7 @@ bool ComposeControls::preventsClose(Fn &&continueCallback) const { } bool ComposeControls::hasSilentBroadcastToggle() const { - if (!_history) { + if (!_features.silentBroadcastToggle || !_history) { return false; } const auto &peer = _history->peer; @@ -2981,9 +3083,9 @@ void ComposeControls::applyInlineBotQuery( if (result.open) { const auto request = result.result->openRequest(); if (const auto photo = request.photo()) { - _regularWindow->openPhoto(photo, {}, {}); + _regularWindow->openPhoto(photo, {}); } else if (const auto document = request.document()) { - _regularWindow->openDocument(document, {}, {}); + _regularWindow->openDocument(document, false, {}); } } else { _inlineResultChosen.fire_copy(result); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index c09b938d6..de7002012 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -7,12 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "base/required.h" #include "api/api_common.h" +#include "base/required.h" #include "base/unique_qptr.h" #include "base/timer.h" +#include "chat_helpers/compose/compose_features.h" #include "dialogs/dialogs_key.h" #include "history/view/controls/compose_controls_common.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/widgets/input_fields.h" @@ -21,6 +23,10 @@ class History; class DocumentData; class FieldAutocomplete; +namespace style { +struct ComposeControls; +} // namespace style + namespace SendMenu { enum class Type; } // namespace SendMenu @@ -89,12 +95,17 @@ enum class ComposeControlsMode { }; struct ComposeControlsDescriptor { + const style::ComposeControls *stOverride = nullptr; std::shared_ptr show; Fn)> unavailableEmojiPasted; ComposeControlsMode mode = ComposeControlsMode::Normal; SendMenu::Type sendMenuType = {}; Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; + rpl::producer customPlaceholder; + QString voiceCustomCancelText; + bool voiceLockFromBottom = false; + ChatHelpers::ComposeFeatures features; }; class ComposeControls final { @@ -136,6 +147,8 @@ public: [[nodiscard]] int heightCurrent() const; bool focus(); + [[nodiscard]] rpl::producer focusedValue() const; + [[nodiscard]] rpl::producer tabbedPanelShownValue() const; [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer sendRequests() const; [[nodiscard]] rpl::producer sendVoiceRequests() const; @@ -189,6 +202,7 @@ public: void cancelForward(); bool handleCancelRequest(); + void tryProcessKeyInput(not_null e); [[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const; [[nodiscard]] WebPageId webPageId() const; @@ -203,6 +217,10 @@ public: [[nodiscard]] rpl::producer lockShowStarts() const; [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isRecording() const; + [[nodiscard]] bool isRecordingPressed() const; + [[nodiscard]] rpl::producer recordingActiveValue() const; + [[nodiscard]] rpl::producer hasSendTextValue() const; + [[nodiscard]] rpl::producer fieldMenuShownValue() const; void applyCloudDraft(); void applyDraft( @@ -310,12 +328,14 @@ private: void registerDraftSource(); void changeFocusedControl(); + const style::ComposeControls &_st; + const ChatHelpers::ComposeFeatures _features; const not_null _parent; const std::shared_ptr _show; const not_null _session; Window::SessionController * const _regularWindow = nullptr; - const std::unique_ptr _ownedSelector; + std::unique_ptr _ownedSelector; const not_null _selector; rpl::event_stream _stickerOrEmojiChosen; @@ -331,12 +351,15 @@ private: const std::unique_ptr _wrap; const std::unique_ptr _writeRestricted; + std::optional _backgroundRect; + const std::shared_ptr _send; const not_null _attachToggle; std::unique_ptr _replaceMedia; const not_null _tabbedSelectorToggle; + rpl::producer _fieldCustomPlaceholder; const not_null _field; - const not_null _botCommandStart; + Ui::IconButton * const _botCommandStart = nullptr; std::unique_ptr _sendAs; std::unique_ptr _silent; std::unique_ptr _ttlInfo; @@ -365,6 +388,8 @@ private: rpl::event_stream> _attachRequests; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; + rpl::variable _recording; + rpl::variable _hasSendText; TextUpdateEvents _textUpdateEvents = TextUpdateEvents() | TextUpdateEvent::SaveDraft @@ -389,9 +414,11 @@ private: std::unique_ptr _preview; - rpl::lifetime _uploaderSubscriptions; - Fn _raiseEmojiSuggestions; + rpl::event_stream _focusChanges; + + rpl::lifetime _historyLifetime; + rpl::lifetime _uploaderSubscriptions; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 45ba94da2..0786b0745 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 30e3b26e0..b00624867 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Controls { namespace { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp index f0b2eda2f..945253ec6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_ttl_validator.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "styles/style_chat_helpers.h" #include "styles/style_chat.h" namespace HistoryView::Controls { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index acf6e466a..6fc441078 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -27,14 +27,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" -#include "styles/style_chat.h" -#include "styles/style_layers.h" -#include "styles/style_media_player.h" #include "ui/controls/send_button.h" #include "ui/effects/animation_value.h" #include "ui/effects/ripple_animation.h" #include "ui/text/format_values.h" #include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" +#include "styles/style_media_player.h" // AyuGram includes #include "ayu/ayu_settings.h" @@ -42,7 +43,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" namespace HistoryView::Controls { - namespace { using SendActionUpdate = VoiceRecordBar::SendActionUpdate; @@ -95,7 +95,6 @@ enum class FilterType { [[nodiscard]] std::unique_ptr ProcessCaptureResult( const ::Media::Capture::Result &data) { auto voiceData = std::make_unique(); - voiceData->duration = Duration(data.samples); voiceData->waveform = data.waveform; voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) @@ -211,6 +210,7 @@ class ListenWrap final { public: ListenWrap( not_null parent, + const style::RecordBar &st, not_null session, ::Media::Capture::Result &&data, const style::font &font); @@ -236,12 +236,12 @@ private: not_null _parent; + const style::RecordBar &_st; const not_null _session; const not_null _document; const std::unique_ptr _voiceData; const std::shared_ptr _mediaView; const std::unique_ptr<::Media::Capture::Result> _data; - const style::IconButton &_stDelete; const base::unique_qptr _delete; const style::font &_durationFont; const QString _duration; @@ -269,17 +269,18 @@ private: ListenWrap::ListenWrap( not_null parent, + const style::RecordBar &st, not_null session, ::Media::Capture::Result &&data, const style::font &font) : _parent(parent) +, _st(st) , _session(session) , _document(DummyDocument(&session->data())) , _voiceData(ProcessCaptureResult(data)) , _mediaView(_document->createMediaView()) , _data(std::make_unique<::Media::Capture::Result>(std::move(data))) -, _stDelete(st::historyRecordDelete) -, _delete(base::make_unique_q(parent, _stDelete)) +, _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) , _duration(Ui::FormatDurationText( float64(_data->samples) / ::Media::Player::kDefaultFrequency)) @@ -304,7 +305,7 @@ void ListenWrap::init() { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto m = _stDelete.width + _waveformBgRect.height() / 2; + const auto m = _st.remove.width + _waveformBgRect.height() / 2; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( style::margins(m, 0, m, 0)); } @@ -324,22 +325,23 @@ void ListenWrap::init() { PainterHighQualityEnabler hq(p); const auto progress = _showProgress.current(); p.setOpacity(progress); + const auto &remove = _st.remove; if (progress > 0. && progress < 1.) { - _stDelete.icon.paint(p, _stDelete.iconPosition, _parent->width()); + remove.icon.paint(p, remove.iconPosition, _parent->width()); } { const auto hideOffset = _isShowAnimation ? 0 : anim::interpolate(kHideWaveformBgOffset, 0, progress); - const auto deleteIconLeft = _stDelete.iconPosition.x(); + const auto deleteIconLeft = remove.iconPosition.x(); const auto bgRectRight = anim::interpolate( deleteIconLeft, - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectLeft = anim::interpolate( _parent->width() - deleteIconLeft - _waveformBgRect.height(), - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectMargins = style::margins( bgRectLeft - hideOffset, @@ -362,7 +364,7 @@ void ListenWrap::init() { p.setOpacity(progress); } p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordCancelActive); + p.setBrush(_st.cancelActive); QPainterPath path; path.setFillRule(Qt::WindingFill); path.addEllipse(bgLeftCircleRect); @@ -610,10 +612,13 @@ rpl::lifetime &ListenWrap::lifetime() { class RecordLock final : public Ui::RippleButton { public: - RecordLock(not_null parent); + RecordLock( + not_null parent, + const style::RecordBarLock &st); void requestPaintProgress(float64 progress); void requestPaintLockToStopProgress(float64 progress); + void setVisibleTopPart(int part); [[nodiscard]] rpl::producer<> locks() const; [[nodiscard]] bool isLocked() const; @@ -632,6 +637,7 @@ private: void setProgress(float64 progress); void startLockingAnimation(float64 to); + const style::RecordBarLock &_st; const QRect _rippleRect; const QPen _arcPen; @@ -639,10 +645,15 @@ private: float64 _lockToStopProgress = 0.; rpl::variable _progress = 0.; + int _visibleTopPart = -1; + }; -RecordLock::RecordLock(not_null parent) -: RippleButton(parent, st::defaultRippleAnimation) +RecordLock::RecordLock( + not_null parent, + const style::RecordBarLock &st) +: RippleButton(parent, st.ripple) +, _st(st) , _rippleRect(QRect( 0, 0, @@ -658,6 +669,10 @@ RecordLock::RecordLock(not_null parent) init(); } +void RecordLock::setVisibleTopPart(int part) { + _visibleTopPart = part; +} + void RecordLock::init() { shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -675,7 +690,13 @@ void RecordLock::init() { paintRequest( ) | rpl::start_with_next([=](const QRect &clip) { + if (!_visibleTopPart) { + return; + } Painter p(this); + if (_visibleTopPart > 0 && _visibleTopPart < height()) { + p.setClipRect(0, 0, width(), _visibleTopPart); + } if (isLocked()) { const auto top = anim::interpolate( 0, @@ -692,12 +713,12 @@ void RecordLock::init() { void RecordLock::drawProgress(Painter &p) { const auto progress = _progress.current(); - const auto &originTop = st::historyRecordLockTop; - const auto &originBottom = st::historyRecordLockBottom; - const auto &originBody = st::historyRecordLockBody; - const auto &shadowTop = st::historyRecordLockTopShadow; - const auto &shadowBottom = st::historyRecordLockBottomShadow; - const auto &shadowBody = st::historyRecordLockBodyShadow; + const auto &originTop = _st.originTop; + const auto &originBottom = _st.originBottom; + const auto &originBody = _st.originBody; + const auto &shadowTop = _st.shadowTop; + const auto &shadowBottom = _st.shadowBottom; + const auto &shadowBody = _st.shadowBody; const auto &shadowMargins = st::historyRecordLockMargin; const auto bottomMargin = anim::interpolate( @@ -744,7 +765,7 @@ void RecordLock::drawProgress(Painter &p) { originBody.fill(p, content); } { - const auto &arrow = st::historyRecordLockArrow; + const auto &arrow = _st.arrow; const auto arrowRect = QRect( inner.x(), content.y() + content.height() - arrow.height() / 2, @@ -796,7 +817,7 @@ void RecordLock::drawProgress(Painter &p) { PainterHighQualityEnabler hq(p); p.translate(inner.topLeft() + lockTranslation); p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordLockIconFg); + p.setBrush(_st.fg); p.drawRoundedRect(blockRect, xRadius, 3); } else { // Paint an animation frame. @@ -849,7 +870,7 @@ void RecordLock::drawProgress(Painter &p) { p.drawImage( inner.topLeft(), - style::colorizeImage(frame, st::historyRecordLockIconFg)); + style::colorizeImage(frame, _st.fg)); } } } @@ -919,7 +940,10 @@ QPoint RecordLock::prepareRippleStartPosition() const { class CancelButton final : public Ui::RippleButton { public: - CancelButton(not_null parent, int height); + CancelButton( + not_null parent, + const style::RecordBar &st, + int height); void requestPaintProgress(float64 progress); @@ -930,6 +954,7 @@ protected: private: void init(); + const style::RecordBar &_st; const int _width; const QRect _rippleRect; @@ -939,8 +964,12 @@ private: }; -CancelButton::CancelButton(not_null parent, int height) -: Ui::RippleButton(parent, st::defaultLightButton.ripple) +CancelButton::CancelButton( + not_null parent, + const style::RecordBar &st, + int height) +: Ui::RippleButton(parent, st.cancelRipple) +, _st(st) , _width(st::historyRecordCancelButtonWidth) , _rippleRect(QRect(0, (height - _width) / 2, _width, _width)) , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now)) { @@ -963,7 +992,7 @@ void CancelButton::init() { paintRipple(p, _rippleRect.x(), _rippleRect.y()); - p.setPen(st::historyRecordCancelButtonFg); + p.setPen(_st.cancelActive); _text.draw( p, 0, @@ -988,24 +1017,25 @@ void CancelButton::requestPaintProgress(float64 progress) { VoiceRecordBar::VoiceRecordBar( not_null parent, - not_null sectionWidget, - std::shared_ptr show, - std::shared_ptr send, - int recorderHeight) + VoiceRecordBarDescriptor &&descriptor) : RpWidget(parent) -, _sectionWidget(sectionWidget) -, _show(std::move(show)) -, _send(send) -, _lock(std::make_unique(sectionWidget)) -, _level(std::make_unique(sectionWidget)) -, _cancel(std::make_unique(this, recorderHeight)) +, _st(descriptor.stOverride ? *descriptor.stOverride : st::defaultRecordBar) +, _outerContainer(descriptor.outerContainer) +, _show(std::move(descriptor.show)) +, _send(std::move(descriptor.send)) +, _lock(std::make_unique(_outerContainer, _st.lock)) +, _level(std::make_unique(_outerContainer, _st)) +, _cancel(std::make_unique(this, _st, descriptor.recorderHeight)) , _startTimer([=] { startRecording(); }) , _message( st::historyRecordTextStyle, - tr::lng_record_cancel(tr::now), + (!descriptor.customCancelText.isEmpty() + ? descriptor.customCancelText + : tr::lng_record_cancel(tr::now)), TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto }) +, _lockFromBottom(descriptor.lockFromBottom) , _cancelFont(st::historyRecordFont) { - resize(QSize(parent->width(), recorderHeight)); + resize(QSize(parent->width(), descriptor.recorderHeight)); init(); hideFast(); } @@ -1015,7 +1045,12 @@ VoiceRecordBar::VoiceRecordBar( std::shared_ptr show, std::shared_ptr send, int recorderHeight) -: VoiceRecordBar(parent, parent, std::move(show), send, recorderHeight) { +: VoiceRecordBar(parent, { + .outerContainer = parent, + .show = std::move(show), + .send = std::move(send), + .recorderHeight = recorderHeight, +}) { } VoiceRecordBar::~VoiceRecordBar() { @@ -1045,14 +1080,32 @@ void VoiceRecordBar::updateMessageGeometry() { } void VoiceRecordBar::updateLockGeometry() { - const auto right = anim::interpolate( - -_lock->width(), - st::historyRecordLockPosition.x(), - _showLockAnimation.value(_lockShowing.current() ? 1. : 0.)); - _lock->moveToRight(right, _lock->y()); + const auto parent = parentWidget(); + const auto me = Ui::MapFrom(_outerContainer, parent, geometry()); + const auto finalTop = me.y() + - st::historyRecordLockPosition.y() + - _lock->height(); + const auto finalRight = _outerContainer->width() + - (me.x() + me.width()) + + st::historyRecordLockPosition.x(); + const auto progress = _showLockAnimation.value( + _lockShowing.current() ? 1. : 0.); + if (_lockFromBottom) { + const auto top = anim::interpolate(me.y(), finalTop, progress); + _lock->moveToRight(finalRight, top); + _lock->setVisibleTopPart(me.y() - top); + } else { + const auto from = -_lock->width(); + const auto right = anim::interpolate(from, finalRight, progress); + _lock->moveToRight(right, finalTop); + } } void VoiceRecordBar::init() { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } + // Keep VoiceRecordBar behind SendButton. rpl::single( ) | rpl::then( @@ -1092,7 +1145,6 @@ void VoiceRecordBar::init() { } _cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0); updateMessageGeometry(); - updateLockGeometry(); }, lifetime()); paintRequest( @@ -1101,7 +1153,11 @@ void VoiceRecordBar::init() { if (_showAnimation.animating()) { p.setOpacity(showAnimationRatio()); } - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + _backgroundRect->paint(p, rect()); + } else { + p.fillRect(clip, _st.bg); + } p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio())); const auto opacity = p.opacity(); @@ -1242,6 +1298,9 @@ void VoiceRecordBar::init() { _cancel->setClickedCallback([=] { hideAnimated(); }); + + initLockGeometry(); + initLevelGeometry(); } void VoiceRecordBar::activeAnimate(bool active) { @@ -1283,22 +1342,28 @@ void VoiceRecordBar::setStartRecordingFilter(Fn &&callback) { _startRecordingFilter = std::move(callback); } -void VoiceRecordBar::setLockBottom(rpl::producer &&bottom) { +void VoiceRecordBar::initLockGeometry() { rpl::combine( - std::move(bottom), - _lock->sizeValue() | rpl::map_to(true) // Dummy value. - ) | rpl::start_with_next([=](int value, bool dummy) { - _lock->moveToLeft(_lock->x(), value - _lock->height()); + _lock->heightValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=] { + updateLockGeometry(); }, lifetime()); } -void VoiceRecordBar::setSendButtonGeometryValue( - rpl::producer &&geometry) { - std::move( - geometry - ) | rpl::start_with_next([=](QRect r) { - const auto center = (r.width() - _level->width()) / 2; - _level->moveToLeft(r.x() + center, r.y() + center); +void VoiceRecordBar::initLevelGeometry() { + rpl::combine( + _send->geometryValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=](QRect send, auto, auto) { + const auto mapped = Ui::MapFrom( + _outerContainer, + _send->parentWidget(), + send); + const auto center = (send.width() - _level->width()) / 2; + _level->moveToLeft(mapped.x() + center, mapped.y() + center); }, lifetime()); } @@ -1455,6 +1520,7 @@ void VoiceRecordBar::stopRecording(StopType type) { } else if (type == StopType::Listen) { _listen = std::make_unique( this, + _st, &_show->session(), std::move(data), _cancelFont); @@ -1468,7 +1534,7 @@ void VoiceRecordBar::stopRecording(StopType type) { void VoiceRecordBar::drawDuration(Painter &p) { const auto duration = FormatVoiceDuration(_recordingSamples); p.setFont(_cancelFont); - p.setPen(st::historyRecordDurationFg); + p.setPen(_st.durationFg); p.drawText(_durationRect, style::al_left, duration); } @@ -1502,11 +1568,7 @@ void VoiceRecordBar::drawRedCircle(Painter &p) { } void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) { - p.setPen( - anim::pen( - st::historyRecordCancel, - st::historyRecordCancelActive, - 1. - recordActive)); + p.setPen(anim::pen(_st.cancel, _st.cancelActive, 1. - recordActive)); const auto opacity = p.opacity(); p.setOpacity(opacity * (1. - _lock->lockToStopProgress())); @@ -1567,6 +1629,10 @@ bool VoiceRecordBar::isRecording() const { return _recording.current(); } +bool VoiceRecordBar::isRecordingLocked() const { + return isRecording() && _lock->isLocked(); +} + bool VoiceRecordBar::isActive() const { return isRecording() || isListenState(); } @@ -1664,8 +1730,8 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) { void VoiceRecordBar::orderControls() { stackUnder(_send.get()); - _level->raise(); _lock->raise(); + _level->raise(); } void VoiceRecordBar::installListenStateFilter() { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index ea48e887a..09c491678 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -11,10 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "history/view/controls/compose_controls_common.h" #include "ui/effects/animations.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" struct VoiceData; +namespace style { +struct RecordBar; +} // namespace style + namespace Ui { class SendButton; } // namespace Ui @@ -34,6 +39,16 @@ class ListenWrap; class RecordLock; class CancelButton; +struct VoiceRecordBarDescriptor { + not_null outerContainer; + std::shared_ptr show; + std::shared_ptr send; + QString customCancelText; + const style::RecordBar *stOverride = nullptr; + int recorderHeight = 0; + bool lockFromBottom = false; +}; + class VoiceRecordBar final : public Ui::RpWidget { public: using SendActionUpdate = Controls::SendActionUpdate; @@ -41,10 +56,7 @@ public: VoiceRecordBar( not_null parent, - not_null sectionWidget, - std::shared_ptr show, - std::shared_ptr send, - int recorderHeight); + VoiceRecordBarDescriptor &&descriptor); VoiceRecordBar( not_null parent, std::shared_ptr show, @@ -75,11 +87,10 @@ public: void requestToSendWithOptions(Api::SendOptions options); - void setLockBottom(rpl::producer &&bottom); - void setSendButtonGeometryValue(rpl::producer &&geometry); void setStartRecordingFilter(Fn &&callback); [[nodiscard]] bool isRecording() const; + [[nodiscard]] bool isRecordingLocked() const; [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isListenState() const; [[nodiscard]] bool isActive() const; @@ -93,6 +104,8 @@ private: }; void init(); + void initLockGeometry(); + void initLevelGeometry(); void updateMessageGeometry(); void updateLockGeometry(); @@ -125,7 +138,8 @@ private: void computeAndSetLockProgress(QPoint globalPos); - const not_null _sectionWidget; + const style::RecordBar &_st; + const not_null _outerContainer; const std::shared_ptr _show; const std::shared_ptr _send; const std::unique_ptr _lock; @@ -159,11 +173,13 @@ private: rpl::event_stream<> _recordingTipRequests; bool _recordingTipRequired = false; + bool _lockFromBottom = false; const style::font &_cancelFont; rpl::lifetime _recordingLifetime; + std::optional _backgroundRect; Ui::Animations::Simple _showLockAnimation; Ui::Animations::Simple _lockToStopAnimation; Ui::Animations::Simple _showListenAnimation; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index f05b3ed44..4dbc3cd10 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" namespace HistoryView::Controls { @@ -47,7 +48,9 @@ auto Blobs() { } // namespace -VoiceRecordButton::VoiceRecordButton(not_null parent) +VoiceRecordButton::VoiceRecordButton( + not_null parent, + const style::RecordBar &st) : AbstractButton(parent) , _blobs(std::make_unique( Blobs(), diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index 750c8abfb..bd3f0150c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -11,17 +11,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/rp_widget.h" -namespace Ui { -namespace Paint { +namespace style { +struct RecordBar; +} // namespace style + +namespace Ui::Paint { class Blobs; -} // namespace Paint -} // namespace Ui +} // namespace Ui::Paint namespace HistoryView::Controls { class VoiceRecordButton final : public Ui::AbstractButton { public: - explicit VoiceRecordButton(not_null parent); + VoiceRecordButton( + not_null parent, + const style::RecordBar &st); ~VoiceRecordButton(); enum class Type { diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 6f6e14d4a..dffcdde95 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/peers/edit_contact_box.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index f2ccf07af..dac19c310 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_types.h" #include "data/data_forum_topic.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_groups.h" #include "data/data_channel.h" #include "data/data_file_click_handler.h" @@ -68,6 +69,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/spellcheck_types.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include @@ -999,7 +1001,9 @@ base::unique_qptr FillContextMenu( list, st::popupMenuWithIcons); - if (request.overSelection && !list->hasCopyRestrictionForSelected()) { + if (request.overSelection + && !list->hasCopyRestrictionForSelected() + && !list->getSelectedText().empty()) { const auto text = request.selectedItems.empty() ? tr::lng_context_copy_selected(tr::now) : tr::lng_context_copy_selected_items(tr::now); @@ -1159,6 +1163,20 @@ void CopyPostLink( : tr::lng_context_about_private_link(tr::now)); } +void CopyStoryLink( + std::shared_ptr show, + FullStoryId storyId) { + const auto session = &show->session(); + const auto maybeStory = session->data().stories().lookup(storyId); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; + QGuiApplication::clipboard()->setText( + session->api().exportDirectStoryLink(story)); + show->showToast(tr::lng_channel_public_link_copied(tr::now)); +} + void AddPollActions( not_null menu, not_null poll, @@ -1227,11 +1245,11 @@ void AddSaveSoundForNotifications( } else if (int(ringtones.list().size()) >= ringtones.maxSavedCount()) { return; } else if (const auto song = document->song()) { - if (song->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else if (const auto voice = document->voice()) { - if (voice->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 84e443d41..d534d9c24 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -15,6 +15,7 @@ struct ReactionId; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Ui { @@ -58,6 +59,9 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context); +void CopyStoryLink( + std::shared_ptr show, + FullStoryId storyId); void AddPollActions( not_null menu, not_null poll, diff --git a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp index 5bd763c48..0ea99bc50 100644 --- a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp +++ b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/toast/toast.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 11d6ebe8e..6873a4a88 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_settings.h" #include "core/click_handler_types.h" +#include "core/file_utilities.h" #include "core/ui_integration.h" #include "main/main_session.h" #include "main/main_domain.h" @@ -248,6 +249,7 @@ TextSelection ShiftItemSelection( QString DateTooltipText(not_null view) { const auto locale = QLocale(); const auto format = QLocale::LongFormat; + const auto item = view->data(); auto dateText = locale.toString(view->dateTime(), format); if (const auto editedDate = view->displayedEditDate()) { dateText += '\n' + tr::lng_edited_date( @@ -255,18 +257,22 @@ QString DateTooltipText(not_null view) { lt_date, locale.toString(base::unixtime::parse(editedDate), format)); } - if (const auto forwarded = view->data()->Get()) { - dateText += '\n' + tr::lng_forwarded_date( - tr::now, - lt_date, - locale.toString(base::unixtime::parse(forwarded->originalDate), format)); - if (forwarded->imported) { - dateText = tr::lng_forwarded_imported(tr::now) - + "\n\n" + dateText; + if (const auto forwarded = item->Get()) { + if (!forwarded->story && forwarded->psaType.isEmpty()) { + dateText += '\n' + tr::lng_forwarded_date( + tr::now, + lt_date, + locale.toString( + base::unixtime::parse(forwarded->originalDate), + format)); + if (forwarded->imported) { + dateText = tr::lng_forwarded_imported(tr::now) + + "\n\n" + dateText; + } } } if (view->isSignedAuthorElided()) { - if (const auto msgsigned = view->data()->Get()) { + if (const auto msgsigned = item->Get()) { dateText += '\n' + tr::lng_signed_author(tr::now, lt_user, msgsigned->author); } @@ -834,14 +840,20 @@ auto Element::contextDependentServiceText() -> TextWithLinks { void Element::validateText() { const auto item = data(); const auto &text = item->_text; - if (_text.isEmpty() == text.empty()) { - return; + const auto media = item->media(); + const auto storyMention = media && media->storyMention(); + if (media && media->storyExpired()) { + _media = nullptr; + if (!storyMention) { + if (_text.isEmpty()) { + setTextWithLinks( + Ui::Text::Italic(u"This story has expired"_q)); + } + return; + } } - const auto context = Core::MarkedTextContext{ - .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; - if (_flags & Flag::ServiceMessage) { + if (_text.isEmpty() == text.empty()) { + } else if (_flags & Flag::ServiceMessage) { const auto contextDependentText = contextDependentServiceText(); const auto &markedText = contextDependentText.text.empty() ? text @@ -849,28 +861,33 @@ void Element::validateText() { const auto &customLinks = contextDependentText.text.empty() ? item->customTextLinks() : contextDependentText.links; - _text.setMarkedText( - st::serviceTextStyle, - markedText, - Ui::ItemTextServiceOptions(), - context); + setTextWithLinks(markedText, customLinks); + } else { + setTextWithLinks(item->translatedTextWithLocalEntities()); + } +} + +void Element::setTextWithLinks( + const TextWithEntities &text, + const std::vector &links) { + const auto context = Core::MarkedTextContext{ + .session = &history()->session(), + .customEmojiRepaint = [=] { customEmojiRepaint(); }, + }; + if (_flags & Flag::ServiceMessage) { + const auto &options = Ui::ItemTextServiceOptions(); + _text.setMarkedText(st::serviceTextStyle, text, options, context); auto linkIndex = 0; - for (const auto &link : customLinks) { + for (const auto &link : links) { // Link indices start with 1. _text.setLink(++linkIndex, link); } } else { + const auto item = data(); + const auto &options = Ui::ItemTextOptions(item); clearSpecialOnlyEmoji(); - const auto context = Core::MarkedTextContext{ - .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; - _text.setMarkedText( - st::messageTextStyle, - item->translatedTextWithLocalEntities(), - Ui::ItemTextOptions(item), - context); - if (!text.empty() && _text.isEmpty()) { + _text.setMarkedText(st::messageTextStyle, text, options, context); + if (!item->_text.empty() && _text.isEmpty()){ // If server has allowed some text that we've trim-ed entirely, // just replace it with something so that UI won't look buggy. _text.setMarkedText( @@ -981,7 +998,9 @@ ClickHandlerPtr Element::fromLink() const { auto &sponsored = session->data().sponsoredMessages(); const auto itemId = my.itemId ? my.itemId : item->fullId(); const auto details = sponsored.lookupDetails(itemId); - if (const auto &hash = details.hash) { + if (!details.externalLink.isEmpty()) { + File::OpenUrl(details.externalLink); + } else if (const auto &hash = details.hash) { Api::CheckChatInvite(window, *hash); } else if (const auto peer = details.peer) { window->showPeerInfo(peer); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index a2b9cb049..3faaa355b 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -536,6 +536,9 @@ private: virtual QSize performCountCurrentSize(int newWidth) = 0; void refreshMedia(Element *replacing); + void setTextWithLinks( + const TextWithEntities &text, + const std::vector &links = {}); struct TextWithLinks { TextWithEntities text; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp index bce9881b4..7fc19532a 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_instance.h" #include "core/application.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index f45f4f62b..9ca933844 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2460,6 +2460,9 @@ void ListWidget::keyPressEvent(QKeyEvent *e) { #endif // Q_OS_MAC } else if (e == QKeySequence::Delete) { _delegate->listDeleteRequest(); + } else if (!(e->modifiers() & ~Qt::ShiftModifier) + && e->key() != Qt::Key_Shift) { + _delegate->listTryProcessKeyInput(e); } else { e->ignore(); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index ad3517723..704fb1685 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -93,6 +93,7 @@ public: virtual bool listScrollTo(int top, bool syntetic = true) = 0; virtual void listCancelRequest() = 0; virtual void listDeleteRequest() = 0; + virtual void listTryProcessKeyInput(not_null e) = 0; virtual rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 5e0f36ddf..ad06f53c4 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" namespace HistoryView { @@ -1776,9 +1777,8 @@ void Message::unloadHeavyPart() { bool Message::showForwardsFromSender( not_null forwarded) const { const auto peer = data()->history()->peer; - return peer->isSelf() - || peer->isRepliesChat() - || forwarded->imported; + return !forwarded->story + && (peer->isSelf() || peer->isRepliesChat() || forwarded->imported); } bool Message::hasFromPhoto() const { @@ -2268,7 +2268,8 @@ bool Message::getStateReplyInfo( if (auto reply = displayedReply()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); if (point.y() >= trect.top() && point.y() < trect.top() + h) { - if (reply->replyToMsg && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { + if ((reply->replyToMsg || reply->replyToStory) + && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { outResult->link = reply->replyToLink(); } return true; @@ -2875,7 +2876,9 @@ bool Message::displayFromName() const { bool Message::displayForwardedFrom() const { const auto item = data(); if (const auto forwarded = item->Get()) { - if (showForwardsFromSender(forwarded)) { + if (forwarded->story) { + return true; + } else if (showForwardsFromSender(forwarded)) { return false; } if (const auto sender = item->discussionPostOriginalSender()) { @@ -3683,6 +3686,9 @@ bool Message::needInfoDisplay() const { bool Message::hasVisibleText() const { if (data()->emptyText()) { + if (const auto media = data()->media()) { + return media->storyExpired(); + } return false; } const auto media = this->media(); @@ -3711,6 +3717,9 @@ void Message::refreshInfoSkipBlock() { const auto media = this->media(); const auto hasTextSkipBlock = [&] { if (item->_text.empty()) { + if (const auto media = data()->media()) { + return media->storyExpired(); + } return false; } else if (item->Has()) { return false; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index 7ec3b365c..037819498 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 0fc999bec..0fbaf0349 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -496,6 +497,9 @@ void PinnedWidget::listDeleteRequest() { confirmDeleteSelected(); } +void PinnedWidget::listTryProcessKeyInput(not_null e) { +} + rpl::producer PinnedWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -642,14 +646,14 @@ void PinnedWidget::listShowPremiumToast(not_null document) { void PinnedWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, MsgId()); + controller()->openPhoto(photo, { context }); } void PinnedWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, MsgId(), showInMediaView); + controller()->openDocument(document, showInMediaView, { context }); } void PinnedWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 1ee0a9b7d..7125a1f18 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -82,6 +82,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 22145b376..7b878ec52 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -97,17 +98,6 @@ namespace { constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200); -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - rpl::producer RootViewContent( not_null history, MsgId rootId, @@ -819,7 +809,7 @@ void RepliesWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, @@ -1012,7 +1002,7 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage().msg == action.replyTo) { + if (_composeControls->replyingToMessage().msg == action.replyTo.msgId) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } @@ -1123,7 +1113,8 @@ bool RepliesWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } @@ -1134,8 +1125,7 @@ bool RepliesWidget::showSendingFilesError( Api::SendAction RepliesWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = _rootId; + result.replyTo = { .msgId = replyToId(), .topicRootId = _rootId }; result.options.sendAs = _composeControls->sendAsPeer(); return result; } @@ -1176,7 +1166,7 @@ void RepliesWidget::send(Api::SendOptions options) { const auto webPageId = _composeControls->webPageId(); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -2393,6 +2383,10 @@ void RepliesWidget::listDeleteRequest() { confirmDeleteSelected(); } +void RepliesWidget::listTryProcessKeyInput(not_null e) { + _composeControls->tryProcessKeyInput(e); +} + rpl::producer RepliesWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -2517,8 +2511,7 @@ void RepliesWidget::listSendBotCommand( _history->peer, command, context); - auto message = ApiWrap::MessageToSend( - prepareSendAction({})); + auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { text }; session().api().sendMessage(std::move(message)); finishSending(); @@ -2564,14 +2557,17 @@ void RepliesWidget::listShowPremiumToast(not_null document) { void RepliesWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, _rootId); + controller()->openPhoto(photo, { context, _rootId }); } void RepliesWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, _rootId, showInMediaView); + controller()->openDocument( + document, + showInMediaView, + { context, _rootId }); } void RepliesWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index a8645a5c5..b048dca36 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -126,6 +126,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 85ce03471..602f46ae2 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -65,20 +66,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace HistoryView { -namespace { - -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - -} // namespace object_ptr ScheduledMemento::createWidget( QWidget *parent, @@ -308,7 +295,7 @@ void ScheduledWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, @@ -559,7 +546,8 @@ bool ScheduledWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } @@ -599,7 +587,7 @@ void ScheduledWidget::send() { void ScheduledWidget::send(Api::SendOptions options) { const auto webPageId = _composeControls->webPageId(); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -1090,6 +1078,10 @@ void ScheduledWidget::listDeleteRequest() { confirmDeleteSelected(); } +void ScheduledWidget::listTryProcessKeyInput(not_null e) { + _composeControls->tryProcessKeyInput(e); +} + rpl::producer ScheduledWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -1247,7 +1239,7 @@ void ScheduledWidget::listSendBotCommand( _history->peer, command, context); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = { text }; session().api().sendMessage(std::move(message)); }; @@ -1302,14 +1294,14 @@ void ScheduledWidget::listShowPremiumToast( void ScheduledWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, MsgId()); + controller()->openPhoto(photo, { context }); } void ScheduledWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, MsgId(), showInMediaView); + controller()->openDocument(document, showInMediaView, { context }); } void ScheduledWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index acbec32d8..2173d3dcc 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -104,6 +104,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 456e5cd87..4207c59ca 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -284,7 +284,7 @@ void TranslateBar::setup(not_null history) { button->paintRequest( ) | rpl::start_with_next([=](QRect clip) { - QPainter(button).fillRect(clip, st::historyComposeButton.bgColor); + QPainter(button).fillRect(clip, st::historyComposeButtonBg); }, button->lifetime()); button->setClickedCallback([=] { diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp index 1a2db4d6b..71b258ed9 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_invite.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "core/file_utilities.h" #include "data/data_cloud_themes.h" #include "data/data_session.h" #include "data/data_sponsored_messages.h" @@ -42,6 +43,8 @@ inline auto SponsoredPhrase(SponsoredType type) { case SponsoredType::Broadcast: return tr::lng_view_button_channel; case SponsoredType::Post: return tr::lng_view_button_message; case SponsoredType::Bot: return tr::lng_view_button_bot; + case SponsoredType::ExternalLink: + return tr::lng_view_button_external_link; } Unexpected("SponsoredType in SponsoredPhrase."); }(); @@ -52,6 +55,8 @@ inline auto WebPageToPhrase(not_null webpage) { const auto type = webpage->type; return Ui::Text::Upper((type == WebPageType::Theme) ? tr::lng_view_button_theme(tr::now) + : (type == WebPageType::Story) + ? tr::lng_view_button_story(tr::now) : (type == WebPageType::Message) ? tr::lng_view_button_message(tr::now) : (type == WebPageType::Group) @@ -113,6 +118,7 @@ struct ViewButton::Inner { const ClickHandlerPtr link; const Fn updateCallback; bool belowInfo = true; + bool externalLink = false; int lastWidth = 0; QPoint lastPoint; std::unique_ptr ripple; @@ -139,6 +145,8 @@ bool ViewButton::MediaHasViewButton( || ((type == WebPageType::Theme) && webpage->document && webpage->document->isTheme()) + || ((type == WebPageType::Story) + && (webpage->photo || webpage->document)) || ((type == WebPageType::WallPaper) && webpage->document && webpage->document->isWallPaper()); @@ -154,7 +162,9 @@ ViewButton::Inner::Inner( const auto &data = controller->session().data(); const auto itemId = my.itemId; const auto details = data.sponsoredMessages().lookupDetails(itemId); - if (details.hash) { + if (!details.externalLink.isEmpty()) { + File::OpenUrl(details.externalLink); + } else if (details.hash) { Api::CheckChatInvite(controller, *details.hash); } else if (details.peer) { controller->showPeerHistory( @@ -165,6 +175,7 @@ ViewButton::Inner::Inner( } })) , updateCallback(std::move(updateCallback)) +, externalLink(sponsored->type == SponsoredType::ExternalLink) , text(st::historyViewButtonTextStyle, SponsoredPhrase(sponsored->type)) { } @@ -256,6 +267,17 @@ void ViewButton::draw( r.width(), 1, style::al_center); + + if (_inner->externalLink) { + const auto &icon = st::msgBotKbUrlIcon; + const auto padding = st::msgBotKbIconPadding; + icon.paint( + p, + r.left() + r.width() - icon.width() - padding, + r.top() + padding, + r.width(), + stm->fwdTextPalette.linkFg->c); + } } p.restore(); if (_inner->lastWidth != r.width()) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 19832fc23..d1ddb2b96 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -172,7 +172,7 @@ void PaintWaveform( accumulate_max(result, st::normalFont->width(text)); }; add(FormatDownloadText(document->size, document->size)); - const auto duration = document->getDuration(); + const auto duration = document->duration() / 1000; if (const auto song = document->song()) { add(FormatPlayedText(duration, duration)); add(FormatDurationAndSizeText(duration, document->size)); @@ -1212,14 +1212,16 @@ bool Document::uploading() const { } void Document::setStatusSize(int64 newSize, TimeId realDuration) const { - TimeId duration = _data->isSong() - ? _data->song()->duration - : (_data->isVoiceMessage() - ? _data->voice()->duration - : _transcribedRound - ? _data->round()->duration - : -1); - File::setStatusSize(newSize, _data->size, duration, realDuration); + const auto duration = (_data->isSong() + || _data->isVoiceMessage() + || _transcribedRound) + ? _data->duration() + : -1; + File::setStatusSize( + newSize, + _data->size, + (duration >= 0) ? duration / 1000 : -1, + realDuration); if (auto thumbed = Get()) { if (_statusSize == Ui::FileStatusSizeReady) { thumbed->link = tr::lng_media_download(tr::now).toUpper(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index a395f8f09..d3040ce07 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/effects/spoiler_mess.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_document.h" #include "data/data_file_click_handler.h" @@ -90,6 +91,11 @@ Gif::Gif( , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) , _downloadSize(Ui::FormatSizeText(_data->size)) { + if (const auto media = realParent->media()) { + if (media->storyId()) { + _story = true; + } + } setDocumentLinks(_data, realParent, [=] { if (!_data->createMediaView()->canBePlayed(realParent) || !_data->isAnimation() @@ -129,6 +135,7 @@ Gif::~Gif() { _parent->checkHeavyPart(); } } + togglePollingStory(false); } bool Gif::CanPlayInline(not_null document) { @@ -1427,6 +1434,22 @@ void Gif::dataMediaCreated() const { _dataMedia->videoThumbnailWanted(_realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); +} + +void Gif::togglePollingStory(bool enabled) const { + if (!_story || _pollingStory == enabled) { + return; + } + const auto polling = Data::Stories::Polling::Chat; + const auto media = _parent->data()->media(); + const auto id = media ? media->storyId() : FullStoryId(); + if (!enabled) { + _data->owner().stories().unregisterPolling(id, polling); + } else if (!_data->owner().stories().registerPolling(id, polling)) { + return; + } + _pollingStory = enabled; } bool Gif::uploading() const { @@ -1441,7 +1464,9 @@ void Gif::hideSpoilers() { } bool Gif::needsBubble() const { - if (_data->isVideoMessage()) { + if (_story) { + return true; + } else if (_data->isVideoMessage()) { return false; } else if (!_caption.isEmpty()) { return true; @@ -1600,12 +1625,12 @@ void Gif::setStatusSize(int64 newSize) const { _statusText = Ui::FormatDurationText(-newSize - 1); } else if (_data->isVideoMessage()) { _statusSize = newSize; - _statusText = Ui::FormatDurationText(_data->getDuration()); + _statusText = Ui::FormatDurationText(_data->duration() / 1000); } else { File::setStatusSize( newSize, _data->size, - _data->isVideoFile() ? _data->getDuration() : -2, + _data->isVideoFile() ? (_data->duration() / 1000) : -2, 0); } } @@ -1638,7 +1663,7 @@ void Gif::updateStatusText() const { } statusSize = -1 - int((state.length - position) / state.frequency + 1); } else { - statusSize = -1 - _data->getDuration(); + statusSize = -1 - (_data->duration() / 1000); } } if (statusSize != _statusSize) { @@ -1679,6 +1704,7 @@ void Gif::unloadHeavyPart() { _thumbCache = QImage(); _videoThumbnailFrame = nullptr; _caption.unloadPersistentAnimation(); + togglePollingStory(false); } void Gif::refreshParentId(not_null realParent) { @@ -1808,6 +1834,7 @@ void Gif::setStreamed(std::unique_ptr value) { _streamed = std::move(value); if (set) { history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } else if (removed) { _parent->checkHeavyPart(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 7900f9e46..50c5583ac 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -208,6 +208,8 @@ private: StateRequest request, QPoint position) const; + void togglePollingStory(bool enabled) const; + const not_null _data; Ui::Text::String _caption; std::unique_ptr _streamed; @@ -219,8 +221,10 @@ private: mutable QImage _thumbCache; mutable QImage _roundingMask; mutable std::optional _thumbCacheRounding; - mutable bool _thumbCacheBlurred = false; - mutable bool _thumbIsEllipse = false; + mutable bool _thumbCacheBlurred : 1 = false; + mutable bool _thumbIsEllipse : 1 = false; + mutable bool _story : 1 = false; + mutable bool _pollingStory : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index da0f56871..22b5ef296 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -56,7 +56,7 @@ TimeId DurationForTimestampLinks(not_null document) { && !document->isVoiceMessage()) { return TimeId(0); } - return std::max(document->getDuration(), TimeId(0)); + return std::max(document->duration(), crl::time(0)) / 1000; } QString TimestampLinkBase( diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 78c4be6f3..ed5c49ac9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_photo.h" #include "data/data_photo_media.h" @@ -40,6 +41,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { +constexpr auto kStoryWidth = 720; +constexpr auto kStoryHeight = 1280; + using Data::PhotoSize; } // namespace @@ -67,6 +71,11 @@ Photo::Photo( , _data(photo) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) { + if (const auto media = realParent->media()) { + if (media->storyId()) { + _story = 1; + } + } _caption = createCaption(realParent); create(realParent->fullId()); } @@ -93,6 +102,7 @@ Photo::~Photo() { _parent->checkHeavyPart(); } } + togglePollingStory(false); } void Photo::create(FullMsgId contextId, PeerData *chat) { @@ -137,6 +147,7 @@ void Photo::dataMediaCreated() const { _dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } bool Photo::hasHeavyPart() const { @@ -152,11 +163,28 @@ void Photo::unloadHeavyPart() { } _imageCache = QImage(); _caption.unloadPersistentAnimation(); + togglePollingStory(false); +} + +void Photo::togglePollingStory(bool enabled) const { + const auto pollingStory = (enabled ? 1 : 0); + if (!_story || _pollingStory == pollingStory) { + return; + } + const auto polling = Data::Stories::Polling::Chat; + const auto media = _parent->data()->media(); + const auto id = media ? media->storyId() : FullStoryId(); + if (!enabled) { + _data->owner().stories().unregisterPolling(id, polling); + } else if (!_data->owner().stories().registerPolling(id, polling)) { + return; + } + _pollingStory = pollingStory; } QSize Photo::countOptimalSize() { if (_serviceWidth > 0) { - return { _serviceWidth, _serviceWidth }; + return { int(_serviceWidth), int(_serviceWidth) }; } if (_parent->media() != this) { @@ -167,7 +195,7 @@ QSize Photo::countOptimalSize() { _parent->skipBlockHeight()); } - const auto dimensions = QSize(_data->width(), _data->height()); + const auto dimensions = photoSize(); const auto scaled = CountDesiredMediaSize(dimensions); const auto minWidth = std::clamp( _parent->minWidthForMedia(), @@ -201,7 +229,7 @@ QSize Photo::countOptimalSize() { QSize Photo::countCurrentSize(int newWidth) { if (_serviceWidth) { - return { _serviceWidth, _serviceWidth }; + return { int(_serviceWidth), int(_serviceWidth) }; } const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize); const auto minWidth = std::clamp( @@ -210,7 +238,7 @@ QSize Photo::countCurrentSize(int newWidth) { ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), thumbMaxWidth); - const auto dimensions = QSize(_data->width(), _data->height()); + const auto dimensions = photoSize(); auto pix = CountPhotoMediaSize( CountDesiredMediaSize(dimensions), newWidth, @@ -255,7 +283,11 @@ int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const { } void Photo::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; + } else if (_story && _data->isNull()) { + return; + } ensureDataMediaCreated(); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); @@ -590,11 +622,20 @@ void Photo::paintUserpicFrame( } } +QSize Photo::photoSize() const { + if (_story) { + return { kStoryWidth, kStoryHeight }; + } + return QSize(_data->width(), _data->height()); +} + TextState Photo::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; + } else if (_story && _data->isNull()) { + return result; } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); @@ -657,9 +698,8 @@ TextState Photo::textState(QPoint point, StateRequest request) const { } QSize Photo::sizeForGroupingOptimal(int maxWidth) const { - const auto width = _data->width(); - const auto height = _data->height(); - return { std::max(width, 1), std::max(height, 1) }; + const auto size = photoSize(); + return { std::max(size.width(), 1), std::max(size.height(), 1)}; } QSize Photo::sizeForGrouping(int width) const { @@ -848,8 +888,9 @@ void Photo::validateGroupedCache( return; } - const auto originalWidth = style::ConvertScale(_data->width()); - const auto originalHeight = style::ConvertScale(_data->height()); + const auto unscaled = photoSize(); + const auto originalWidth = style::ConvertScale(unscaled.width()); + const auto originalHeight = style::ConvertScale(unscaled.height()); const auto pixSize = Ui::GetImageScaleSizeForGeometry( { originalWidth, originalHeight }, { width, height }); @@ -905,6 +946,7 @@ void Photo::setStreamed(std::unique_ptr value) { _streamed = std::move(value); if (set) { history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } else if (removed) { _parent->checkHeavyPart(); } @@ -1012,7 +1054,7 @@ void Photo::hideSpoilers() { } bool Photo::needsBubble() const { - if (!_caption.isEmpty()) { + if (_story || !_caption.isEmpty()) { return true; } const auto item = _parent->data(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 9440d1421..94404ddbe 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -158,6 +158,10 @@ private: const PaintContext &context, QPoint photoPosition) const; + [[nodiscard]] QSize photoSize() const; + + void togglePollingStory(bool enabled) const; + const not_null _data; Ui::Text::String _caption; mutable std::shared_ptr _dataMedia; @@ -165,9 +169,11 @@ private: const std::unique_ptr _spoiler; mutable QImage _imageCache; mutable std::optional _imageCacheRounding; - int _serviceWidth : 30 = 0; - mutable int _imageCacheForum : 1 = 0; - mutable int _imageCacheBlurred : 1 = 0; + uint32 _serviceWidth : 28 = 0; + mutable uint32 _imageCacheForum : 1 = 0; + mutable uint32 _imageCacheBlurred : 1 = 0; + mutable uint32 _story : 1 = 0; + mutable uint32 _pollingStory : 1 = 0; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index b991e911f..c08d8fde5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -182,7 +182,7 @@ struct Poll::CloseInformation { }; struct Poll::RecentVoter { - not_null user; + not_null peer; mutable Ui::PeerUserpicView userpic; }; @@ -487,20 +487,20 @@ void Poll::updateRecentVoters() { _recentVoters, sliced, ranges::equal_to(), - &RecentVoter::user); + &RecentVoter::peer); if (changed) { auto updated = ranges::views::all( sliced - ) | ranges::views::transform([](not_null user) { - return RecentVoter{ user }; + ) | ranges::views::transform([](not_null peer) { + return RecentVoter{ peer }; }) | ranges::to_vector; const auto has = hasHeavyPart(); if (has) { for (auto &voter : updated) { const auto i = ranges::find( _recentVoters, - voter.user, - &RecentVoter::user); + voter.peer, + &RecentVoter::peer); if (i != end(_recentVoters)) { voter.userpic = std::move(i->userpic); } @@ -892,7 +892,7 @@ void Poll::paintRecentVoters( auto created = false; for (auto &recent : _recentVoters) { const auto was = !recent.userpic.null(); - recent.user->paintUserpic(p, recent.userpic, x, y, size); + recent.peer->paintUserpic(p, recent.userpic, x, y, size); if (!was && !recent.userpic.null()) { created = true; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 252b1e621..7df009fde 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -54,8 +54,8 @@ QString PremiumGift::title() { return tr::lng_premium_summary_title(tr::now); } -QString PremiumGift::subtitle() { - return FormatGiftMonths(_gift->months()); +TextWithEntities PremiumGift::subtitle() { + return { FormatGiftMonths(_gift->months()) }; } QString PremiumGift::button() { @@ -76,6 +76,10 @@ ClickHandlerPtr PremiumGift::createViewLink() { }); } +int PremiumGift::buttonSkip() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + void PremiumGift::draw( Painter &p, const PaintContext &context, diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index 53431ad2b..f3839291d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -26,8 +26,9 @@ public: int top() override; QSize size() override; QString title() override; - QString subtitle() override; + TextWithEntities subtitle() override; QString button() override; + int buttonSkip() override; void draw( Painter &p, const PaintContext &context, diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index e129d28f5..57369d1fa 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" #include "styles/style_chat.h" #include "styles/style_premium.h" @@ -57,8 +58,15 @@ ServiceBox::ServiceBox( _maxWidth) , _subtitle( st::premiumPreviewAbout.style, - _content->subtitle(), - kDefaultTextOptions, + Ui::Text::Filtered( + _content->subtitle(), + { + EntityType::Bold, + EntityType::StrikeOut, + EntityType::Underline, + EntityType::Italic, + }), + kMarkupTextOptions, _maxWidth) , _size( st::msgServiceGiftBoxSize.width(), @@ -73,8 +81,7 @@ ServiceBox::ServiceBox( + _subtitle.countHeight(_maxWidth) + (_button.empty() ? 0 - : (st::msgServiceGiftBoxButtonMargins.top() - + _button.size.height())) + : (_content->buttonSkip() + _button.size.height())) + st::msgServiceGiftBoxButtonMargins.bottom())) , _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) { } @@ -157,6 +164,11 @@ TextState ServiceBox::textState(QPoint point, StateRequest request) const { if (rect.contains(point)) { result.link = _button.link; _button.lastPoint = point - rect.topLeft(); + } else if (contentRect().contains(point)) { + if (!_contentLink) { + _contentLink = _content->createViewLink(); + } + result.link = _contentLink; } } return result; diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index a986b01a8..1ae0c30ea 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -22,7 +22,10 @@ public: [[nodiscard]] virtual int top() = 0; [[nodiscard]] virtual QSize size() = 0; [[nodiscard]] virtual QString title() = 0; - [[nodiscard]] virtual QString subtitle() = 0; + [[nodiscard]] virtual TextWithEntities subtitle() = 0; + [[nodiscard]] virtual int buttonSkip() { + return top(); + } [[nodiscard]] virtual QString button() = 0; virtual void draw( Painter &p, @@ -84,6 +87,7 @@ private: const not_null _parent; const std::unique_ptr _content; + mutable ClickHandlerPtr _contentLink; struct Button { void drawBg(QPainter &p) const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp new file mode 100644 index 000000000..07a469097 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp @@ -0,0 +1,187 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_story_mention.h" + +#include "core/click_handler_types.h" // ClickHandlerContext +#include "data/data_document.h" +#include "data/data_photo.h" +#include "data/data_user.h" +#include "data/data_photo_media.h" +#include "data/data_file_click_handler.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "editor/photo_editor_common.h" +#include "editor/photo_editor_layer_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/view/media/history_view_sticker_player_abstract.h" +#include "history/view/history_view_element.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "window/window_session_controller.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/outline_segments.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/painter.h" +#include "mainwidget.h" +#include "apiwrap.h" +#include "api/api_peer_photo.h" +#include "settings/settings_information.h" // UpdatePhotoLocally +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +constexpr auto kReadOutlineAlpha = 0.5; + +} // namespace + +StoryMention::StoryMention( + not_null parent, + not_null story) +: _parent(parent) +, _story(story) +, _unread(story->owner().stories().isUnread(story) ? 1 : 0) { +} + +StoryMention::~StoryMention() { + if (_subscribed) { + changeSubscribedTo(0); + _parent->checkHeavyPart(); + } +} + +int StoryMention::top() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +QSize StoryMention::size() { + return { st::msgServicePhotoWidth, st::msgServicePhotoWidth }; +} + +QString StoryMention::title() { + return QString(); +} + +int StoryMention::buttonSkip() { + return st::storyMentionButtonSkip; +} + +QString StoryMention::button() { + return tr::lng_action_story_mention_button(tr::now); +} + +TextWithEntities StoryMention::subtitle() { + return _parent->data()->notificationText(); +} + +ClickHandlerPtr StoryMention::createViewLink() { + const auto itemId = _parent->data()->fullId(); + return std::make_shared(crl::guard(this, [=]( + ClickContext) { + if (const auto photo = _story->photo()) { + _parent->delegate()->elementOpenPhoto(photo, itemId); + } else if (const auto video = _story->document()) { + _parent->delegate()->elementOpenDocument(video, itemId); + } + })); +} + +void StoryMention::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + const auto showStory = _story->forbidsForward() ? 0 : 1; + if (!_thumbnail || _thumbnailFromStory != showStory) { + using namespace Dialogs::Stories; + const auto item = _parent->data(); + const auto history = item->history(); + _thumbnail = showStory + ? MakeStoryThumbnail(_story) + : MakeUserpicThumbnail(item->out() + ? history->session().user() + : history->peer); + _thumbnailFromStory = showStory; + changeSubscribedTo(0); + } + if (changeSubscribedTo(1)) { + _thumbnail->subscribeToUpdates([=] { + _parent->data()->history()->owner().requestViewRepaint(_parent); + }); + } + + const auto padding = (geometry.width() - st::storyMentionSize) / 2; + const auto size = geometry.width() - 2 * padding; + p.drawImage( + geometry.topLeft() + QPoint(padding, padding), + _thumbnail->image(size)); + + const auto thumbnail = QRectF(geometry.marginsRemoved( + QMargins(padding, padding, padding, padding))); + const auto added = 0.5 * (_unread + ? st::storyMentionUnreadSkipTwice + : st::storyMentionReadSkipTwice); + const auto outline = thumbnail.marginsAdded( + QMarginsF(added, added, added, added)); + if (_unread && _paletteVersion != style::PaletteVersion()) { + _paletteVersion = style::PaletteVersion(); + _unreadBrush = QBrush(Ui::UnreadStoryOutlineGradient(outline)); + } + auto readColor = context.st->msgServiceFg()->c; + readColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha)); + p.setPen(QPen( + _unread ? _unreadBrush : QBrush(readColor), + 0.5 * (_unread + ? st::storyMentionUnreadStrokeTwice + : st::storyMentionReadStrokeTwice))); + p.setBrush(Qt::NoBrush); + auto hq = PainterHighQualityEnabler(p); + p.drawEllipse(outline); +} + +void StoryMention::stickerClearLoopPlayed() { +} + +std::unique_ptr StoryMention::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return nullptr; +} + +bool StoryMention::hasHeavyPart() { + return _subscribed != 0; +} + +void StoryMention::unloadHeavyPart() { + if (changeSubscribedTo(0)) { + _thumbnail->subscribeToUpdates(nullptr); + } +} + +bool StoryMention::changeSubscribedTo(uint32 value) { + Expects(value == 0 || value == 1); + + if (_subscribed == value) { + return false; + } + _subscribed = value; + const auto stories = &_parent->history()->owner().stories(); + if (value) { + _parent->history()->owner().registerHeavyViewPart(_parent); + stories->registerPolling(_story, Data::Stories::Polling::Chat); + } else { + stories->unregisterPolling(_story, Data::Stories::Polling::Chat); + } + return true; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h new file mode 100644 index 000000000..51704d440 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h @@ -0,0 +1,71 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_media_unwrapped.h" +#include "history/view/media/history_view_service_box.h" + +namespace Data { +class Story; +} // namespace Data + +namespace Dialogs::Stories { +class Thumbnail; +} // namespace Dialogs::Stories + +namespace HistoryView { + +class StoryMention final + : public ServiceBoxContent + , public base::has_weak_ptr { +public: + StoryMention(not_null parent, not_null story); + ~StoryMention(); + + int top() override; + QSize size() override; + QString title() override; + TextWithEntities subtitle() override; + int buttonSkip() override; + QString button() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override { + return true; + } + + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + using Thumbnail = Dialogs::Stories::Thumbnail; + + bool changeSubscribedTo(uint32 value); + + const not_null _parent; + const not_null _story; + std::shared_ptr _thumbnail; + QBrush _unreadBrush; + uint32 _paletteVersion : 29 = 0; + uint32 _thumbnailFromStory : 1 = 0; + uint32 _subscribed : 1 = 0; + uint32 _unread : 1 = 0; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index 0cd31f483..ad5dfcac1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -460,8 +460,8 @@ QString ThemeDocumentBox::title() { return QString(); } -QString ThemeDocumentBox::subtitle() { - return _parent->data()->notificationText().text; +TextWithEntities ThemeDocumentBox::subtitle() { + return _parent->data()->notificationText(); } QString ThemeDocumentBox::button() { diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index f923ca253..94549e951 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -99,7 +99,7 @@ public: int top() override; QSize size() override; QString title() override; - QString subtitle() override; + TextWithEntities subtitle() override; QString button() override; void draw( Painter &p, diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp index 7056a7780..5c37ccb04 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp @@ -211,8 +211,8 @@ QString UserpicSuggestion::button() { : tr::lng_action_suggested_photo_button(tr::now); } -QString UserpicSuggestion::subtitle() { - return _photo.parent()->data()->notificationText().text; +TextWithEntities UserpicSuggestion::subtitle() { + return _photo.parent()->data()->notificationText(); } ClickHandlerPtr UserpicSuggestion::createViewLink() { diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h index bed87c324..42ad65f44 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h +++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h @@ -30,7 +30,7 @@ public: int top() override; QSize size() override; QString title() override; - QString subtitle() override; + TextWithEntities subtitle() override; QString button() override; void draw( Painter &p, diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 3b909485f..cec353017 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -168,6 +168,7 @@ QSize WebPage::countOptimalSize() { && _data->photo && _data->type != WebPageType::Photo && _data->type != WebPageType::Document + && _data->type != WebPageType::Story && _data->type != WebPageType::Video) { if (_data->type == WebPageType::Profile) { _asArticle = true; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 9160e2703..911472560 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Reactions { namespace { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp index 0117ad4a5..345512ea1 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "base/event_filter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" namespace HistoryView::Reactions { @@ -318,6 +319,7 @@ Manager::Manager( : _outer(CountOuterSize()) , _inner(QRect({}, st::reactionCornerSize)) , _strip( + st::reactPanelEmojiPan, _inner, st::reactionCornerImage, crl::guard(this, [=] { updateCurrentButton(); }), diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index ca6ddf226..4aa2370a8 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "history/history_item.h" #include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "main/main_session.h" @@ -105,48 +106,59 @@ bool StripEmoji::readyInDefaultState() { Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, - Fn close) + Fn close, + bool child) : Selector( parent, - parentController, + st, + std::move(show), reactions, (reactions.customAllowed ? ChatHelpers::EmojiListMode::FullReactions : ChatHelpers::EmojiListMode::RecentReactions), {}, iconFactory, - close) { + close, + child) { } Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, ChatHelpers::EmojiListMode mode, std::vector recent, - Fn close) + Fn close, + bool child) : Selector( parent, - parentController, + st, + std::move(show), { .customAllowed = true }, mode, std::move(recent), nullptr, - close) { + close, + child) { } Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, ChatHelpers::EmojiListMode mode, std::vector recent, IconFactory iconFactory, - Fn close) + Fn close, + bool child) : RpWidget(parent) -, _parentController(parentController.get()) +, _st(st) +, _show(std::move(show)) , _reactions(reactions) , _recent(std::move(recent)) , _listMode(mode) @@ -157,6 +169,7 @@ Selector::Selector( st::reactStripHeight) , _strip(iconFactory ? std::make_unique( + _st, QRect(0, 0, st::reactStripSize, st::reactStripSize), st::reactStripImage, crl::guard(this, [=] { update(_inner); }), @@ -167,12 +180,7 @@ Selector::Selector( , _skipy((st::reactStripHeight - st::reactStripSize) / 2) { setMouseTracking(true); - _useTransparency = Ui::Platform::TranslucentWindowsSupported(); - - parentController->content()->alive( - ) | rpl::start_with_done([=] { - close(true); - }, lifetime()); + _useTransparency = child || Ui::Platform::TranslucentWindowsSupported(); } bool Selector::useTransparency() const { @@ -241,14 +249,14 @@ QMargins Selector::extentsForShadow() const { } int Selector::extendTopForCategories() const { - return _reactions.customAllowed ? st::reactPanelEmojiPan.footer : 0; + return _reactions.customAllowed ? _st.footer : 0; } int Selector::minimalHeight() const { return _skipy + (_recentRows * _size) + st::emojiPanRadius - + st::reactPanelEmojiPan.padding.bottom(); + + _st.padding.bottom(); } void Selector::setSpecialExpandTopSkip(int skip) { @@ -269,7 +277,7 @@ void Selector::initGeometry(int innerTop) { ? (extendTopForCategories() + _specialExpandTopSkip) : 0; const auto top = innerTop - extents.top() - _collapsedTopSkip; - const auto add = st::reactStripBubble.height() - extents.bottom(); + const auto add = _st.icons.stripBubble.height() - extents.bottom(); _outer = QRect(0, _collapsedTopSkip, width, height); _outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add }); setGeometry(_outerWithBubble.marginsAdded( @@ -288,6 +296,10 @@ void Selector::beforeDestroy() { } } +rpl::producer<> Selector::escapes() const { + return _escapes.events(); +} + void Selector::updateShowState( float64 progress, float64 opacity, @@ -325,7 +337,7 @@ void Selector::paintAppearing(QPainter &p) { if (_paintBuffer.size() != _outerWithBubble.size() * factor) { _paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size()); } - _paintBuffer.fill(st::defaultPopupMenu.menu.itemBg->c); + _paintBuffer.fill(_st.bg->c); auto q = QPainter(&_paintBuffer); const auto extents = extentsForShadow(); const auto appearedWidth = anim::interpolate( @@ -344,7 +356,7 @@ void Selector::paintAppearing(QPainter &p) { 1., false); - _cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c); + _cachedRound.setBackgroundColor(_st.bg->c); _cachedRound.setShadowColor(st::shadowFg->c); q.translate(QPoint(0, _collapsedTopSkip) - _inner.topLeft()); const auto radius = st::reactStripHeight / 2; @@ -379,7 +391,7 @@ void Selector::paintBackgroundToBuffer() { } _paintBuffer.fill(Qt::transparent); - _cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c); + _cachedRound.setBackgroundColor(_st.bg->c); _cachedRound.setShadowColor(st::shadowFg->c); auto p = QPainter(&_paintBuffer); @@ -399,7 +411,7 @@ void Selector::paintCollapsed(QPainter &p) { } p.drawImage(_outer.topLeft(), _paintBuffer); } else { - p.fillRect(_inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(_inner, _st.bg); } _strip->paint( p, @@ -422,8 +434,9 @@ void Selector::paintExpanding(Painter &p, float64 progress) { } _list->paintExpanding( p, - rects.list.marginsRemoved(st::reactPanelEmojiPan.margin), + rects.list.marginsRemoved(_st.margin), rects.finalBottom, + rects.expanding, progress, RectPart::TopRight); paintFadingExpandIcon(p, progress); @@ -453,11 +466,11 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress) const auto pattern = _cachedRound.validateFrame(frame, 1., radius); const auto fill = _cachedRound.FillWithImage(p, outer, pattern); if (!fill.isEmpty()) { - p.fillRect(fill, st::defaultPopupMenu.menu.itemBg); + p.fillRect(fill, _st.bg); } } else { const auto inner = outer.marginsRemoved(extentsForShadow()); - p.fillRect(inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(inner, _st.bg); p.fillRect( inner.x(), inner.y() + inner.height(), @@ -479,6 +492,7 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress) .categories = QRect(inner.x(), inner.y(), inner.width(), categories), .list = inner.marginsRemoved({ 0, categories, 0, 0 }), .radius = radius, + .expanding = expanding, .finalBottom = height() - extents.bottom(), }; } @@ -507,7 +521,7 @@ void Selector::paintExpanded(QPainter &p) { p.drawImage(0, 0, _paintBuffer); } else { const auto inner = rect().marginsRemoved(extentsForShadow()); - p.fillRect(inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(inner, _st.bg); p.fillRect( inner.x(), inner.y() + inner.height(), @@ -530,7 +544,7 @@ void Selector::finishExpand() { st::emojiPanRadius); const auto fill = _cachedRound.FillWithImage(q, rect(), pattern); if (!fill.isEmpty()) { - q.fillRect(fill, st::defaultPopupMenu.menu.itemBg); + q.fillRect(fill, _st.bg); } } if (_footer) { @@ -538,14 +552,11 @@ void Selector::finishExpand() { } _scroll->show(); _list->afterShown(); - - if (const auto controller = _parentController.get()) { - controller->session().api().updateCustomEmoji(); - } + _show->session().api().updateCustomEmoji(); } void Selector::paintBubble(QPainter &p, int innerWidth) { - const auto &bubble = st::reactStripBubble; + const auto &bubble = _st.icons.stripBubble; const auto bubbleRight = std::min( st::reactStripBubbleRight, (innerWidth - bubble.width()) / 2); @@ -657,11 +668,30 @@ ChosenReaction Selector::lookupChosen(const Data::ReactionId &id) const { return result; } +void Selector::preloadAllRecentsAnimations() { + const auto preload = [&](DocumentData *document) { + const auto view = document + ? document->activeMediaView() + : nullptr; + if (view) { + view->checkStickerLarge(); + } + }; + for (const auto &reaction : _reactions.recent) { + if (!reaction.id.custom()) { + preload(reaction.centerIcon); + } + preload(reaction.aroundAnimation); + } +} + void Selector::expand() { if (_expandScheduled) { return; } _expandScheduled = true; + _willExpand.fire({}); + preloadAllRecentsAnimations(); const auto parent = parentWidget()->geometry(); const auto extents = extentsForShadow(); const auto heightLimit = _reactions.customAllowed @@ -672,15 +702,14 @@ void Selector::expand() { extents.top() + heightLimit + extents.bottom()); const auto additionalBottom = willBeHeight - height(); const auto additional = _specialExpandTopSkip + additionalBottom; - const auto strong = _parentController.get(); - if (additionalBottom < 0 || additional <= 0 || !strong) { + if (additionalBottom < 0 || additional <= 0) { return; } else if (additionalBottom > 0) { resize(width(), height() + additionalBottom); raise(); } - createList(strong); + createList(); cacheExpandIcon(); [[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll); @@ -705,7 +734,7 @@ void Selector::cacheExpandIcon() { _strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.); } -void Selector::createList(not_null controller) { +void Selector::createList() { using namespace ChatHelpers; auto recent = _recent; auto defaultReactionIds = base::flat_map(); @@ -725,7 +754,7 @@ void Selector::createList(not_null controller) { } }; } - const auto manager = &controller->session().data().customEmojiManager(); + const auto manager = &_show->session().data().customEmojiManager(); _stripPaintOneShift = [&] { // See EmojiListWidget custom emoji position resolving. const auto area = st::emojiPanArea; @@ -769,15 +798,14 @@ void Selector::createList(not_null controller) { _scroll = Ui::CreateChild(this, st::reactPanelScroll); _scroll->hide(); - const auto st = lifetime().make_state( - st::reactPanelEmojiPan); + const auto st = lifetime().make_state(_st); st->padding.setTop(_skipy); if (!_reactions.customAllowed) { st->bg = st::transparent; } _list = _scroll->setOwnedWidget( object_ptr(_scroll, EmojiListDescriptor{ - .show = controller->uiShow(), + .show = _show, .mode = _listMode, .paused = [] { return false; }, .customRecentList = std::move(recent), @@ -786,6 +814,8 @@ void Selector::createList(not_null controller) { }) ).data(); + _list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime()); + _list->customChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { const auto id = DocumentId{ data.document->id }; @@ -830,8 +860,7 @@ void Selector::createList(not_null controller) { }, _shadow->lifetime()); _shadow->show(); } - const auto geometry = inner.marginsRemoved( - st::reactPanelEmojiPan.margin); + const auto geometry = inner.marginsRemoved(_st.margin); _list->move(0, 0); _list->resizeToWidth(geometry.width()); _list->refreshEmoji(); @@ -852,7 +881,7 @@ void Selector::createList(not_null controller) { }, _list->lifetime()); _scroll->setGeometry(inner.marginsRemoved({ - st::reactPanelEmojiPan.margin.left(), + _st.margin.left(), _footer ? _footer->height() : 0, 0, 0, @@ -937,10 +966,12 @@ AttachSelectorResult MakeJustSelectorMenu( Fn chosen) { const auto selector = Ui::CreateChild( menu.get(), - controller, + st::reactPanelEmojiPan, + controller->uiShow(), mode, std::move(recent), - [=](bool fast) { menu->hideMenu(fast); }); + [=](bool fast) { menu->hideMenu(fast); }, + false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { return AttachSelectorResult::Failed; } @@ -1011,10 +1042,12 @@ AttachSelectorResult AttachSelectorToMenu( const auto withSearch = reactions.customAllowed; const auto selector = Ui::CreateChild( menu.get(), - controller, + st::reactPanelEmojiPan, + controller->uiShow(), std::move(reactions), std::move(iconFactory), - [=](bool fast) { menu->hideMenu(fast); }); + [=](bool fast) { menu->hideMenu(fast); }, + false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { return AttachSelectorResult::Failed; } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index 6e6d119cd..b47dfa2d8 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -19,6 +19,7 @@ struct ReactionId; } // namespace Data namespace ChatHelpers { +class Show; class TabbedPanel; class EmojiListWidget; class StickersListFooter; @@ -41,16 +42,20 @@ class Selector final : public Ui::RpWidget { public: Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, - Fn close); + Fn close, + bool child = false); Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, ChatHelpers::EmojiListMode mode, std::vector recent, - Fn close); + Fn close, + bool child = false); [[nodiscard]] bool useTransparency() const; @@ -68,6 +73,10 @@ public: [[nodiscard]] rpl::producer<> premiumPromoChosen() const { return _premiumPromoChosen.events(); } + [[nodiscard]] rpl::producer<> willExpand() const { + return _willExpand.events(); + } + [[nodiscard]] rpl::producer<> escapes() const; void updateShowState( float64 progress, @@ -82,17 +91,20 @@ private: QRect categories; QRect list; float64 radius = 0.; + float64 expanding = 0.; int finalBottom = 0; }; Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, ChatHelpers::EmojiListMode mode, std::vector recent, IconFactory iconFactory, - Fn close); + Fn close, + bool child); void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; @@ -116,11 +128,13 @@ private: void expand(); void cacheExpandIcon(); - void createList(not_null controller); + void createList(); void finishExpand(); ChosenReaction lookupChosen(const Data::ReactionId &id) const; + void preloadAllRecentsAnimations(); - const base::weak_ptr _parentController; + const style::EmojiPan &_st; + const std::shared_ptr _show; const Data::PossibleItemReactions _reactions; const std::vector _recent; const ChatHelpers::EmojiListMode _listMode; @@ -133,6 +147,8 @@ private: rpl::event_stream _chosen; rpl::event_stream<> _premiumPromoChosen; + rpl::event_stream<> _willExpand; + rpl::event_stream<> _escapes; Ui::ScrollArea *_scroll = nullptr; ChatHelpers::EmojiListWidget *_list = nullptr; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp index 25d7fa4dd..f9c27b15c 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/animated_icon.h" #include "ui/painter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Reactions { namespace { @@ -44,11 +45,13 @@ constexpr auto kHoverScale = 1.24; } // namespace Strip::Strip( + const style::EmojiPan &st, QRect inner, int size, Fn update, IconFactory iconFactory) -: _iconFactory(std::move(iconFactory)) +: _st(st) +, _iconFactory(std::move(iconFactory)) , _inner(inner) , _finalSize(size) , _update(std::move(update)) { @@ -173,7 +176,7 @@ void Strip::paintOne( } else { const auto paintFrame = [&](not_null animation) { const auto size = int(std::floor(target.width() + 0.01)); - const auto &textColor = st::windowFg->c; + const auto &textColor = _st.textFg->c; const auto frame = animation->frame( textColor, { size, size }, @@ -238,9 +241,9 @@ int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const { } const auto &icon = *i; if (const auto &appear = icon.appear; appear && appear->animating()) { - chosen.icon = appear->frame(st::windowFg->c); + chosen.icon = appear->frame(_st.textFg->c); } else if (const auto &select = icon.select; select && select->valid()) { - chosen.icon = select->frame(st::windowFg->c); + chosen.icon = select->frame(_st.textFg->c); } return (i - begin(_icons)); } @@ -263,7 +266,7 @@ void Strip::paintPremiumIcon( p.translate(-target.center()); } auto hq = PainterHighQualityEnabler(p); - st::reactionPremiumLocked.paintInCenter(p, to); + _st.icons.stripPremiumLocked.paintInCenter(p, to); if (scale != 1.) { p.restore(); } @@ -288,8 +291,8 @@ void Strip::paintExpandIcon( } auto hq = PainterHighQualityEnabler(p); ((_finalSize == st::reactionCornerImage) - ? st::reactionsExpandDropdown - : st::reactionExpandPanel).paintInCenter(p, to); + ? _st.icons.stripExpandDropdown + : _st.icons.stripExpandPanel).paintInCenter(p, to); if (scale != 1.) { p.restore(); } @@ -495,7 +498,7 @@ void Strip::setMainReactionIcon() { if (i != end(_loadCache) && i->second.icon) { const auto &icon = i->second.icon; if (!icon->frameIndex() && icon->width() == MainReactionSize()) { - _mainReactionImage = i->second.icon->frame(st::windowFg->c); + _mainReactionImage = i->second.icon->frame(_st.textFg->c); return; } } @@ -543,7 +546,7 @@ Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) { if (_mainReactionImage.isNull() && _mainReactionIcon) { _mainReactionImage = base::take(_mainReactionIcon)->frame( - st::windowFg->c); + _st.textFg->c); } if (!_mainReactionImage.isNull()) { const auto target = QRect( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h index 9447b0d77..e8a98fd49 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/round_area_with_shadow.h" #include "data/data_message_reaction_id.h" +namespace style { +struct EmojiPan; +} // namespace style + class HistoryItem; namespace Data { @@ -44,7 +48,12 @@ class Strip final { public: using ReactionId = Data::ReactionId; - Strip(QRect inner, int size, Fn update, IconFactory iconFactory); + Strip( + const style::EmojiPan &st, + QRect inner, + int size, + Fn update, + IconFactory iconFactory); enum class AddedButton : uchar { None, @@ -121,6 +130,7 @@ private: void resolveMainReactionIcon(); void setMainReactionIcon(); + const style::EmojiPan &_st; const IconFactory _iconFactory; const QRect _inner; const int _finalSize = 0; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 26a46d22a..6efd528f6 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null controller) } Memento::Memento(not_null self) -: ContentMemento(Downloads::Tag{}) +: ContentMemento(Tag{}) , _media(self, 0, Media::Type::File) { } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index d4c146ce7..c5567dd79 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -28,6 +28,25 @@ InfoPeerBadge { sizeTag: int; } +InfoTopBar { + height: pixels; + back: IconButton; + title: FlatLabel; + titlePosition: point; + bg: color; + mediaCancel: IconButton; + mediaActionsSkip: pixels; + mediaForward: IconButton; + mediaDelete: IconButton; + storiesSave: IconButton; + storiesArchive: IconButton; + search: IconButton; + searchRow: SearchFieldRow; + highlightBg: color; + highlightDuration: int; + radius: pixels; +} + infoMediaHeaderFg: windowFg; infoToggle: InfoToggle { @@ -156,6 +175,14 @@ infoTopBarDelete: IconButton(infoTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoTopBarSaveStories: IconButton(infoTopBarForward) { + icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }}; +} +infoTopBarArchiveStories: IconButton(infoTopBarForward) { + icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; +} infoTopBar: InfoTopBar { height: infoTopBarHeight; back: infoTopBarBack; @@ -166,6 +193,8 @@ infoTopBar: InfoTopBar { mediaActionsSkip: 4px; mediaForward: infoTopBarForward; mediaDelete: infoTopBarDelete; + storiesSave: infoTopBarSaveStories; + storiesArchive: infoTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; highlightBg: windowBgOver; @@ -221,6 +250,14 @@ infoLayerTopBarDelete: IconButton(infoLayerTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }}; +} +infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; +} infoLayerTopBar: InfoTopBar(infoTopBar) { height: infoLayerTopBarHeight; back: infoLayerTopBarBack; @@ -231,6 +268,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { mediaActionsSkip: 6px; mediaForward: infoLayerTopBarForward; mediaDelete: infoLayerTopBarDelete; + storiesSave: infoLayerTopBarSaveStories; + storiesArchive: infoLayerTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; radius: boxRadius; @@ -367,6 +406,9 @@ infoIconMediaAudio: icon {{ "info/info_media_audio", infoIconFg }}; infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }}; infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }}; infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }}; +infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }}; +infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; +infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }}; infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }}; @@ -389,22 +431,6 @@ infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 19px); -infoLabeledOneLine: FlatLabel(defaultFlatLabel) { - maxHeight: 20px; - style: TextStyle(defaultTextStyle) { - lineHeight: 19px; - } - margin: margins(5px, 5px, 5px, 5px); -} -infoLabelSkip: 2px; -infoLabeled: FlatLabel(infoLabeledOneLine) { - minWidth: 180px; - maxHeight: 0px; - margin: margins(5px, 5px, 5px, 5px); -} -infoLabel: FlatLabel(infoLabeled) { - textFg: windowSubTextFg; -} infoBlockHeaderLabel: FlatLabel(infoProfileStatus) { textFg: windowBoldFg; @@ -462,6 +488,15 @@ infoMembersList: PeerList(defaultPeerList) { photoPosition: point(18px, 6px); namePosition: point(79px, 11px); statusPosition: point(79px, 31px); + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; } } infoMembersButtonPosition: point(12px, 0px); @@ -509,7 +544,8 @@ infoMediaHeaderStyle: TextStyle(semiboldTextStyle) { } infoMediaHeaderHeight: 28px; infoMediaHeaderPosition: point(14px, 6px); -infoMediaSkip: 5px; +infoMediaSkip: 2px; +infoMediaLeft: 3px; infoMediaMargin: margins(0px, 6px, 0px, 2px); infoMediaMinGridSize: 90px; @@ -589,6 +625,7 @@ infoEmptyAudio: icon {{ "info/info_media_audio_empty", infoEmptyFg }}; infoEmptyFile: icon {{ "info/info_media_file_empty", infoEmptyFg }}; infoEmptyVoice: icon {{ "info/info_media_voice_empty", infoEmptyFg }}; infoEmptyLink: icon {{ "info/info_media_link_empty", infoEmptyFg }}; +infoEmptyStories: icon {{ "info/info_media_story_empty", infoEmptyFg }}; infoEmptyIconTop: 120px; infoEmptyLabelTop: 40px; infoEmptyLabelSkip: 20px; @@ -597,6 +634,14 @@ infoEmptyLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } +infoStoriesAboutArchive: FlatLabel(defaultFlatLabel) { + minWidth: 245px; + align: align(top); + textFg: windowSubTextFg; + style: defaultTextStyle; +} +infoStoriesAboutArchivePadding: margins(22px, 12px, 22px, 12px); + editPeerBottomButtonsLayoutMargins: margins(0px, 7px, 0px, 0px); editPeerTopButtonsLayoutSkip: 13px; @@ -888,10 +933,4 @@ shortInfoCover: ShortInfoCover { } } -reportReasonTopSkip: 8px; -reportReasonButton: SettingsButton(infoProfileButton) { - style: boxTextStyle; - padding: margins(62px, 7px, 8px, 7px); -} - permissionsExpandIcon: icon{{ "info/edit/expand_arrow_small", windowBoldFg }}; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index fcd405a5e..e96ba3cfc 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -273,6 +273,11 @@ void ContentWidget::setViewport( }, _scroll->lifetime()); } +auto ContentWidget::titleStories() +-> rpl::producer { + return nullptr; +} + void ContentWidget::saveChanges(FnMut done) { done(); } @@ -332,6 +337,8 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; + } else if (const auto peer = storiesPeer()) { + return Stories::Tag{ peer, storiesTab() }; } else { return Downloads::Tag(); } @@ -363,4 +370,9 @@ ContentMemento::ContentMemento(Settings::Tag settings) ContentMemento::ContentMemento(Downloads::Tag downloads) { } +ContentMemento::ContentMemento(Stories::Tag stories) +: _storiesPeer(stories.peer) +, _storiesTab(stories.tab) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 44f4e2d8c..6a6f75bbd 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "info/info_wrap_widget.h" +namespace Dialogs::Stories { +struct Content; +} // namespace Dialogs::Stories + namespace Storage { enum class SharedMediaType : signed char; } // namespace Storage @@ -24,14 +28,20 @@ template class PaddingWrap; } // namespace Ui -namespace Info { -namespace Settings { +namespace Info::Settings { struct Tag; -} // namespace Settings +} // namespace Info::Settings -namespace Downloads { +namespace Info::Downloads { struct Tag; -} // namespace Downloads +} // namespace Info::Downloads + +namespace Info::Stories { +struct Tag; +enum class Tab; +} // namespace Info::Stories + +namespace Info { class ContentMemento; class Controller; @@ -82,6 +92,8 @@ public: } [[nodiscard]] virtual rpl::producer title() = 0; + [[nodiscard]] virtual auto titleStories() + -> rpl::producer; virtual void saveChanges(FnMut done); @@ -150,6 +162,7 @@ public: PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); + explicit ContentMemento(Stories::Tag stories); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { @@ -172,6 +185,12 @@ public: UserData *settingsSelf() const { return _settingsSelf; } + PeerData *storiesPeer() const { + return _storiesPeer; + } + Stories::Tab storiesTab() const { + return _storiesTab; + } PollData *poll() const { return _poll; } @@ -214,6 +233,8 @@ private: const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; UserData * const _settingsSelf = nullptr; + PeerData * const _storiesPeer = nullptr; + Stories::Tab _storiesTab = {}; PollData * const _poll = nullptr; const FullMsgId _pollContextId; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 58dda92fe..d7beb981e 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -40,6 +40,9 @@ Key::Key(Settings::Tag settings) : _value(settings) { Key::Key(Downloads::Tag downloads) : _value(downloads) { } +Key::Key(Stories::Tag stories) : _value(stories) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -72,6 +75,20 @@ bool Key::isDownloads() const { return v::is(_value); } +PeerData *Key::storiesPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +Stories::Tab Key::storiesTab() const { + if (const auto tag = std::get_if(&_value)) { + return tag->tab; + } + return Stories::Tab(); +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; @@ -249,7 +266,8 @@ bool Controller::validateMementoPeer( not_null memento) const { return memento->peer() == peer() && memento->migratedPeerId() == migratedPeerId() - && memento->settingsSelf() == settingsSelf(); + && memento->settingsSelf() == settingsSelf() + && memento->storiesPeer() == storiesPeer(); } void Controller::setSection(not_null memento) { diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index cdbf53396..82eb6f8eb 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -36,6 +36,25 @@ struct Tag { } // namespace Info::Downloads +namespace Info::Stories { + +enum class Tab { + Saved, + Archive, +}; + +struct Tag { + explicit Tag(not_null peer, Tab tab = {}) + : peer(peer) + , tab(tab) { + } + + not_null peer; + Tab tab = {}; +}; + +} // namespace Info::Stories + namespace Info { class Key { @@ -44,12 +63,15 @@ public: explicit Key(not_null topic); Key(Settings::Tag settings); Key(Downloads::Tag downloads); + Key(Stories::Tag stories); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; Data::ForumTopic *topic() const; UserData *settingsSelf() const; bool isDownloads() const; + PeerData *storiesPeer() const; + Stories::Tab storiesTab() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -63,6 +85,7 @@ private: not_null, Settings::Tag, Downloads::Tag, + Stories::Tag, PollKey> _value; }; @@ -81,6 +104,7 @@ public: Members, Settings, Downloads, + Stories, PollResults, }; using SettingsType = ::Settings::Type; @@ -123,23 +147,29 @@ class AbstractController : public Window::SessionNavigation { public: AbstractController(not_null parent); - virtual Key key() const = 0; - virtual PeerData *migrated() const = 0; - virtual Section section() const = 0; + [[nodiscard]] virtual Key key() const = 0; + [[nodiscard]] virtual PeerData *migrated() const = 0; + [[nodiscard]] virtual Section section() const = 0; - PeerData *peer() const; - PeerId migratedPeerId() const; - Data::ForumTopic *topic() const { + [[nodiscard]] PeerData *peer() const; + [[nodiscard]] PeerId migratedPeerId() const; + [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } - UserData *settingsSelf() const { + [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } - bool isDownloads() const { + [[nodiscard]] bool isDownloads() const { return key().isDownloads(); } - PollData *poll() const; - FullMsgId pollContextId() const { + [[nodiscard]] PeerData *storiesPeer() const { + return key().storiesPeer(); + } + [[nodiscard]] Stories::Tab storiesTab() const { + return key().storiesTab(); + } + [[nodiscard]] PollData *poll() const; + [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); } diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 37dfe5529..f15a6b549 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" @@ -30,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_user.h" +#include "styles/style_dialogs.h" #include "styles/style_info.h" namespace Info { @@ -88,9 +91,11 @@ void TopBar::setTitle(rpl::producer &&title) { object_ptr(this, std::move(title), _st.title), st::infoTopBarScale); _title->setDuration(st::infoTopBarDuration); - _title->toggle(!selectionMode(), anim::type::instant); + _title->toggle( + !selectionMode() && !storiesTitle(), + anim::type::instant); registerToggleControlCallback(_title.data(), [=] { - return !selectionMode() && !searchMode(); + return !selectionMode() && !storiesTitle() && !searchMode(); }); if (_back) { @@ -119,6 +124,9 @@ void TopBar::enableBackButton() { if (_title) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); } + if (_storiesWrap) { + _storiesWrap->raise(); + } updateControlsGeometry(width()); } @@ -309,12 +317,15 @@ int TopBar::resizeGetHeight(int newWidth) { void TopBar::updateControlsGeometry(int newWidth) { updateDefaultControlsGeometry(newWidth); updateSelectionControlsGeometry(newWidth); + updateStoriesGeometry(newWidth); } void TopBar::updateDefaultControlsGeometry(int newWidth) { auto right = 0; for (auto &button : _buttons) { - if (!button) continue; + if (!button) { + continue; + } button->moveToRight(right, 0, newWidth); right += button->width(); } @@ -344,6 +355,10 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) { _delete->moveToRight(right, 0, newWidth); right += _delete->width(); } + if (_canToggleStoryPin) { + _toggleStoryPin->moveToRight(right, 0, newWidth); + right += _toggleStoryPin->width(); + } if (_canForward) { _forward->moveToRight(right, 0, newWidth); right += _forward->width(); @@ -362,6 +377,31 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) { newWidth); } +void TopBar::updateStoriesGeometry(int newWidth) { + if (!_stories) { + return; + } + + auto right = 0; + for (auto &button : _buttons) { + if (!button) { + continue; + } + button->moveToRight(right, 0, newWidth); + right += button->width(); + } + const auto &small = st::dialogsStories; + const auto wrapLeft = (_back ? _st.back.width : 0); + const auto left = _back + ? 0 + : (_st.titlePosition.x() - small.left - small.photoLeft); + const auto height = small.photo + 2 * small.photoTop; + const auto top = _st.titlePosition.y() + + (_st.title.style.font->height - height) / 2; + _stories->setLayoutConstraints({ left, top }, style::al_left); + _storiesWrap->move(wrapLeft, 0); +} + void TopBar::paintEvent(QPaintEvent *e) { auto p = QPainter(this); @@ -412,6 +452,99 @@ void TopBar::updateControlsVisibility(anim::type animated) { } } +void TopBar::setStories(rpl::producer content) { + _storiesLifetime.destroy(); + delete _storiesWrap.data(); + if (content) { + using namespace Dialogs::Stories; + + auto last = std::move( + content + ) | rpl::start_spawning(_storiesLifetime); + + _storiesWrap = _storiesLifetime.make_state< + Ui::FadeWrap + >(this, object_ptr(this), st::infoTopBarScale); + registerToggleControlCallback( + _storiesWrap.data(), + [this] { return _storiesCount > 0; }); + _storiesWrap->toggle(false, anim::type::instant); + _storiesWrap->setDuration(st::infoTopBarDuration); + + const auto button = _storiesWrap->entity(); + const auto stories = Ui::CreateChild( + button, + st::dialogsStoriesListInfo, + rpl::duplicate( + last + ) | rpl::filter([](const Content &content) { + return !content.elements.empty(); + })); + const auto label = Ui::CreateChild( + button, + QString(), + _st.title); + stories->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + stories->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + const auto skip = _st.title.style.font->spacew; + label->move( + geometry.x() + geometry.width() + skip, + _st.titlePosition.y()); + }, label->lifetime()); + rpl::combine( + _storiesWrap->positionValue(), + label->geometryValue() + ) | rpl::start_with_next([=] { + button->resize( + label->x() + label->width() + _st.titlePosition.x(), + _st.height); + }, button->lifetime()); + + _stories = stories; + _stories->clicks( + ) | rpl::start_to_stream(_storyClicks, _stories->lifetime()); + + button->setClickedCallback([=] { + _storyClicks.fire({}); + }); + + rpl::duplicate( + last + ) | rpl::start_with_next([=](const Content &content) { + const auto count = int(content.elements.size()); + if (_storiesCount != count) { + const auto was = (_storiesCount > 0); + _storiesCount = count; + const auto now = (_storiesCount > 0); + if (was != now) { + updateControlsVisibility(anim::type::normal); + } + if (now) { + label->setText( + tr::lng_contacts_stories_status( + tr::now, + lt_count, + _storiesCount)); + } + updateControlsGeometry(width()); + } + }, _storiesLifetime); + + _storiesLifetime.add([weak = QPointer(label)] { + delete weak.data(); + }); + } else { + _storiesCount = 0; + } + updateControlsVisibility(anim::type::instant); +} + +void TopBar::setStoriesArchive(bool archive) { + _storiesArchive = archive; +} + void TopBar::setSelectedItems(SelectedItems &&items) { auto wasSelectionMode = selectionMode(); _selectedItems = std::move(items); @@ -439,13 +572,14 @@ rpl::producer TopBar::selectionActionRequests() const { } void TopBar::updateSelectionState() { - Expects(_selectionText && _delete && _forward); + Expects(_selectionText && _delete && _forward && _toggleStoryPin); _canDelete = computeCanDelete(); _canForward = computeCanForward(); _selectionText->entity()->setValue(generateSelectedText()); _delete->toggle(_canDelete, anim::type::instant); _forward->toggle(_canForward, anim::type::instant); + _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); updateSelectionControlsGeometry(width()); } @@ -460,6 +594,7 @@ void TopBar::createSelectionControls() { }; _canDelete = computeCanDelete(); _canForward = computeCanForward(); + _canToggleStoryPin = computeCanToggleStoryPin(); _cancelSelection = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaCancel), @@ -511,6 +646,24 @@ void TopBar::createSelectionControls() { _selectionActionRequests, _cancelSelection->lifetime()); _delete->entity()->setVisible(_canDelete); + const auto archive = + _toggleStoryPin = wrap(Ui::CreateChild>( + this, + object_ptr( + this, + _storiesArchive ? _st.storiesSave : _st.storiesArchive), + st::infoTopBarScale)); + registerToggleControlCallback( + _toggleStoryPin.data(), + [this] { return selectionMode() && _canToggleStoryPin; }); + _toggleStoryPin->setDuration(st::infoTopBarDuration); + _toggleStoryPin->entity()->clicks( + ) | rpl::map_to( + SelectionAction::ToggleStoryPin + ) | rpl::start_to_stream( + _selectionActionRequests, + _cancelSelection->lifetime()); + _toggleStoryPin->entity()->setVisible(_canToggleStoryPin); updateControlsGeometry(width()); } @@ -523,6 +676,12 @@ bool TopBar::computeCanForward() const { return ranges::all_of(_selectedItems.list, &SelectedItem::canForward); } +bool TopBar::computeCanToggleStoryPin() const { + return ranges::all_of( + _selectedItems.list, + &SelectedItem::canToggleStoryPin); +} + Ui::StringWithNumbers TopBar::generateSelectedText() const { using Type = Storage::SharedMediaType; const auto phrase = [&] { @@ -534,6 +693,7 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const { case Type::MusicFile: return tr::lng_media_selected_song; case Type::Link: return tr::lng_media_selected_link; case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + case Type::PhotoVideo: return tr::lng_stories_row_count; } Unexpected("Type in TopBar::generateSelectedText()"); }(); @@ -548,6 +708,10 @@ bool TopBar::selectionMode() const { return !_selectedItems.list.empty(); } +bool TopBar::storiesTitle() const { + return _storiesCount > 0; +} + bool TopBar::searchMode() const { return _searchModeAvailable && _searchModeEnabled; } diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h index a2ed052c5..496166954 100644 --- a/Telegram/SourceFiles/info/info_top_bar.h +++ b/Telegram/SourceFiles/info/info_top_bar.h @@ -18,11 +18,17 @@ namespace style { struct InfoTopBar; } // namespace style +namespace Dialogs::Stories { +class List; +struct Content; +} // namespace Dialogs::Stories + namespace Window { class SessionNavigation; } // namespace Window namespace Ui { +class AbstractButton; class IconButton; class FlatLabel; class InputField; @@ -43,11 +49,16 @@ public: const style::InfoTopBar &st, SelectedItems &&items); - auto backRequest() const { + [[nodiscard]] auto backRequest() const { return _backClicks.events(); } + [[nodiscard]] auto storyClicks() const { + return _storyClicks.events(); + } void setTitle(rpl::producer &&title); + void setStories(rpl::producer content); + void setStoriesArchive(bool archive); void enableBackButton(); void highlight(); @@ -95,6 +106,7 @@ private: void updateControlsGeometry(int newWidth); void updateDefaultControlsGeometry(int newWidth); void updateSelectionControlsGeometry(int newWidth); + void updateStoriesGeometry(int newWidth); Ui::FadeWrap *pushButton( base::unique_qptr button); void forceButtonVisibility( @@ -104,16 +116,19 @@ private: void startHighlightAnimation(); void updateControlsVisibility(anim::type animated); - bool selectionMode() const; - bool searchMode() const; - Ui::StringWithNumbers generateSelectedText() const; + [[nodiscard]] bool selectionMode() const; + [[nodiscard]] bool storiesTitle() const; + [[nodiscard]] bool searchMode() const; + [[nodiscard]] Ui::StringWithNumbers generateSelectedText() const; [[nodiscard]] bool computeCanDelete() const; [[nodiscard]] bool computeCanForward() const; + [[nodiscard]] bool computeCanToggleStoryPin() const; void updateSelectionState(); void createSelectionControls(); void performForward(); void performDelete(); + void performToggleStoryPin(); void setSearchField( base::unique_qptr field, @@ -147,16 +162,25 @@ private: QPointer _searchField; rpl::event_stream<> _backClicks; + rpl::event_stream _storyClicks; SelectedItems _selectedItems; bool _canDelete = false; bool _canForward = false; + bool _canToggleStoryPin = false; + bool _storiesArchive = false; QPointer> _cancelSelection; QPointer> _selectionText; QPointer> _forward; QPointer> _delete; + QPointer> _toggleStoryPin; rpl::event_stream _selectionActionRequests; + QPointer> _storiesWrap; + QPointer _stories; + rpl::lifetime _storiesLifetime; + int _storiesCount = 0; + using UpdateCallback = Fn; std::map _updateControlCallbacks; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 0c5ba45ed..214e191fb 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -244,6 +244,12 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const { return Dialogs::RowDescriptor( peer->owner().history(peer), FullMsgId()); + } else if (const auto storiesPeer = key().storiesPeer()) { + return (key().storiesTab() == Stories::Tab::Saved) + ? Dialogs::RowDescriptor( + storiesPeer->owner().history(storiesPeer), + FullMsgId()) + : Dialogs::RowDescriptor(); } else if (key().settingsSelf() || key().isDownloads() || key().poll()) { return Dialogs::RowDescriptor(); } @@ -297,6 +303,11 @@ void WrapWidget::createTopBar() { _controller->parentController()->closeThirdSection(); }); } + _topBar->storyClicks() | rpl::start_with_next([=] { + if (const auto peer = _controller->key().peer()) { + _controller->parentController()->openPeerStories(peer->id); + } + }, _topBar->lifetime()); if (wrapValue == Wrap::Layer) { auto close = _topBar->addButton( base::make_unique_q( @@ -573,6 +584,9 @@ void WrapWidget::finishShowContent() { _content->setIsStackBottom(!hasStackHistory()); if (_topBar) { _topBar->setTitle(_content->title()); + _topBar->setStories(_content->titleStories()); + _topBar->setStoriesArchive( + _controller->key().storiesTab() == Stories::Tab::Archive); } _desiredHeights.fire(desiredHeightForContent()); _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index a04c4d89c..47215d340 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -58,6 +58,7 @@ struct SelectedItem { GlobalMsgId globalId; bool canDelete = false; bool canForward = false; + bool canToggleStoryPin = false; }; struct SelectedItems { @@ -73,6 +74,7 @@ enum class SelectionAction { Clear, Forward, Delete, + ToggleStoryPin, }; class WrapWidget final : public Window::SectionWidget { diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 1780d741a..9d3693280 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include "lang/lang_keys.h" +#include "data/data_stories_ids.h" #include "storage/storage_shared_media.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" +#include "info/stories/info_stories_widget.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" @@ -124,4 +126,29 @@ inline auto AddCommonGroupsButton( return result; }; +inline auto AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker) { + auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds( + user, + ServerMaxStoryId - 1, + 0 + ) | rpl::map([](const Data::StoriesIdsSlice &slice) { + return slice.fullCount().value_or(0); + })); + auto result = AddCountedButton( + parent, + std::move(count), + [](int count) { + return tr::lng_profile_saved_stories(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection(Info::Stories::Make(user)); + }); + return result; +}; + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index 20c3051ca..27e8d4b9f 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -30,15 +30,12 @@ struct ListItemSelectionData { TextSelection text; bool canDelete = false; bool canForward = false; -}; + bool canToggleStoryPin = false; -inline bool operator==( - ListItemSelectionData a, - ListItemSelectionData b) { - return (a.text == b.text) - && (a.canDelete == b.canDelete) - && (a.canForward == b.canForward); -} + friend inline bool operator==( + ListItemSelectionData, + ListItemSelectionData) = default; +}; using ListSelectedMap = base::flat_map< not_null, diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h index a7a2ce61d..4e305de40 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h @@ -20,10 +20,10 @@ class SearchFieldController; } // namespace Ui namespace Info { - class Controller; +} // namespace Info -namespace Media { +namespace Info::Media { class Memento; class ListWidget; @@ -86,5 +86,4 @@ private: }; -} // namespace Media -} // namespace Info +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp index d956d4d76..2bf957192 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -338,15 +338,19 @@ void ListSection::resizeToWidth(int newWidth) { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: case Type::RoundFile: { - _itemsLeft = st::infoMediaSkip; + const auto skip = st::infoMediaSkip; + _itemsLeft = st::infoMediaLeft; _itemsTop = st::infoMediaSkip; - _itemsInRow = (newWidth - _itemsLeft) - / (st::infoMediaMinGridSize + st::infoMediaSkip); - _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) + _itemsInRow = (newWidth - _itemsLeft * 2 + skip) + / (st::infoMediaMinGridSize + skip); + _itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow) - st::infoMediaSkip; + _itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip) + / 2; for (auto &item : _items) { - item->resizeGetHeight(_itemWidth); + _itemHeight = item->resizeGetHeight(_itemWidth); } } break; @@ -375,8 +379,9 @@ int ListSection::recountHeight() { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: case Type::RoundFile: { - auto itemHeight = _itemWidth + st::infoMediaSkip; + auto itemHeight = _itemHeight + st::infoMediaSkip; auto index = 0; result += _itemsTop; for (auto &item : _items) { diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.h b/Telegram/SourceFiles/info/media/info_media_list_section.h index 77666a31a..358712a75 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.h +++ b/Telegram/SourceFiles/info/media/info_media_list_section.h @@ -82,6 +82,7 @@ private: int _itemsLeft = 0; int _itemsTop = 0; int _itemWidth = 0; + int _itemHeight = 0; int _itemsInRow = 1; mutable int _rowsCount = 0; int _top = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 48e00a41b..ae029cf9e 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" #include "layout/layout_selection.h" @@ -21,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_download_manager.h" @@ -30,6 +32,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" +#include "media/stories/media_stories_controller.h" // ...TogglePinnedToast. +#include "media/stories/media_stories_share.h" // PrepareShareBox. #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "ui/widgets/popup_menu.h" @@ -53,6 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "core/file_utilities.h" #include "core/application.h" +#include "ui/toast/toast.h" #include "styles/style_overview.h" #include "styles/style_info.h" #include "styles/style_layers.h" @@ -88,6 +93,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->storiesPeer()) { + return std::make_unique(controller); } return std::make_unique(controller); } @@ -126,6 +133,7 @@ ListWidget::DateBadge::DateBadge( , hideTimer(std::move(hideCallback)) , goodType(type == Type::Photo || type == Type::Video + || type == Type::PhotoVideo || type == Type::GIF) { } @@ -171,6 +179,9 @@ void ListWidget::start() { ) | rpl::start_with_next([this](QString &&query) { _provider->setSearchQuery(std::move(query)); }, lifetime()); + } else if (_controller->storiesPeer()) { + trackSession(&session()); + restart(); } else { trackSession(&session()); @@ -250,6 +261,7 @@ void ListWidget::selectionAction(SelectionAction action) { case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Delete: deleteSelected(); return; + case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; } } @@ -327,6 +339,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { auto result = SelectedItem(item->globalId()); result.canDelete = selection.canDelete; result.canForward = selection.canForward; + result.canToggleStoryPin = selection.canToggleStoryPin; return result; }; auto transformation = [&](const auto &item) { @@ -341,6 +354,12 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { std::back_inserter(items.list), transformation); } + if (_controller->storiesPeer() && items.list.size() > 1) { + // Don't allow forwarding more than one story. + for (auto &entry : items.list) { + entry.canForward = false; + } + } return items; } @@ -469,18 +488,31 @@ bool ListWidget::tooltipWindowActive() const { } void ListWidget::openPhoto(not_null photo, FullMsgId id) { - _controller->parentController()->openPhoto(photo, id, topicRootId()); + using namespace Data; + + const auto tab = _controller->storiesTab(); + const auto context = (tab == Stories::Tab::Archive) + ? Data::StoriesContext{ Data::StoriesContextArchive() } + : Data::StoriesContext{ Data::StoriesContextSaved() }; + _controller->parentController()->openPhoto( + photo, + { id, topicRootId() }, + _controller->storiesPeer() ? &context : nullptr); } void ListWidget::openDocument( not_null document, FullMsgId id, bool showInMediaView) { + const auto tab = _controller->storiesTab(); + const auto context = (tab == Stories::Tab::Archive) + ? Data::StoriesContext{ Data::StoriesContextArchive() } + : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openDocument( document, - id, - topicRootId(), - showInMediaView); + showInMediaView, + { id, topicRootId() }, + _controller->storiesPeer() ? &context : nullptr); } void ListWidget::trackSession(not_null session) { @@ -516,6 +548,7 @@ void ListWidget::refreshRows() { resizeToWidth(width()); restoreScrollState(); mouseActionUpdate(); + update(); } bool ListWidget::preventAutoHide() const { @@ -883,6 +916,11 @@ void ListWidget::showContextMenu( auto canForwardAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canForward; + }) && (!_controller->key().storiesPeer() || _selected.size() == 1); + }; + auto canToggleStoryPinAll = [&] { + return ranges::none_of(_selected, [](auto &&item) { + return !item.second.canToggleStoryPin; }); }; @@ -984,6 +1022,18 @@ void ListWidget::showContextMenu( } } if (overSelected == SelectionState::OverSelectedItems) { + if (canToggleStoryPinAll()) { + const auto tab = _controller->key().storiesTab(); + const auto pin = (tab == Stories::Tab::Archive); + _contextMenu->addAction( + (pin + ? tr::lng_mediaview_save_to_profile + : tr::lng_archived_add)(tr::now), + crl::guard(this, [this] { toggleStoryPinSelected(); }), + (pin + ? &st::menuIconStoriesSave + : &st::menuIconStoriesArchive)); + } if (canForwardAll()) { _contextMenu->addAction( tr::lng_context_forward_selected(tr::now), @@ -1013,6 +1063,20 @@ void ListWidget::showContextMenu( const auto selectionData = _provider->computeSelectionData( item, FullSelection); + if (selectionData.canToggleStoryPin) { + const auto tab = _controller->key().storiesTab(); + const auto pin = (tab == Stories::Tab::Archive); + _contextMenu->addAction( + (pin + ? tr::lng_mediaview_save_to_profile + : tr::lng_mediaview_archive_story)(tr::now), + crl::guard(this, [=] { + toggleStoryPin({ 1, globalId.itemId }); + }), + (pin + ? &st::menuIconStoriesSave + : &st::menuIconStoriesArchive)); + } if (selectionData.canForward) { _contextMenu->addAction( tr::lng_context_forward_msg(tr::now), @@ -1034,6 +1098,20 @@ void ListWidget::showContextMenu( } } } + if (const auto peer = _controller->key().storiesPeer()) { + if (!peer->isSelf() && IsStoryMsgId(globalId.itemId.msg)) { + const auto storyId = FullStoryId{ + globalId.itemId.peer, + StoryIdFromMsgId(globalId.itemId.msg), + }; + _contextMenu->addAction( + tr::lng_profile_report(tr::now), + [=] { ::Media::Stories::ReportRequested( + _controller->uiShow(), + storyId); }, + &st::menuIconReport); + } + } if (!_provider->hasSelectRestriction()) { _contextMenu->addAction( tr::lng_context_select_msg(tr::now), @@ -1088,15 +1166,25 @@ void ListWidget::forwardItem(GlobalMsgId globalId) { } void ListWidget::forwardItems(MessageIdsList &&items) { - auto callback = [weak = Ui::MakeWeak(this)] { - if (const auto strong = weak.data()) { - strong->clearSelected(); + if (_controller->storiesPeer()) { + if (items.size() == 1 && IsStoryMsgId(items.front().msg)) { + const auto id = items.front(); + _controller->parentController()->show( + ::Media::Stories::PrepareShareBox( + _controller->parentController()->uiShow(), + { id.peer, StoryIdFromMsgId(id.msg) })); } - }; - setActionBoxWeak(Window::ShowForwardMessagesBox( - _controller, - std::move(items), - std::move(callback))); + } else { + auto callback = [weak = Ui::MakeWeak(this)] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }; + setActionBoxWeak(Window::ShowForwardMessagesBox( + _controller, + std::move(items), + std::move(callback))); + } } void ListWidget::deleteSelected() { @@ -1105,6 +1193,48 @@ void ListWidget::deleteSelected() { })); } +void ListWidget::toggleStoryPinSelected() { + toggleStoryPin(collectSelectedIds(), crl::guard(this, [=] { + clearSelected(); + })); +} + +void ListWidget::toggleStoryPin( + MessageIdsList &&items, + Fn confirmed) { + auto list = std::vector(); + for (const auto &id : items) { + if (IsStoryMsgId(id.msg)) { + list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); + } + } + const auto count = int(list.size()); + const auto pin = (_controller->storiesTab() == Stories::Tab::Archive); + const auto controller = _controller; + const auto sure = [=](Fn close) { + controller->session().data().stories().togglePinnedList(list, pin); + controller->showToast( + ::Media::Stories::PrepareTogglePinnedToast(count, pin)); + close(); + if (confirmed) { + confirmed(); + } + }; + const auto onePhrase = pin + ? tr::lng_stories_save_sure + : tr::lng_stories_archive_sure; + const auto manyPhrase = pin + ? tr::lng_stories_save_sure_many + : tr::lng_stories_archive_sure_many; + _controller->parentController()->show(Ui::MakeConfirmBox({ + .text = (count == 1 + ? onePhrase() + : manyPhrase(lt_count, rpl::single(count) | tr::to_count())), + .confirmed = sure, + .confirmText = tr::lng_box_ok(), + })); +} + void ListWidget::deleteItem(GlobalMsgId globalId) { if (const auto item = MessageByGlobalId(globalId)) { auto items = SelectedItems(_provider->type()); @@ -1113,7 +1243,6 @@ void ListWidget::deleteItem(GlobalMsgId globalId) { item, FullSelection); items.list.back().canDelete = selectionData.canDelete; - items.list.back().canForward = selectionData.canForward; deleteItems(std::move(items)); } } @@ -1159,6 +1288,33 @@ void ListWidget::deleteItems(SelectedItems &&items, Fn confirmed) { .confirmText = tr::lng_box_delete(tr::now), .confirmStyle = &st::attentionBoxButton, }))); + } else if (_controller->storiesPeer()) { + auto list = std::vector(); + for (const auto &item : items.list) { + const auto id = item.globalId.itemId; + if (IsStoryMsgId(id.msg)) { + list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); + } + } + const auto session = &_controller->session(); + const auto sure = [=](Fn close) { + session->data().stories().deleteList(list); + close(); + if (confirmed) { + confirmed(); + } + }; + const auto count = int(list.size()); + window->show(Ui::MakeConfirmBox({ + .text = (count == 1 + ? tr::lng_stories_delete_one_sure() + : tr::lng_stories_delete_sure( + lt_count, + rpl::single(count) | tr::to_count())), + .confirmed = sure, + .confirmText = tr::lng_selected_delete(), + .confirmStyle = &st::attentionBoxButton, + })); } else if (auto list = collectSelectedIds(items); !list.empty()) { auto box = Box( &_controller->session(), @@ -1787,6 +1943,7 @@ void ListWidget::applyDragSelection(SelectedMap &applyTo) const { void ListWidget::refreshHeight() { resize(width(), recountHeight()); + update(); } int ListWidget::recountHeight() { diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index e9149d336..10984f27e 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -169,7 +169,6 @@ private: void itemRemoved(not_null item); void itemLayoutChanged(not_null item); - void refreshViewer(); void refreshRows(); void trackSession(not_null session); @@ -190,8 +189,12 @@ private: void forwardItem(GlobalMsgId globalId); void forwardItems(MessageIdsList &&items); void deleteSelected(); + void toggleStoryPinSelected(); void deleteItem(GlobalMsgId globalId); void deleteItems(SelectedItems &&items, Fn confirmed = nullptr); + void toggleStoryPin( + MessageIdsList &&items, + Fn confirmed = nullptr); void applyItemSelection( HistoryItem *item, TextSelection selection); diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index f2d8dcde7..3e75975fa 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -421,19 +421,21 @@ std::unique_ptr Provider::createLayout( } return nullptr; }; - const auto spoiler = [&] { - if (const auto media = item->media()) { - return media->hasSpoiler(); - } - return false; - }; const auto &songSt = st::overviewFileLayout; using namespace Overview::Layout; + const auto options = [&] { + const auto media = item->media(); + return MediaOptions{ .spoiler = media && media->hasSpoiler() }; + }; switch (type) { case Type::Photo: if (const auto photo = getPhoto()) { - return std::make_unique(delegate, item, photo, spoiler()); + return std::make_unique( + delegate, + item, + photo, + options()); } return nullptr; case Type::GIF: @@ -443,7 +445,7 @@ std::unique_ptr Provider::createLayout( return nullptr; case Type::Video: if (const auto file = getFile()) { - return std::make_unique